React Hooks: A Companion Guide for React Beginner Projects
This guide is specifically designed to provide you with the foundational knowledge of React Hooks for the ‘React Beginner Projects’ tutorials which focus on the practical implementation of Hooks in various projects.
Before diving into Hooks, we need to know about state, prop, event handler, and rendering in React. Let’s explore them first!
State
State is a component’s memory that keeps information. The state can hold any JavaScript value, including arrays and objects. However, there is something different about how we treat arrays and objects in state. In React we treat all states as immutable, including arrays and objects, even if they are mutable.
When we want to update an object or update an array stored in a state, we need to create a new one or make a copy of an existing one, then set the state to use that copy.
We shouldn’t directly change objects or arrays in the React state. Mutating them will not trigger renders, which will cause the UI not to represent the component’s data accurately. We need to treat objects and arrays in state as read-only.
However, there are times when we want to include existing data as part of a new object or array. In this situation, we can use the spread syntax ...
. Look at this example below:
//Object spread syntax to create copies of objects
setObject({
...obj, // Copy the old fields
something: 'newValue'// But override this one
});
//Array spread syntax to create arrays with new items
setArray(
[
...arr, // Copy the old fields
newItem// and one new item at the end
]
);
Spread syntax is shallow. It only copies one level deep.
Props
Props are pieces of information that you pass to a JSX tag. React components use props to communicate with each other. Every parent component can pass some information to its child components by giving them props. Props can be any JavaScript value, including objects, arrays, and functions.
//Here we passed 2 props, person and size.
<Avatar
person={person}
size={size}
/>
Notes:
- Every render receives a new version of props. This ensures that the component always displays the most up-to-date information based on the props passed to it from its parent component.
- Props are read-only, you can’t change props. Props allow components to receive information and customize their behavior.
If you need to change a value, what you actually need is not props, but state. Use state for managing dynamic data within a component.
Event Handler
An event handler is a function that will be triggered in response to interaction such as clicking, hovering, or focusing on form inputs.
There are two ways to write an event handler:
1. Add an event handler by defining a function and then passing it as a prop to the appropriate JSX tag. Look at this example below:
export default function Button() {
function handleCount() {
count=count+1
}
return (
<button onClick={handleCount}>
Count Me
</button>
);
}
2. Add an event handler by defining a function inline in the JSX.
<button onClick={function handleCount() {
count=count+1
}}> Count Me </button>
//We can also use an arrow function
<button onClick={() => {
count=count+1
}}> Count Me </button>
Notes:
- Event handler functions have names that start with “handle”, followed by the name of the event, such as
handleClick
andhandleCount
. - An event handler is defined inside a component, allowing it to access props.
- We can declare an event handler in a parent component and then pass it to a child component as a prop.
- When passing an event handler as a prop, we should only pass it without invoking it. Look at this comparation below:
- Event handler props should start with
on
, followed by a capital letter, likeonClick
,onSmash
, etc. - The built-in browser elements, such as <button>, <div>, or <form>, only support the standard browser event names, such as
onClick
,onChange
, oronSubmit
.
import { useState } from 'react';
export default function Form() {
const [name, setName] = useState('John Doe');
return (
<>
<label>
Name:
{/*We use onChange handler so we can update the name state as we type.
This ensures that the input field reflects the user’s input,
and the name state stays in sync. */}
<input
value={name}
onChange={e => setName(e.target.value)}
/>
</label>
<span>{name}</span>
</>
);
}
Rendering
Rendering is the process when React calls your components to figure out what to display on the screen. When rendering occurs, React will construct and update the UI based on the component’s data and logic. It ensures that users see an accurate representation of the application’s state.
There are three steps of how React renders components on screen:
1. Triggering a render
A component renders for two reasons. First during its initial render and second when the component’s state or its parent component’s state is updated.
Updating your component’s state automatically queues a render. This means when we update multiple states within a function component, React batches those state updates together and performs a single re-render.
Check this example below:
We have three states,
stateA
,stateB
, andstateC
. If we update all three states within an event handler or any other function, React doesn’t immediately re-render the component for each individual state change.Instead, React queues up these state updates as a batch. It waits until all synchronous code (including event handlers) has finished executing. Once the call stack is clear, React processes the scheduled re-render for the entire batch of state changes.
After batching the state updates, React performs a single re-render to reflect the combined changes. It efficiently computes the new virtual DOM and updates the actual DOM accordingly.
This batching behavior is an optimization. It minimizes the number of re-renders and ensures that the UI remains responsive. Imagine if React triggered a separate re-render for each state update, it would be inefficient and lead to unnecessary rendering cycles.
2. Rendering the component
In this step, React will call the function component whose state update triggered the render. During the initial render, React creates the DOM nodes.
During a re-render, React will calculate which of their properties have changed since the previous render. However, it doesn’t take any action based on that information until the next step: the commit phase.
The rendering process in React is recursive. When an updated component returns another component, React renders that component. If that component also returns additional components, React continues to render them, and so on. The process will continue until there are no more nested components.
3. Committing to the DOM
After rendering the components, React will modify the DOM. For the initial render, React will use the appendChild()
DOM API to put all the DOM nodes it has created on the screen. And for re-renders, React only updates the DOM nodes if there’s a difference between renders. React will compare the new and old virtual DOM and update the real DOM only for the parts that have changed.
The Virtual DOM is a representation of the UI that is kept in memory and synced with the real DOM. Updating the virtual DOM is much faster than updating the real DOM because there’s nothing to display in the UI. The real DOM will be updated after React compares the difference between the new and old virtual DOM.
After rendering is complete and React has updated the DOM, the browser will repaint the screen (browser rendering).
The Hooks
Hooks are special functions that allow functional components to manage state, side effects, and other React features without writing class components. We can use built-in Hooks or combine them to create our own custom Hooks.
Notes:
- Hooks must be called at the top level of your functional component or within custom hooks that you create.
function MyComponent() {
//Top-level placement
const [count, setCount] = useState(0);
...
}
- You cannot call hooks inside loops, conditions, event handlers, nested functions or any non-top-level code.
//Call hook inside condition
function MyComponent() {
if (someCondition) {
const [count, setCount] = useState(0);
}
...
}
//Using hooks inside loop
function MyComponent() {
const items = ['Apple', 'Banana', 'Orange'];
const [count, setCount] = useState(0);
return (
<div>
<h1>Incorrect Item Counters</h1>
{items.map((item) => (
<div key={item}>
<h3>{item}</h3>
<p>Count: {count}</p>
{/* All items in the loop share the same state (count),
which will display the same count value.
Each item should have its own independent state*/}
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
))}
</div>
);
}
If you need to use hooks inside a loop, condition, or any other non-top-level code, extract that logic into a separate component. Create a new functional component and move the state and hooks into it.
From the example of using hooks inside loop code above, I’ll show you how to fix it by creating a separate component to manage the state for each item individually:
ItemCounter.js
import React, { useState } from 'react';
const ItemCounter = ({ itemName }) => {
const [count, setCount] = useState(0);
const increment = () => {
setCount(count + 1);
};
return (
<div>
<h3>{itemName}</h3>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
};
export default ItemCounter;
ItemList.js
import React from 'react';
import ItemCounter from './ItemCounter';
const ItemList = () => {
const items = ['Apple', 'Banana', 'Orange'];
return (
<div>
<h1>Item Counters</h1>
{items.map((item) => (
<ItemCounter key={item} itemName={item} />
))}
</div>
);
};
export default ItemList;
Alright, now let’s talk about built-in Hooks.
State Hooks
There are two hooks that can be used to add state to a component:
useState
useState is a hook that lets us add a state variable in the component. This hook returns an array with two values, which are the current state and the set function.
This is how we define the useState:
const [state, setState] = useState(initialState)
Notes:
- During the first render, the current state will match the
initialState
we passed.initialState
is the starter value of the state. - The
initialState
can be a value of any type including a function. The function that gets passed as aninitialState
will be an initializer function. React will call the function when initializing the component and store its return value as the initial state. This argument is ignored after the initial render.
//This is the correct way to use initializer function:
const [todos, setTodos] = useState(createInitialTodos);
//Not like this:
const [todos, setTodos] = useState(createInitialTodos());
//Although the result of createInitialTodos() is only used
//for the initial render, we're still calling this function on every render
The initializer function should take no arguments, should return a value of any type, and should be pure (the function should always return the same output for the same input and should not modify any external state or have any side effects),
- The
set
function lets you update the state to a different value and trigger a re-render. We can pass the next state directly, or a function that calculates it from the previous state.
If you pass a function as the next state, it will be treated as an updater function. It must be pure, should take the current state as its only argument, and should return the next state.
- The
set
function only updates the state variable for the next render. If you read the state variable after calling theset
function, you will still get the old value that was on the screen before your call.
//This is the incorrect way to update the state
function handleClick() {
setAge(age + 1); // setAge(42 + 1)
setAge(age + 1); // setAge(42 + 1)
setAge(age + 1); // setAge(42 + 1)
//After one click, age will only be 43 rather than 45
//Calling the set function does not update the age state variable
//in the already running code
}
//This is the correct way to update the state by calculating it from
//the previous state. We pass an updater function to setAge instead
//of the next state
function handleClick() {
setAge(a => a + 1); // setAge(42 => 43)
setAge(a => a + 1); // setAge(43 => 44)
setAge(a => a + 1); // setAge(44 => 45)
}
- If the new value you provide is identical to the current
state
, as determined by anObject.is
comparison, React will skip re-rendering the component and its children. This is an optimization. - You can reset a component’s state by passing a different
key
to a component.
import { useState } from 'react';
export default function App() {
//The Reset button changes the version state variable,
//which we pass as a key to the Form
const [version, setVersion] = useState(0);
//When the key changes, React re-creates the Form component
//including all of its children from scratch, so its state gets reset
function handleReset() {
setVersion(version + 1);
}
return (
<>
<button onClick={handleReset}>Reset</button>
<Form key={version} />
</>
);
}
function Form() {
const [name, setName] = useState('Taylor');
return (
<>
<input
value={name}
onChange={e => setName(e.target.value)}
/>
<p>Hello, {name}.</p>
</>
);
}
useReducer
useReducer is a React Hook that lets you add a reducer to your component.
A reducer is a single function outside component that keeps all of logic in one place. With useReducer, we can extract the state logic from the components into a separate reducer function.
This is how to define useReducer:
const [state, dispatch] = useReducer(reducer, initialArg, init?)
useReducer returns an array with two elements, the current state
and a dispatch
function. The dispatch
function is a function that updates the state
to a different value and trigger a re-render.
useReducer is called with three arguments: a reducer
function, an initialArg
, and an init
function as an optional.
- A
reducer
is a function that is used to manage thestate
and it takes two parameters, thecurrent state
and anaction
, then returns the newstate
. initialArg
is the initialstate
that we passed to useReducer.- The
init
function is used to set up theinitial state
. When you use theinit
function, theinitialArg
is passed to theinit
function, which then returns the actualinitial state
. - If you don’t use an
init
function, theinitialArg
is used directly as theinitial state
. - When defining a reducer avoid recreating the initial state.
function createInitialState(username) {
// ...
}
//The correct way
function TodoList({ username }) {
const [state, dispatch] = useReducer(reducer, username, createInitialState);
//The incorrect way
const [state, dispatch] = useReducer(reducer, username, createInitialState(username));
Notes:
- React will pass the current state and the action to the reducer function. The reducer will calculate and return the next state. React will store that next state, render your component with it, and update the UI.
//The example of a reducer function
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
return {
name: state.name,
age: state.age + 1
};
}
case 'changed_name': {
return {
name: action.nextName,
age: state.age
};
}
default:
throw Error('Unknown action: ' + action.type);
}
}
It is common to write the reducer function as a
switch
statement to handle theactions
. For eachcase
in theswitch
, it will return some next state.
- The
type
property specifies the type of the action that we want to perform. - When we want to use the reducer function in a component we can call the dispatch function inside an event handler.
function Form() {
const [state, dispatch] = useReducer(reducer, { name: 'Taylor', age: 42 });
function handleButtonClick() {
dispatch({ type: 'incremented_age' });
}
function handleInputChange(e) {
dispatch({
type: 'changed_name',
nextName: e.target.value
});
}
// ...
Actions can have any type of value. But it’s common to pass objects with a
type
property identifying the action. It should include the minimal necessary information that the reducer needs to compute the next state.
- When we dispatched an action but logging still gives us the old state value, we can manually call the reducer. This all because when we are updating the state, it does not affect the
state
variable in already-running event handler.
function handleClick() {
console.log(state.age); // 42
dispatch({ type: 'incremented_age' }); // Request a re-render with 43
//Updating state does not affect the state variable in already-running code
//The updated state value is not available until the next render
console.log(state.age); // Still 42!
}
//If we need to guess the next state value, we can calculate it
//manually by calling the reducer function
const action = { type: 'incremented_age' };
dispatch(action);
const nextState = reducer(state, action);
console.log(state); // { age: 42 }
console.log(nextState); // { age: 43 }
useReducer is very similar to useState, but it lets you move the state update logic from event handlers into a single function outside of your component. This means we can manage multiple states using a single reducer function, which can reduce the complexity of state logic.
useReducer can also help optimize the performance. When we want to pass the
dispatch
function down to child components, we can pass it directly and it would not change between re-renders.
useContext Hook
useContext is a React Hook that lets you read and subscribe to context from your component. To use useContext we need to define the context first.
- Create Context:
Context is a mechanism for sharing data between components without having to pass it explicitly through props. It allows you to create a global state that can be accessed by any component in your application.
Context prevents “prop drilling”, which is the process of passing data from one component to other components based on hierarchy, until the data reaches the target component, even if the other components don’t need the data.
Context lets a component receive information from top-level components to lower-level components without worrying about component hierarchy.
This is how we create a context:
const SomeContext = createContext(defaultValue)
Notes:
defaultValue
is the initial value that the context will have when there is no matching context provider above the component that reads the context. When we don’t have any meaningful default value, we specify it asnull
.
When a component tries to access context data using
useContext
, React looks up the component tree to find the closest context provider, then uses the context data. If there is no context provider, thedefaultValue
will be used. Look at this example below:
const ThemeContext = createContext('light'); // Default theme is 'light'
// In the component tree:
<ThemeContext.Provider value="dark">
{/* Components */}
</ThemeContext.Provider>
If a component deeper in the tree tries to access the theme context but there’s no theme provider wrapping it, it will use the defaultValue (‘light’). And if we forget to specify
value
, the default value is undefined.
createContext
returns a context object.- SomeContext is the context object and it does not hold any information. It only represents the kind of information you can provide or read from components.
2. Provide Context Data
After we create context, we wrap the component tree with a context provider. With the provider, we can pass the data we want to share via the value
prop.
function App() {
const [activity, setActivity] = useState(null);
return (
<SomeContext.Provider value={activity}>
<ChildComponent1/>
<ChildComponent2/>
</SomeContext.Provider>
);
}
Note:
- We can override the context provider value by wrapping that part in a provider with a different value.
<ThemeContext.Provider value="dark">
...
<ThemeContext.Provider value="light">
<Footer />
</ThemeContext.Provider>
...
</ThemeContext.Provider>
<ThemeContext.Provider>
3. Consume Context Data
To use the context in child components, we need to access it using the useContext. This is how we call the useContext:
const value = useContext(SomeContext)
Notes:
- useContext returns the context value from the context we passed which is
SomeContext
.
function ChildComponent1() {
const value = useContext(SomeContext)
return (
<h1>This is the value : {value}</h1>
);
}
- With
useContext
, the child component will only re-render when thecontext
value changes. Usually when a parent component re-renders, every child component will re-render too even if the components don’t depend on the update state or props.
useRef Hook
useRef is a React Hook that returns a ref object with a single current
property which is set to the initial value. With ref we can keep information that we don’t want to trigger new renders, this is because ref is a plain JavaScript object. React is not aware when we change it.
We can mutate the
ref.current
property. Unlike state, it is mutable. But if it holds an object that is used for rendering, then you shouldn’t mutate that object.
This is how we call the useRef:
const ref = useRef(initialValue)
Notes:
initialValue
is the value we want the ref object’scurrent
property to be initially. It can be a value of any type. This argument is ignored after the initial render.- Avoiding recreating the ref contents like this:
const ref = useRef(initialValue())
//The result is only used for the initial render,
// but we're still calling this function on every render.
- To update the value inside the ref, we need to manually change its
current
property.
import { useRef } from 'react';
export default function Counter() {
let ref = useRef(0);
function handleClick() {
//This is how to update the value inside the ref
ref.current = ref.current + 1;
alert('You clicked ' + ref.current + ' times!');
}
return (
<button onClick={handleClick}>
Click me!
</button>
);
}
- We can use a ref to manipulate the DOM. To do this, we need to declare a ref object with an initial value of
null
, then pass your ref object as theref
attribute to the JSX.
import { useRef } from 'react';
function MyComponent() {
const inputRef = useRef(null);
//How to access the DOM
function handleClick() {
inputRef.current.focus();
}
// ...
return <input ref={inputRef} />;
After React creates the DOM node and puts it on the screen, React will set the
current
property to the DOM node. React will set thecurrent
property back tonull
when the node is removed from the screen.
- If you pass the ref object to React as a
ref
attribute to a JSX node, React will set itscurrent
property. - Do not write or read
ref.current
during rendering, except for initialization.
//The incorrect ways for updating the ref value
function MyComponent() {
// ...
// Write a ref during rendering
myRef.current = 123;
// ...
//Read a ref during rendering
return <h1>{myOtherRef.current}</h1>;
}
//The correct ways for updating the ref value
function MyComponent() {
// ...
useEffect(() => {
// Read or write refs in effects
myRef.current = 123;
});
// ...
function handleClick() {
// Read or write refs in event handlers
doSomething(myOtherRef.current);
}
// ...
}
- Don’t try to pass a ref to a component. A component doesn’t expose refs to the DOM nodes inside them. If you want to do it, then use
forwardRef
.
const inputRef = useRef(null);
//The incorrect way to pass a ref
return <MyInput ref={inputRef} />;
forwardRef
forwardRef
lets your component expose a DOM node to parent component with a ref. By default, each component’s DOM nodes are private. But, sometimes you might need to interact directly with a DOM element or a child component from a parent component. This is when you need to use the forwardRef.
const SomeComponent = forwardRef(render)
Notes:
forwardRef
accepts a render function as an argument. React calls this function with theprops
andref
that the component received from its parent. The JSX we return will be the output of your component.
app.js
import { useRef } from 'react';
import MyVideoPlayer from './MyVideoPlayer.js';
export default function App() {
//The App component defines a ref and passes it to
//the MyVideoPlayer component.
const ref = useRef(null);
return (
<>
<button onClick={() => ref.current.play()}>
Play
</button>
<button onClick={() => ref.current.pause()}>
Pause
</button>
<br />
{/*Passing a ref to MyVideoPlayer Component allows the app
component play and pause the <video> node within it */}
<MyVideoPlayer
ref={ref}
src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
type="video/mp4"
width="250"
/>
</>
);
}
MyVideoPlayer.js
import { forwardRef } from 'react';
//The MyVideoPlayer component forwards that ref
//to the browser <video> node.
const VideoPlayer = forwardRef(function VideoPlayer({ src, type, width }, ref) {
return (
<video width={width} ref={ref}>
<source
src={src}
type={type}
/>
</video>
);
});
export default VideoPlayer;
forwardRef
returns a React component that you can render in JSX. Unlike React components defined as plain functions, a component returned byforwardRef
is also able to receive aref
prop.
useEffect Hook
useEffect
is a hook that lets you have side effects happen in your component. useEffect lets you connect a component to an external system. This includes dealing with network, browser DOM, animations, widgets written using a different UI library, and other non-React code.
If there is no external system involved, like if you want to update a component’s state when some props or state change, you shouldn’t need an Effect. Removing unnecessary Effects will make your code easier to follow, faster to run, and less error-prone.
This is how we define the useEffect :
useEffect(setup, dependencies?)
Notes:
- useEffect has 2 arguments, setup and dependencies.
- The setup is a function with the Effect’s logic. The dependencies are the list of all reactive values, like props, state, and all the variables and functions declared directly inside the component body.
useEffect(() => {
//Effect's logic
}, [dependency1, dependency2]);
React will compare each dependency with its previous value using the
Object.is
comparison. If the value is still the same, then useEffect doesn’t need to re-run.If we have linter that configured for React, it will verify that every reactive value is correctly specified as a dependency.
- If we want to let the effects re-run after every re-render of the component we don’t need to list any reactive values as dependencies for useEffect.
useEffect(() => {
//Effect's logic
});
- And if we want the effect run only on the first render, then we can use an empty array.
useEffect(() => {
//Effect's logic
}, []);
- The setup function may also optionally return a cleanup function. Some effects require cleanup to reduce memory leaks. Timeouts, subscriptions, event listeners, and other effects that are no longer needed should be cleaned up. We define the cleanup function by including a return function at the end of the useEffect.
When a component’s dependencies change, React triggers a re-render. During this phase, if we have provided a cleanup function, React runs it with the old values (before the update) and then runs the setup function with the new values.
However, if we don’t have any dependencies then the cleanup function is ready to run but doesn’t execute immediately. When we navigate away from the page, the cleanup function will be triggered.
const [count, setCount] = useState(0);
useEffect(() => {
const intervalId = setInterval(() => {
setCount((prevCount) => prevCount + 1);
}, 1000);
// Cleanup function: Runs before the next effect
return () => {
clearInterval(intervalId); // Clear the interval
};
}, []);
- If some of your dependencies are objects or functions defined inside the component, there is a risk that they will cause the Effect to re-run more often than needed. To fix this, remove unnecessary object and function dependencies.
Objects and functions are reference types. Even if their content doesn’t change, their reference (memory address) might. React compares dependencies by reference, not by content. If an object or function reference changes frequently (even if its content remains the same), the effect will re-run unnecessarily.
- Avoid using a function created during rendering as a dependency. It’s better to declare functions inside the effect itself rather than including them in the dependency array.
// This function is created from scratch on every re-render
function createOptions() {
return {
serverUrl: serverUrl,
roomId: roomId
};
}
useEffect(() => {
const options = createOptions(); // It's used inside the Effect
const connection = createConnection();
connection.connect();
return () => connection.disconnect();
}, [createOptions]);
//This effect re-connects after every render because the createOptions
//function is different for every render,
//as a result, these dependencies are always different on a re-render
...
//This is how we declare functions inside the effect itself
useEffect(() => {
function createOptions() {
return {
serverUrl: serverUrl,
roomId: roomId
};
}
const options = createOptions();
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
});
useEffect
returnsundefined
, and its effects only run on the client. They don’t run during server rendering.
During SSR(Server-Side Rendering), React renders components on the server before sending them to the client (the user’s browser). The server generates the initial HTML content based on the React components. This initial HTML includes the rendered output of the components, including any data or dynamic content. After the initial HTML is sent to the client, React takes over on the client side. React “hydrates” the components by attaching event handlers, setting up state, and making the page interactive. It runs the rendering code again to ensure that the client-side components match the server-rendered output. For hydration to work correctly, the initial render output (the HTML) must be identical on both the server and the client.
- If the effect is doing something visual (for example, positioning a tooltip), and the delay is noticeable (for example, it flickers), replace useEffect with useLayoutEffect.
There’s so much more to discover and learn about React Hooks, but I hope this guide can help you gain basic knowledge as a beginner.
Here are some tutorials I’ve written that can help you implement the hooks in this guide:
- Build a Tic-Tac-Toe Game with React Hooks (React Beginner Projects Part 1). In this tutorial, we will learn how to use
useState
,useEffect
, anduseRef
. - Build a To-Do List App with React Hooks, Tailwind, and localStorage (React Beginner Projects Part 2). In this tutorial, we will use
useState
,useEffect
,useContext
, anduseReducer
, but we will focus more on how to useuseContext
anduseReducer
hooks.
Have a good day!