Crucial React Hooks That Are Less Understood
Introduction
As React has become the top choice for building web applications, it's important to master all the concepts and topics that makes learning react incomplete without. If you have ever studied react for at least once, I am sure you know that react have a list of built-in hooks which are crucial for working with react and I am sure you know useState
and useEffect
among them pretty well. That's why I am assuming with confidence that I don't need to touch them. Instead, I'll help you master the hooks that are often less understood:
useLayoutEffect
useMemo
useCallback
useReducer
useRef
useImperativeHandle
useDeferredValue
useTransition
These are very important if you want to be comfortable to work on any feature with react which you should as a react developer.
useLayoutEffect
Before getting into useLayoutEffect
we need to talk about useEffect
. How does useEffect
hook work? Imagine where the component is rendered is a canvas for you to paint on. So, you visualize the picture on your head first and then paint it on the canvas. Now you want to add some extra details after painting is done. In this task, adding those extra details right after you complete painting is useEffect
hook. On the other hand, adding those extra details before actual painting is useLayoutEffect
hook.
Visualization on head > rendering the component DOM.
Adding Extra details > Side effect or the code written inside useEffect or useLayoutEffect hook.
Painting picture > painting the web page UI on the browser window.
Not clear yet? See if you understand the following explanation:
useEffect
runs after the screen updates, which is good for non-urgent tasks and won't delay what the user sees.useLayoutEffect
runs after the DOM is rendered and before the screen updates, which is good for urgent tasks that need to happen right away but can delay what the user sees if it takes too long.
For useLayoutEffect
hook it doesn't have to work on the first render. It works the same for all the re-renders as well.
import React, { useLayoutEffect, useRef } from 'react';
function MyComponent() {
const ref = useRef(null);
useLayoutEffect(() => {
// This code will run after the render but before the browser paints
if (ref.current) {
console.log(ref.current.getBoundingClientRect());
}
}, []);
return <div ref={ref}>Hello, World!</div>;
}
How useLayoutEffect
can hurt performance
useLayoutEffect
can hurt performance because it runs synchronously after all DOM mutations but before the browser has a chance to paint. This synchronous behavior means that useLayoutEffect
can block the browser's painting process, causing the following potential performance issues:
Blocking the Browser's Render: Since the browser needs to wait for the synchronous effect to complete before painting
Heavy Computations: If you perform heavy computations or complex operations inside
useLayoutEffect
, these operations will block the main thread. As a result, the browser cannot perform other critical tasks like handling user interactions or rendering other parts of the UI, leading to a sluggish user experience.Frequent Updates: If
useLayoutEffect
is used in a component that updates frequently, each update will involve running the synchronous effect, which can repeatedly block rendering
useMemo
useMemo
helps you optimize your components by memoizing (caching) the results of expensive calculations. This means React will remember the result of a calculation and reuse it until the inputs change, rather than recalculating it every time the component renders.
When to UseuseMemo:
Expensive Calculations: Use
useMemo
for calculations that take a lot of time or resources, to avoid recalculating them on every render.Referential Equality: Helps to prevent unnecessary re-renders of child components by ensuring that the reference to the value remains the same unless the dependencies change.
How it works: When you wrap the expensive calculation inside useMemo
and give a list of dependencies, React will only recalculate the result when one of the dependencies changes.
Simple Example: Imagine you have a list of numbers and you want to calculate the sum. This sum calculation is expensive, and you don’t want to do it on every render unless the list of numbers changes.
import React, { useMemo, useState } from 'react';
function SumCalculator({ numbers }) {
const sum = useMemo(() => {
console.log('Calculating sum...');
return numbers.reduce((acc, num) => acc + num, 0);
}, [numbers]);
return <div>The sum is: {sum}</div>;
}
export default function App() {
const [numbers, setNumbers] = useState([1, 2, 3, 4]);
return (
<div>
<SumCalculator numbers={numbers} />
<button onClick={() => setNumbers([...numbers, numbers.length + 1])}>
Add Number
</button>
</div>
);
}
useCallback
Unlike useMemo()
, useCallback()
hook memoizes(caches) functions. This means React will remember the function and reuse it until its dependencies change, rather than recreating it on every render. As we know, functions in a component get recreated when a re-render happens.
useCallback()
use cases:
When you pass functions as props to child components, using
useCallback
ensures that the function reference remains the same unless dependencies change. This prevents unnecessary re-renders of the child components.using
useCallback()
on event handlers(like onClick) can help avoid recreating these handlers on every render.useCallback()
Helps in preventing the creation of new function instances on every render, which can improve performance in components that re-render frequently.
Simple Example: Imagine you have a counter component, and you pass an increment function to a button component:
import React, { useState, useCallback } from 'react';
function IncrementButton({ onIncrement }) {
console.log('Button rendered');
return <button onClick={onIncrement}>Increment</button>;
}
export default function Counter() {
const [count, setCount] = useState(0);
const increment = useCallback(() => {
setCount(count + 1);
}, [count]);
return (
<div>
<p>Count: {count}</p>
<IncrementButton onIncrement={increment} />
</div>
);
}
The increment
function is created and passed to the IncrementButton
component. If count
doesn’t change, the increment
function remains the same, so IncrementButton
doesn’t re-render unnecessarily. If count
changes, useCallback
recreates the increment
function, and IncrementButton
re-renders with the new function.
useReducer
useReducer
is a tool in React that helps you manage complex state logic in your components. Think of it as a way to handle state that involves multiple steps or rules. It’s like a more powerful version of useState
that can handle more complicated situations. Syntax:
const [state, dispatch] = useReducer(reducer, initialState);
The following example can explain the best:
import React, { useReducer } from 'react';
// Define the reducer function
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
throw new Error('Unknown action type');
}
}
export default function Counter() {
// Use useReducer with the reducer function and initial state
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
<button onClick={() => dispatch({ type: 'decrement' })}>Decrement</button>
</div>
);
}
Reducer Function: This function says what to do when actions like 'increment' or 'decrement' happen. For example, if the action is 'increment', it adds 1 to the count.
The initial state is { count: 0 }
. useReducer
returns the current state and a dispatch
function to send actions. The buttons use the dispatch
function to send actions (increment
and decrement
), which the reducer function handles to update the state.
When to use:
Complex State Logic: When you have more complex state transitions or when the next state depends on the previous state.
Multiple State Values: When your state consists of multiple sub-values or is structured as an object.
State Management: When you need a more predictable way to manage state transitions.
useRef
The useRef
hook provides a way to persist values across renders. But it doesn't cause a re-render when the value changes. It returns a mutable ref object whose .current
property is initialized to the passed argument (initialValue
). The ref object persists for the lifetime of the component.
Primary Uses ofuseRef:
Accessing DOM Elements:useRef
is often used to directly access and interact with DOM elements. This is useful when you need to manage focus, select text, or perform other direct DOM manipulations.
Persisting Values: It can hold any mutable value that you want to persist across renders and doesn't cause re-renders. For example, it can be used to keep track of intervals, timeouts, or other mutable values that change over time.
Basic Example: Accessing DOM Elements:
import React, { useRef, useEffect } from 'react';
function FocusInput() {
const inputRef = useRef(null);
useEffect(() => {
// Focus the input element when the component mounts
inputRef.current.focus();
}, []);
return <input ref={inputRef} type="text" />;
}
export default FocusInput;
useRef(null)
creates a ref object with the initial value null
. The ref
attribute on the <input>
element is assigned to inputRef
. The useEffect
hook focuses the input element when the component mounts by calling inputRef.current.focus()
.
useImperativeHandle
The useImperativeHandle
hook is used to control and manage imperative actions on child components from parent components. This is useful when you need to interact with the child component in an imperative manner (e.g., triggering a focus, playing a video, or manually managing animations).
Normally, ref
provides direct access to a DOM node or a class component instance. useImperativeHandle
allows you to define what is exposed when the parent component uses a ref to interact with the child component.
How It Works:useImperativeHandle
is used within a functional component. It takes three arguments:
ref
: The ref object passed from the parent component.createHandle
: A function that returns the object you want to expose to the parent component.[deps]
: An optional array of dependencies. The handle is re-created if any dependency changes.
Example Scenario: Consider a custom input component where you want to expose a focus
method to the parent component. This allows the parent to programmatically focus the input field.
// Child Component
import React, { useImperativeHandle, useRef, forwardRef } from 'react';
const CustomInput = forwardRef((props, ref) => {
const inputRef = useRef(null);
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
},
clear: () => {
inputRef.current.value = '';
}
}));
return <input ref={inputRef} {...props} />;
});
export default CustomInput;
// Parent Component
import React, { useRef } from 'react';
import CustomInput from './CustomInput';
function ParentComponent() {
const inputRef = useRef(null);
const handleFocus = () => {
inputRef.current.focus();
};
const handleClear = () => {
inputRef.current.clear();
};
return (
<div>
<CustomInput ref={inputRef} />
<button onClick={handleFocus}>Focus Input</button>
<button onClick={handleClear}>Clear Input</button>
</div>
);
}
export default ParentComponent;
CustomInput
is wrapped with forwardRef
to forward the ref from the parent to the child component. Inside CustomInput
, useImperativeHandle
is used to define a custom handle. This handle exposes two methods: focus
and clear
. Rest of the story is done by useRef()
hook.
useDeferredValue
useDeferredValue()
hook allows you to defer updates to a value. The value passed to useDeferredValue will be deferred, meaning React will prioritize more urgent updates over it. The deferred value is updated at a lower priority, helping to avoid blocking the main thread during critical interactions. By deferring less critical updates, useDeferredValue
helps keep the UI responsive, ensuring smoother user experiences during high-priority tasks like typing or clicking.
Example Scenario: Suppose you're building a search component that filters a large list of items based on user input. Each keystroke updates the search query and triggers a re-render of the list. Without deferring, each keystroke might cause the list to re-render immediately. As the component has a large list of items to be re-rendered, it could take a while and in the mean time the next render could start which could lead to lags.
By using useDeferredValue
you can defer the update of the filtered list until the continuous rendering gets to complete. Thus the input remains responsive.
import React, { useState, useDeferredValue, useMemo } from 'react';
function FilteredList({ items }) {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
const filteredItems = useMemo(() => {
// Simulate a heavy computation
return items.filter(item => item.toLowerCase().includes(deferredQuery.toLowerCase()));
}, [items, deferredQuery]);
return (
<div>
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
/>
<ul>
{filteredItems.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
</div>
);
}
export default FilteredList;
useState
is used to manage the query
state, which represents the user's input. useDeferredValue
is used to create a deferred version of query
. This means deferredQuery
will lag behind query
when React prioritizes other updates. useMemo
is used to memoize the filteredItems
computation, ensuring it only re-computes when items
or deferredQuery
changes. This simulates a heavy computation that should not block the main thread. The input field updates instantly as the user types, keeping the UI responsive. The filtering operation on items
uses the deferred query, allowing the UI to remain responsive during the filtering process.
useTransition
The useTransition() hook allows you to mark certain state updates as "transitions," allowing the UI to remain responsive by prioritizing urgent updates (like user input) over less critical ones (like data fetching or rendering large lists). Basically this hook allows smoother user interactions by avoiding blocking the main thread with heavy computations or renders.
How It Works: useTransition
returns an array with two elements: a startTransition
function and isPending
boolean. You can wrap your state updates inside the startTransition
function to mark them as transitions (less priority code). Which means the part wrapped with startTransition
starts to work after the continuous process completes.
Example: Here's a practical example illustrating the use of useTransition
:
import React, { useState, useTransition, useMemo } from 'react';
function FilteredList({ items }) {
const [query, setQuery] = useState('');
const [isPending, startTransition] = useTransition();
const filteredItems = useMemo(() => {
// Simulate a heavy computation
return items.filter(item => item.toLowerCase().includes(query.toLowerCase()));
}, [items, query]);
const handleChange = (e) => {
const value = e.target.value;
startTransition(() => {
setQuery(value);
});
};
return (
<div>
<input
type="text"
value={query}
onChange={handleChange}
placeholder="Search..."
/>
{isPending && <p>Loading...</p>}
<ul>
{filteredItems.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
</div>
);
}
export default FilteredList;
useState
is used to manage the query
state, representing the user's input. useTransition
returns startTransition
and isPending
. startTransition
marks the state update as a transition, and isPending
indicates if the transition is in progress. useMemo
is used to memoize the filteredItems
computation, ensuring it only re-computes when items
or query
changes. This helps simulate a heavy computation that should not block the main thread. The handleChange
function wraps the state update within startTransition
, ensuring the update is performed as a transition. This keeps the input field responsive. isPending
is used to show a loading indicator while the transition is ongoing.
Last Words
These hooks are some of the most important yet often less understood aspects of React. I hope you grasp their concepts as I do. I often found myself confused about how to use these hooks effectively. To clarify my understanding, I carefully read several articles and watched some excellent tutorials, notably from WebDevSimplified and Cosden Solutions. I then compiled everything into this comprehensive note, which serves as a reference for both you and me.
Subscribe to my newsletter
Read articles from Abeer Abdul Ahad directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Abeer Abdul Ahad
Abeer Abdul Ahad
I am a Full stack developer. Currently focusing on Next.js and Backend.