React remains the pacesetter among JavaScript UI frameworks. There are plenty of ongoing developments in React, but the most important shift of the last few years was the move to functional components. Functional components rely on hooks for many of their capabilities. The most common hook is useState
, but there are many others.
Here’s a look at eight useful React hooks you may not know about, and how to use them.
useReducer
Everyone knows about useState
because it replaces an essential feature of class-based components—the member variables to hold state—with a functional equivalent. The useReducer
hook does something similar, but for more complex scenarios where state transitions are more involved and the application benefits from making transitions explicit. The useReducer
hook is inspired by the reducers found in Redux. It can be seen as a middle ground between the simplicity of useState
and the complexity of a state management system like Redux.
Here’s an example of how to work with the useReducer
hook. You can also see the reducer live in this JSFiddle.
Listing 1. Simple useReducer example
const {useReducer, Fragment} = React;
const initialState = {
text: "",
isUpperCase: false,
};
const reducer = (state, action) => {
switch (action.type) {
case 'SET_TEXT':
return {
...state,
text: action.text,
};
case 'TOGGLE_UPPER_CASE':
return {
...state,
text: state.text.toUpperCase(),
};
case 'LOWERCASE':
return {
...state,
text: state.text.toLowerCase(),
};
default:
return state;
}
};
const App = () => {
const [state, dispatch] = useReducer(reducer, initialState);
const handleChange = (event) => {
dispatch({ type: 'SET_TEXT', text: event.target.value });
};
const handleToggleUpperCase = () => {
dispatch({ type: 'TOGGLE_UPPER_CASE' });
};
const handleLowerCase = () => {
dispatch({ type: 'LOWERCASE' });
};
return (
<div>
<input type="text" value={state.text} onChange={handleChange} />
<button onClick={handleToggleUpperCase}>Toggle Upper Case</button>
<button onClick={handleLowerCase}>Toggle Lower Case</button>
<p>{state.text}</p>
</div>
);
};
The purpose of this example is to take the text from the input box and let the user click buttons to display the text in all uppercase or all lowercase. The code declares a new reducer with const [state, dispatch] = useReducer(reducer, initialState);
. The useReducer
takes the reducer function and the initialstate
and returns an array, which we then destructure to state and dispatch variables.
The reducer itself is defined with: const reducer = (state, action) =>
, giving a two-argument function. Whenever the dispatch function is called in the code, it will pass the current state along with an action object. In this case, the action object has a type field and we use that to determine how to mutate the state.
In a moderately complex application, useReducer
can be helpful in managing complexity, and can even be shared across the application using the context. When useState
is difficult to manage because of the complexity of the application, the useReducer
hook can help.
useCallback
The useCallback
hook is a performance hook. It takes a function and ensures that only a single version will be returned and reused for all callers. If the function is expensive and called repeatedly by a loop or child components, the useCallback
hook can net significant performance gains. This kind of optimization is known as memoizing a function.
In Listing 2, we have an example of using useCallback
to use the same function across many items in a list. Here's the example in a live JSFiddle.
Listing 2. useCallback in a iterated list
const List = ({ items }) => {
const [counter, setCounter] = React.useState(0);
const incrementCounter = React.useCallback(() => {
setCounter(counter + 1);
}, [counter]);
return (
<ul>
{items.map((item) => (
<li key={item.id}>
{item} - {counter}
<button onClick={incrementCounter}>Increment</button>
</li>
))}
</ul>
);
};
We use React.useCallback()
to create a new memoized function at incrementCounter
. We can use the memoized function as a normal function in the onClick
handler, in the list. useCallback()
takes a function as a first argument. Within that function, we can perform any work we need. The key difference is that React simply returns the cached value of the function unless something has changed in the list of dependency variables, which in our example is the counter variable.
This is a precious magic power in cases where you need to share an expensive function among several callers, especially child components. Bear in mind as we look at the next hook (useMemo
) that useCallback
stashes the function itself. That is to say, useCallback
prevents the actual function from being recreated each time it appears, and only recreates it when necessary.
useMemo
The useMemo
hook is useCallback
’s sibling. Where useCallback
caches the function, useMemo
caches the function return value. It's a subtle distinction, but important.
When should you use useMemo
versus useCallback
? The answer is: use useMemo
when you can, and useCallback
when you have to. The useCallback
hook is merited when the performance hit you are avoiding is the creation of the function itself in the rendering, while useMemo
will not prevent the function from being recreated wherever it appears. However, useMemo
will ensure the function returns a cached value if the dependencies have not changed.
Listing 3 shows useMemo
in an example. ,You can also see this example in a live JSFiddle.
Listing 3. useMemo
const { useMemo, useState } = React;
const ExpensiveComputationComponent = () => {
const [count, setCount] = useState(0);
const computeExpensiveValue = (count) => {
let result = 0;
for (let i = 0; i < count * 10000000; i++) {
result += i;
}
return result;
};
const memoizedValue = useMemo(() => computeExpensiveValue(count), [count]);
return (
<div>
<p>Count: {count}</p>
<p>Expensive Value: {memoizedValue}</p>
<button onClick={() => setCount(count + 1)}>Increment Count</button>
</div>
);
};
In this example, we have a function that costs a lot to compute: computeExpensiveValue
. It depends on a single input, count
. We can use computeExpensiveValue(count), [count])
to tell react: only run this function if the count has changed; otherwise, return the cached computation.
Again, the difference with useCallback
is not obvious. The important distinction is: useCallback
is the hook to use when the function itself is being repeatedly instantiated, incurring a performance hit. Otherwise, useMemo
is the better choice.
useContext
In React, the context is a variable scope that exists outside of the components and to which all components have access. As such, it is a quick and easy global space for application-wide data. For complex scenarios, it might be better to use an official data store like Redux, but for many uses, context
will suffice. The useContext
hook is how functional components interact with the context.
In Listing 4, we have two components, Speak
and Happy
, that are used by the application parent component, App
. The user can toggle between a dog and a cat state, and via the global context, the child components will reflect the choice (wagging tail versus purring, for example). You can also check out the live JSFiddle for this example.
Listing 4. useContext in action
const { createContext, useContext, useState } = React;
const AnimalContext = createContext();
const Speak = () => {
const { animalType } = useContext(AnimalContext);
return (
<div>
{animalType === 'dog' ? 'woof' : 'meow'}
</div>
);
};
const Happy = () => {
const { animalType } = useContext(AnimalContext);
return (
<div>
{animalType === 'dog' ? 'wag tail' : 'purr'}
</div>
);
};
const App = () => {
const [animalType, setAnimalType] = useState('dog');
const toggleAnimalType = () => {
setAnimalType(prevAnimalType => (prevAnimalType === 'dog' ? 'cat' : 'dog'));
};
return (
<div>
<button onClick={toggleAnimalType}>Toggle animal type</button>
<div>{animalType}</div>
<AnimalContext.Provider value={{ animalType }}>
<Speak />
<Happy />
</AnimalContext.Provider>
</div>
);
};
useRef
The useRef
hook lets you manage a reference outside the render cycle. useState
causes the React engine to render when it changes, whereas useRef
does not. The useRef
hook is like a special area off to the side of React that says: This variable is special and it isn’t part of the reactive UI. The most common use case for useRef
is to gain access directly to the DOM and its API. Normally, in reactive thinking, this is avoided and everything should be done through the reactive engine, but sometimes it’s unavoidable.
In Listing 5, when the button is clicked on, we use the useRef
hook to hold a reference to the input field and use the DOM methods to put the focus on it and set its value to “Something amazing." Here's the JSFiddle for the useRef example.
Listing 5. Simple useRef example
const App = () => {
const inputRef = React.useRef(null);
const handleClick = () => {
inputRef.current.focus();
inputRef.current.value="Something amazing";
};
return (
<div>
<input ref={inputRef} type="text" />
<button onClick={handleClick}>Focus Input</button>
</div>
);
};
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<App/>
);
useEffect
useEffect
is the second most common hook after useState
. It is frequently used to make API calls, change the DOM, or take other action (that is, cause an effect) when something changes in the component state. In a sense, useEffect
lets you define a reactive variable or variables and the behavior that will occur for them.
In Listing 6, we define a drop-down list to select a Star Wars character. When this value changes, we make an API call to the Star Wars API (SWAPI) and display the character’s data. Here's the live JSFiddle for this example.
Listing 6. useEffect with API call
const { useState, useEffect } = React;
const StarWarsCharacter = () => {
const [character, setCharacter] = useState('Luke Skywalker');
const [data, setData] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch(`https://swapi.dev/api/people/?search=${character}`);
const jsonData = await response.json();
setData(jsonData.results[0]);
} catch (error) {
console.error('Error fetching data:', error);
}
};
fetchData();
}, [character]);
const handleCharacterChange = (event) => {
setCharacter(event.target.value);
};
return (
<div>
<h1>Star Wars Character Details</h1>
<select value={character} onChange={handleCharacterChange}>
<option value="Luke Skywalker">Luke Skywalker</option>
<option value="Han Solo">Han Solo</option>
<option value="Princess Leia">Princess Leia</option>
</select>
{data ? (
<div>
<h2>{data.name}</h2>
<p>Height: {data.height}</p>
<p>Mass: {data.mass}</p>
<p>Hair Color: {data.hair_color}</p>
</div>
) : (
<p>Loading...</p>
)}
</div>
);
};
useLayoutEffect
useLayoutEffect
is a lesser known hook that comes into play when you need to make measurements of the rendered DOM. The useLayoutEffect
hook is called after React draws the UI, so you can count on it giving access to the actual layout.
In Listing 7, we use useRef
to grab an element off the DOM and useLayoutEffect
to be notified when the rendering is complete. Then, we calculate the element size using the DOM API. Also see the JSFiddle for this example.
Listing 7. useLayoutEffect to measure an element
const { useState, useRef, useLayoutEffect } = React;
const App = () => {
const [width, setWidth] = useState(null);
const elementRef = useRef(null);
useLayoutEffect(() => {
if (elementRef.current) {
const { width } = elementRef.current.getBoundingClientRect();
setWidth(width);
}
}, []);
return (
<div>
<div ref={elementRef}>Measure Width</div>
<p>Width: {width}</p>
</div>
);
};
useImperativeHandle
Sometimes, you need to get the reference directly to a component. You can do this with useRef
, and if you also want to provide access to the component's DOM, you can use forwardRef
. Sometimes, though, you need to customize the behavior that the component exposes via the reference. For that, you need the useImperativeHandle
hook.