Top 7 React Hooks Explained
September 13, 2024
In this post we will be covering:
useState
useEffect
useContext
useReducer
useRef
useCallback
useMemo
what each of them do and when to use them.
useState
import { useState } from 'react';
const [value, setValue] = useState(initialValue);
The most common React hook, useState
lets you create "state" variables for a component. "State" can be thought of as a component's memory. In web development, your components may often need to "remember" specific variables between renders. The type of variables that a component needs to remember is referred to as that components state.
You might wonder "why can't I just use a regular variable?", the quickest way to know if you need the useState
hook is to imagine the variable getting reset every time the component updates. If you don't want the variable to get reset when the component updates then you need useState
. If it doesn't matter that the variable gets reset whenever the component is updated, then you can simply create a regular variable.
import { useState } from 'react';
const MyComponent = () => {
const [clicks, setClicks] = useState(0); // state variable
const name = "Alex"; // regular variable
const handleClick = () => {
setClicks(clicks + 1);
}
return (
<>
<p>Hello {name}</p>
<button onClick={handleClick}>{clicks} clicks</button>
</>
);
}
When the user clicks the button, the button will display the number of times it has been clicked during the component's lifecycle. Every time the button changes it's value, the component gets updated. Whenever a component is updated, it gets completely reset. So, because we don't want the clicks
variable to reset back to 0
every time we click the button, we use the useState
hook to keep track of it's value.
We don't care if the name
variable get's reset since we don't want to change it's value anyways, it will always stay the same.
useEffect
import { useEffect } from 'react';
useEffect(setup, dependencies?);
To understand how the useEffect
hook works, we first need to understand a component's lifecycle. The lifecycle is composed of 3 separate events:
Mount
Update
Unmount
A component mounts when it is initially rendered, updates when re-rendered, and finally unmounts when it stops being rendered to the DOM. The reason why this is relevant here is because useEffect
allows us to run code specifically during any of these events.
import { useEffect } from 'react';
const MyComponent = () => {
useEffect(() => {
console.log("Component updated");
});
}
Here we have our component again, but this time with the useEffect
hook inside. The question here is, when will the code inside the useEffect
run? In this specific case, by not passing any dependencies, the console will log "Component updated" once when the component is mounted and every time when the component is updated.
Now this isn't very useful unless you want it behave like that, but we can get more specific with it.
import { useEffect } from 'react';
const MyComponent = () => {
useEffect(() => {
console.log("Component mounted");
}, []);
}
Now we are using the second argument in our useEffect
. This argument takes an array of dependencies. Meaning, the code inside will only run when any of the dependencies changes. Because we are specifying that the useEffect
doesn't have any dependencies with an empty array, the callback function won't fire when the component is updated. The effect will still run when the component initially mounts but no longer when it's updated. This is incredibly useful when we only want to call a function once in our component when it is rendered, like fetching some 3rd party data for instance.
import { useEffect } from 'react';
const MyComponent = () => {
useEffect(() => {
console.log("Component mounted");
return console.log("Component unmounted");
}, []);
}
Here we have our component again but this time the useEffect
callback function has a return statement inside of it. When you have a return statement inside of the callback function, it get's called when the component is going to unmount. This is incredibly useful when we have some effects running in the background and we need to clean them up when the component is unmounted. An example would be a timer that's using setInterval() and we need to call clearInterval() to stop the clock when we don't need it anymore.
import { useState, useEffect } from 'react';
const MyComponent = () => {
const [clicks, setClicks] = useState(0); // state variable
const name = "Alex"; // regular variable
const handleClick = () => {
setClicks(clicks + 1);
}
useEffect(() => {
console.log("Clicks updated");
}, [clicks]);
return (
<>
<p>Hello {name}</p>
<button onClick={handleClick}>{clicks} clicks</button>
</>
);
}
Here is where useEffect
really comes in handy, there will be times when you need to run some arbitrary code when some variable inside of a component get updated. Bringing back our useState
example from before, we can pass our clicks
state to the dependencies array. Every time clicks
is changed the callback function is called.
It is very common to see useEffect
and useState
used together like this.
useContext
import { createContext, useContext } from 'react';
const MyContext = createContext();
const ParentComponent = () => {
const sharedValue = "Hello World!";
return (
<div className="parent">
<MyContext.Provider value={sharedValue}>
<ChildComponent />
</MyContext.provider>
</div>
);
}
const ChildComponent = () => {
const value = useContext(MyContext);
return <p>{value}</p> // "Hello World!"
}
To understand the above code, let's talk about what "prop drilling" is and why we would want to use the useContext
hook.
First let's look at a common case where we need to pass some prop from a parent component to a child component.
const ParentComponent = () => {
const someProp = "Some value";
return (
<div>
<ChildComponent value={someProp} />
</div>
);
}
const ChildComponent = ({ value }) => {
return <div>{value}</div> // "Some value"
}
Now this is perfectly fine and you will find yourself doing this exact thing very often in React, but a problem arises when we go further with this. Let's say we have a parent component that needs to give a property to a child component that is a child component to another child component and so on. One way we could do this is by passing the value as a property for each child component which can become messy and hard to manage as your application grows, which is called "prop drilling".
This is the exact problem that useContext
tries to solve. We create a context in our parent component and then any of it's children can use that context all while avoiding prop drilling. To start we need to create context on our parent.
import { createContext } from 'react';
const MyContext = createContext();
Then we use <Context.Provider>
to create a provider wrapper inside of our parent component.
import { createContext } from 'react';
const MyContext = createContext();
const ParentComponent = () => {
return (
<div className="parent">
<MyContext.Provider>
</MyContext.provider>
</div>
);
}
Now that we have our provider set up, we can create some kind of variable and pass it as a prop to our provider.
import { createContext } from 'react';
const MyContext = createContext();
const ParentComponent = () => {
const sharedValue = "Hello World!";
return (
<div className="parent">
<MyContext.Provider value={sharedValue}>
<ChildComponent />
</MyContext.provider>
</div>
);
}
Now let's set up our child component. We know our child component is a child of our parent component but we don't know how many "levels down" it may be. So let's take advantage of the useContext hook to get the value from the parent. Make sure we import MyContext
to the child component if we need to.
import { useContext } from 'react';
import { MyContext } from '@/components/ParentComponent';
const ChildComponent = () => {
const value = useContext(MyContext);
return <p>{value}</p> // "Hello World!"
}
useContext
is a great way to create global variables, objects, or even state between components. We can organize our code a bit by creating a separate component for the provider. Let's take a look at a real world example to really get the hang of this component.
import { createContext, useContext, useState } from 'react';
// create a context
const ThemeContext = createContext();
// create provider component
const ThemeProvider = ({ children }) => {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(theme === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
};
// a child component
const ThemedComponent = () => {
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<div style={{ background: theme === 'light' ? '#fff' : '#333', color: theme === 'light' ? '#000' : '#fff' }}>
<p>The current theme is {theme}</p>
<button onClick={toggleTheme}>Toggle Theme</button>
</div>
);
};
// main app component
const App = () => {
return (
<ThemeProvider>
<ThemedComponent />
</ThemeProvider>
);
};
And now we can avoid prop drilling by using the useContext
hook.
useReducer
import { useReducer } from 'react';
const [state, dispatch] = useReducer(reducer, initialState);
A lot like the useState
hook, useReducer
is used to store and update states. In fact, you can completely replace useState
with useReducer
. The most common reason why you would use useReducer
over useState
is if you require some additional control over a complex state with multiple properties. It easily allows you to create states that look like this:
const complexState = {
count: 0,
name: 'Alex'
}
Let's look at a simpler example just to get the idea. We'll start with a state with only one property.
const initialState = {
count: 0
}
Here we have a state with a property of count, which covers the second argument in the useReducer
hook. Now that we know what kind of data we are working with, let's create a reducer
function for the first argument.
const reducer = (state, action) => {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 };
case 'DECREMENT':
return { count: state.count - 1 };
case 'RESET':
return { count: 0 };
default:
return state;
}
}
Our reducer function takes in two parameters, state
and action
. state
refers to the current state of our component. action
will represent what action we would like to take to change the state in some way. We can use a switch statement to create as many different actions as we want.
Now that we have everything that we need, let's create our component and initialize our state with the userReducer
hook.
import { useReducer } from 'react';
const initialState = { count: 0 }
const reducer = (state, action) => {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 };
case 'DECREMENT':
return { count: state.count - 1 };
case 'RESET':
return { count: 0 };
default:
return state;
}
}
const Counter = () => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>Increment</button>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>Decrement</button>
<button onClick={() => dispatch({ type: 'RESET' })}>Reset</button>
</div>
);
}
The dispatch variable that we defined with useReducer
here is a function that allows us to pass in an action to our reducer
function. In our reducer
function we implicitly defined action
as an object with a property of type
, so we will pass an object with a property of type
through our dispatch function.
This is a very simple example of the useReducer
hook. You may be able to imagine how customizable this hook can get and it's why it's so useful. useReducer
by no means replaces useState
but it's important to wrap your head around exactly how to utilize this hook.
useRef
import { useRef } from 'react';
const ref = useRef(initialValue);
By now you may have noticed that when a component's state is updated, like with useState
, the component automatically re-renders. There are cases where you need to keep track of some variable and change it throughout the component's life cycle but don't want or need the component to re-render every time the variable changes. useRef
solves this issue.
A common use case with useRef
is to access DOM elements either by using getElementById or querySelector without the need for additional state management within the component.
import { useRef } from 'react';
const MyComponent = () => {
const element = useRef(null); // create ref
return <div ref={element} /> // assigning a div to the ref
}
The above code is an example of assigning a DOM element to a variable using the useRef
hook. Because we have a reference to the element we can then programmatically alter the element.
import { useRef } from 'react';
const MyComponent = () => {
const element = useRef(null);
const handleMouseEnter = () => {
element.current.style.backgroundColor = 'salmon';
element.current.style.transform = 'scale(1.1)';
};
const handleMouseLeave = () => {
element.current.style.backgroundColor = 'lightblue';
element.current.style.transform = 'scale(1)';
};
return (
<div
ref={element}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
style={{
height: '100px',
width: '200px',
backgroundColor: 'lightblue',
transition: 'all 0.3s ease',
}}
>
Hover over me!
</div>
);
};
The useRef
hook in React is a simple yet powerful tool essential for every React developer to master.
useCallback
import { useCallback } from 'react';
const cachedFn = useCallback(fn, dependencies);
When a component is re-rendered, any function defined within the component is recreated. This can lead to performance issues especially when passing said functions to child components. In order to ensure that a function stays the same between renders we can use the useCallback
hook.
import { useState } from 'react';
const List = ({ items, onItemClick }) => {
console.log("List component re-rendered");
return (
<ul>
{items.map((item, index) => (
<li key={index} onClick={() => onItemClick(item)}>
{item}
</li>
))}
</ul>
);
};
const ParentComponent = () => {
const [count, setCount] = useState(0);
const items = ["Apple", "Banana", "Orange"];
const handleClick = (item) => {
console.log(`Clicked on: ${item}`);
};
return (
<div>
<button onClick={() => setCount(count + 1)}>Click me</button>
<p>Count: {count}</p>
<List items={items} onItemClick={handleClick} />
</div>
);
};
In the code above, every time the 'Click me' button is clicked, the ParentComponent
is re-rendered. This re-creates the handleClick
function even though it does the exact same thing every time. We can improve this by using useCallback
to memoize the handleClick
function so it doesn't get re-created every time.
import { useState, useCallback } from 'react';
const List = ({ items, onItemClick }) => {
console.log("List component re-rendered");
return (
<ul>
{items.map((item, index) => (
<li key={index} onClick={() => onItemClick(item)}>
{item}
</li>
))}
</ul>
);
};
const ParentComponent = () => {
const [count, setCount] = useState(0);
const items = ["Apple", "Banana", "Orange"];
const handleClick = useCallback((item) => {
console.log(`Clicked on: ${item}`);
}, []); // no dependencies, so the function is memoized and won't be recreated
return (
<div>
<button onClick={() => setCount(count + 1)}>Click Me</button>
<p>Count: {count}</p>
<List items={items} onItemClick={handleClick} />
</div>
);
};
If the handleClick
function was passed to deeply nested child components or if it's used in large lists, re-created the function could result in unnecessary re-renders of the child components.
Some hooks like useEffect
rely on stable function references, so if the function changes even if it logically does the same thing, useEffect
will re-run. Using useCallback
ensures the function reference remains the same preventing unnecessary side effects.
import { useState, useCallback } from 'react';
const ExpensiveComponent = ({ computeValue }) => {
console.log("ExpensiveComponent rendered");
return <div>Computed Value: {computeValue()}</div>;
};
const ParentComponent = () => {
const [count, setCount] = useState(0);
const computeValue = useCallback(() => {
// simulate a heavy computation
console.log("Computing...");
return count * 2;
}, [count]); // recalculate only when `count` changes
return (
<div>
<button onClick={() => setCount(count + 1)}>Increment</button>
<p>Count: {count}</p>
<ExpensiveComponent computeValue={computeValue} />
</div>
);
};
As a further example, if we have a component that does some heavy computation, we can use useCallback
to memoize the result, only re-running the computeValue
function when the count state changes.
As a rule of thumb, you should use useCallback
when you are passing functions as props to child components, when the function is a dependency in other hook, or when the function is performing some heavy calculations. Using the hook on every function introduces unnecessary overhead and complexity to your code. It's a tool meant for specific cases where performance can be vastly optimized by caching a components function.
useMemo
import { useMemo } from 'react';
const cachedValue = useMemo(calculateValue, dependencies);
To explain the useMemo
hook let's take a look at a simple example with a count button.
import { useState, useEffect } from 'react';
const Parent = () => {
const [count, setCount] = useState(0);
useEffect(() => {
console.log('Parent rendered');
});
return (
<>
<Child />
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</>
);
}
const Child = () => {
useEffect(() => {
console.log('Child rendered');
});
return (
<>
<p>Child</p>
</>
);
}
In this example we have a Parent
and Child
component. When everything is initially rendered we expect to see "Parent rendered" and "Child rendered" in the console. This is exactly what we see. However, when we click the button something interesting happens, we see both "Parent rendered" and "Child rendered" in the console again even though the parent's state is the only one that is changing. In this example, it is completely unnecessary for React to re-render the Child
component since it is rendering the exact same thing every time.
To make our components more performant we can use the useMemo
hook here on the Child
component.
import { useState, useEffect, useMemo } from 'react';
const Parent = () => {
const [count, setCount] = useState(0);
useEffect(() => {
console.log('Parent rendered');
});
const memoized = useMemo(() => {
return <Child />
}, []);
return (
<>
{memoized}
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</>
);
}
const Child = () => {
useEffect(() => {
console.log('Child rendered');
});
return (
<>
<p>Child</p>
</>
);
}
In this example, we only see "Parent rendered" when clicking the button because the Child
component has been memoized. The general rule of thumb for both useCallback
and useMemo
is if React is executing unnecessary and expensive code, we should consider if useCallback
or useMemo
would help our components performance.