React Hooks Explained: The Ultimate Guide
15 min read
React Hooks provide a way to use state and other React features without writing a class. They were introduced in React 16.8 and have since become a fundamental aspect of modern React development.
Hooks are essentially functions that provide a powerful and flexible way to manage state, handle side effects, and optimize performance. They also help maintain a clean and declarative coding style. In this comprehensive guide, we will explore the most essential React Hooks, detailing when and how to use them, along with practical examples to help you integrate them into your projects.
Whether you are new to React or looking to deepen your understanding of Hooks, this guide will equip you with the knowledge needed to harness the full potential of React Hooks. Read on!
1. useState
In React, useState
is a special function that lets you add state to functional components. It provides a way to declare and manage state variables directly within a function component. It's important to note that each call to useState()
can only declare one state variable. Introduced in version 16.8, it simplifies state management in functional components.
Functional components are preferred due to their concise code. useState
simplifies state management. Also, with useState
, you can handle state without converting your component into a class.
Importing the useState
Hook
To import the useState
hook, write the following code at the top level of your component:
import { useState } from "react";
Structure of useState
Hook
This hook takes some initial state and returns two values: the current state and a function to update that state. The value passed to useState
will be treated as the default value.
const [var, setVar] = useState(initialValue);
How useState
Works
useState()
creates a new cell in the functional component’s memory object.New state values are stored in this cell during renders.
The stack pointer points to the latest cell after each render.
Deliberate user refresh triggers stack dump and fresh allocation.
The memory cell preserves state between renders, ensuring persistence.
Declaring a State in React with useState
To use the useState
hook, you must first import it from React, or you will have to append it like React.useState()
each time you create a state. The useState
hook takes the initial value of the state variable as an argument. This value can be of any data type, such as a string, number, object, array, and more.
Basic Example:
import React, { useState } from 'react';
const App = () => {
const number = useState(0);
const string = useState('');
const object = React.useState({});
const array = React.useState([]);
return (
// ...
);
};
Using the State Value in JSX:
const App = () => {
const [count, setCount] = useState(0);
return (
<div>
<p>{count}</p>
</div>
);
};
Updating State with useState
The second value returned by useState
is a function used to update the state. This can be called anything, but it's best practice to use the variable name with a prefix of set
.
Example:
const [state, setState] = useState(initialValue);
const [count, setCount] = useState(initialCount);
const [anything, setAnything] = useState(initialAnything);
Incrementing a Counter:
const App = () => {
const [count, setCount] = useState(0);
const incrementCount = () => {
setCount(count + 1);
};
return (
<div>
<p>{count}</p>
<button type="button" onClick={incrementCount}>
Increment Count
</button>
</div>
);
};
Inline Update Function:
<button type="button" onClick={() => setCount(count + 1)}>
Increment Count
</button>
Using Multiple State Variables
Creating individual states for data that can be combined into one state isn't advisable when working with real-life data. For example, if you want to create a state for user data such as name, age, and hobby:
Separate States:
const App = () => {
const [name, setName] = useState("John Doe");
const [age, setAge] = useState(20);
const [hobby, setHobby] = useState("reading");
return (
// ...
);
};
Combined State Object:
const App = () => {
const [userDetails, setUserDetails] = useState({
name: 'John Doe',
age: 20,
hobby: 'Reading',
});
return (
<div>
<h1>{userDetails.name}</h1>
<p>
{userDetails.age} || {userDetails.hobby}
</p>
</div>
);
};
Destructuring State Object:
const App = () => {
const [{ name, age, hobby }, setUserDetails] = useState({
name: 'John Doe',
age: 20,
hobby: 'Reading',
});
return (
<div>
<h1>{name}</h1>
<p>
{age} || {hobby}
</p>
</div>
);
};
Using an Object as a State Variable
When working with real-life data, you often deal with objects. To update a particular field in an object, use the spread operator to ensure other fields remain unchanged.
Example with Nested Objects:
const App = () => {
const [userDetails, setUserDetails] = useState({
userName: {
firstName: 'John',
lastName: 'Doe',
},
age: 20,
hobby: 'Reading',
});
const changeName = () => {
setUserDetails({
...userDetails,
userName: {
...userDetails.userName,
firstName: 'Jane',
},
});
};
return (
<div>
<h1>
Hello {userDetails.userName.firstName} {userDetails.userName.lastName},
</h1>
<p>
{userDetails.age} || {userDetails.hobby}
</p>
<button onClick={changeName}>Change Name</button>
</div>
);
};
2. useEffect
The useEffect
hook allows you to perform side effects in your components. These side effects can include data fetching, directly updating the DOM, and setting up timers. The useEffect
hook accepts two arguments: a function containing the side effect logic and an optional dependency array that determines when the effect should be executed.
useEffect(() => {
// Side effect logic
}, [dependencies]);
Effect Function: This is the main function where you place your side effect logic.
Dependency Array: This optional array allows you to control when the effect should run. If it's empty, the effect runs only once after the initial render. If it includes dependencies, the effect runs whenever any of the dependencies change.
Basic Usage of useEffect
Here are a few usage examples of useEffect:
Fetching Data
Here's an example of using useEffect
to fetch data from an API when the component mounts:
javascriptCopy codeimport { useState, useEffect } from "react";
function DataFetcher() {
const [data, setData] = useState(null);
useEffect(() => {
fetch("https://api.example.com/data")
.then(response => response.json())
.then(data => setData(data))
.catch(error => console.error("Error fetching data:", error));
}, []); // Empty array ensures this runs only once
return (
<div>
{data ? <pre>{JSON.stringify(data, null, 2)}</pre> : <p>Loading...</p>}
</div>
);
}
export default DataFetcher;
In this example, the useEffect
hook fetches data from an API endpoint when the component first renders. The empty dependency array ensures that this effect runs only once.
Dependency Array
The dependency array in useEffect
controls when the effect should run. Here are three common scenarios:
No Dependency Passed:
useEffect(() => { console.log("Runs on every render"); });
Empty Array:
useEffect(() => { console.log("Runs only on the first render"); }, []);
Props or State Values:
useEffect(() => { console.log("Runs on the first render and whenever prop or state changes"); }, [prop, state]);
Timer with Dependencies
Here's an example of a timer that updates every second, demonstrating how to use dependencies:
import { useState, useEffect } from "react";
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds(prevSeconds => prevSeconds + 1);
}, 1000);
return () => clearInterval(interval); // Cleanup on unmount
}, []); // Empty array means this effect runs once
return <div>Seconds: {seconds}</div>;
}
export default Timer;
In this example, the timer increments the seconds
state every second. The cleanup function ensures that the interval is cleared when the component unmounts, preventing memory leaks.
Side Effects in React:
Side effects are not predictable because they interact with the outside world. Examples include:
Making a request to an API for data
Interacting with browser APIs (like
document
orwindow
)Using timing functions like
setTimeout
orsetInterval
Side effects are essential for most applications, as they allow components to fetch data, update the DOM, and perform other necessary operations. The useEffect
hook provides a way to handle these side effects cleanly and efficiently within functional components.
3. useContext
useContext
lets you subscribe to React context without introducing nesting. It allows components to access and subscribe to data from a central source, eliminating the need for prop drilling (passing data through multiple levels of nested components).
Creating the Context
First, you need to create a context using React.createContext()
. This function returns a context object that you can use throughout your component tree.
import React from 'react';
const UserContext = React.createContext();
Providing the Context
Next, you need to provide the context's value to the components that require access to it. You do this by wrapping your component tree with the context provider and passing the value you want to share.
import React, { useState } from 'react';
function App() {
const [userName, setUserName] = useState('Guest');
return (
<UserContext.Provider value={userName}>
<UserProfile />
</UserContext.Provider>
);
}
export default App;
In this example, the userName
state is provided to all components within the UserContext.Provider
.
Consuming the Context
Finally, you can consume the context in any component that needs access to the shared data using the useContext
hook.
import React, { useContext } from 'react';
import UserContext from './UserContext';
function UserProfile() {
const userName = useContext(UserContext);
return <div>Welcome, {userName}!</div>;
}
export default UserProfile;
Here, the UserProfile
component uses the useContext
hook to access the userName
provided by UserContext.Provider
.
When Do You Need Context?
Context is a powerful tool but should be used judiciously. Here are a few scenarios where context is particularly useful:
Sharing Global Data: When multiple components need access to the same data, such as user authentication, theme settings, or language preferences.
Avoiding Prop Drilling: To avoid passing props through multiple levels of nested components, which can make your code harder to maintain.
Managing State: For centralizing state management in your app, especially for complex state used by many components.
Use Case: Global User Name
To illustrate the benefits of using useContext
, let's explore a practical example where the user's name is displayed in various parts of the UI and can be updated without prop drilling.
Context to the Rescue
First, create a context for the user's name and make it available globally.
// UserContext.js
import React, { createContext, useContext, useState } from 'react';
const UserContext = createContext();
export function UserProvider({ children }) {
const [userName, setUserName] = useState('Guest');
return (
<UserContext.Provider value={{ userName, setUserName }}>
{children}
</UserContext.Provider>
);
}
export function useUser() {
return useContext(UserContext);
}
In this example, UserProvider
component wraps its children with UserContext.Provider
, providing both userName
and setUserName
.
When Context Changes
Any component within the app can access and update the user's name without prop drilling.
import React from 'react';
import { useUser } from './UserContext';
function UserProfile() {
const { userName, setUserName } = useUser();
const handleChangeName = () => {
const newName = prompt('Enter your name:');
if (newName) {
setUserName(newName);
}
};
return (
<div>
<p>Welcome, {userName}!</p>
<button onClick={handleChangeName}>Change Name</button>
</div>
);
}
export default UserProfile;
When setUserName
is called, it updates the userName
in the context, causing all components that consume the context to re-render with the updated name.
Full Example
Here's a complete example demonstrating how to use useContext
to share user information across components.
// UserContext.js
import React, { createContext, useContext, useState } from 'react';
const UserContext = createContext();
export function UserProvider({ children }) {
const [userName, setUserName] = useState('Guest');
return (
<UserContext.Provider value={{ userName, setUserName }}>
{children}
</UserContext.Provider>
);
}
export function useUser() {
return useContext(UserContext);
}
// App.js
import React from 'react';
import { UserProvider } from './UserContext';
import UserProfile from './UserProfile';
function App() {
return (
<UserProvider>
<UserProfile />
</UserProvider>
);
}
export default App;
// UserProfile.js
import React from 'react';
import { useUser } from './UserContext';
function UserProfile() {
const { userName, setUserName } = useUser();
const handleChangeName = () => {
const newName = prompt('Enter your name:');
if (newName) {
setUserName(newName);
}
};
return (
<div>
<p>Welcome, {userName}!</p>
<button onClick={handleChangeName}>Change Name</button>
</div>
);
}
export default UserProfile;
4. useReducer
useReducer
is usually preferable to useState
when you have complex state logic that involves multiple sub-values or when the next state depends on the previous one. You can use useReducer
when managing more complex state logic, particularly when state transitions are based on more than one value.
Usage:
Basic Usage:
import { useReducer } from 'react'; 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(); } } function Counter() { const [state, dispatch] = useReducer(reducer, { count: 0 }); return ( <div> <p>Count: {state.count}</p> <button onClick={() => dispatch({ type: 'increment' })}>+</button> <button onClick={() => dispatch({ type: 'decrement' })}>-</button> </div> ); }
5. useRef
useRef
returns a mutable ref object whose .current
property is initialized with the passed argument. It can be used to access DOM nodes or keep a mutable value that does not cause re-renders when updated.
When to Use: Use useRef
when you need to directly access a DOM element or when you need a persistent, mutable value that doesn't trigger a re-render.
Usage:
Accessing DOM Nodes:
import { useRef } from 'react'; function TextInputWithFocusButton() { const inputEl = useRef(null); const onButtonClick = () => { inputEl.current.focus(); }; return ( <> <input ref={inputEl} type="text" /> <button onClick={onButtonClick}>Focus the input</button> </> ); }
Keeping a Mutable Value:
import { useRef } from 'react'; function Stopwatch() { const timerRef = useRef(0); const startTimer = () => { timerRef.current = setInterval(() => { console.log('Tick'); }, 1000); }; const stopTimer = () => { clearInterval(timerRef.current); }; return ( <div> <button onClick={startTimer}>Start</button> <button onClick={stopTimer}>Stop</button> </div> ); }
6. useMemo
useMemo
returns a memoized value. It is useful for optimizing performance by memoizing expensive calculations.
When to Use: Use useMemo
when you have a computation that is expensive and you want to avoid recalculating it on every render.
Usage:
Basic Usage:
import { useMemo } from 'react'; function CalculationComponent({ a, b }) { const theResult = useMemo(() => { return a + b; // Expensive computation here }, [a, b]); return <div>The result is {theResult}</div>; }
7. useCallback
useCallback
returns a memoized callback. It is useful for passing callbacks to optimized child components that rely on reference equality to prevent unnecessary renders. You can use useCallback
when you need to pass a function as a prop to a child component and want to prevent unnecessary re-renders of that child component.
Basic Usage:
import { useCallback } from 'react'; function ParentComponent() { const [count, setCount] = useState(0); const handleClick = useCallback(() => { setCount(prevCount => prevCount + 1); }, []); // The empty array ensures the function is only created once return <ChildComponent onClick={handleClick} />; } function ChildComponent({ onClick }) { return <button onClick={onClick}>Click me</button>; }
8. useImperativeHandle
useImperativeHandle
customizes the instance value that is exposed when using ref
in parent components. It can be used to expose imperative methods to parent components. You can use useImperativeHandle
when you need to control the ref value passed to a child component from a parent component.
Basic Usage:
import { useImperativeHandle, forwardRef, useRef } from 'react'; function ChildComponent(props, ref) { useImperativeHandle(ref, () => ({ focus: () => { inputRef.current.focus(); }, })); const inputRef = useRef(); return <input ref={inputRef} />; } ChildComponent = forwardRef(ChildComponent); function ParentComponent() { const inputRef = useRef(); return ( <div> <ChildComponent ref={inputRef} /> <button onClick={() => inputRef.current.focus()}>Focus the input</button> </div> ); }
9. useLayoutEffect
useLayoutEffect
fires synchronously after all DOM mutations. Use it when you need to read layout from the DOM and synchronously re-render. You can use the useLayoutEffect
when you need to perform measurements or mutations that should block the browser's painting.
Basic Usage:
import { useLayoutEffect, useRef } from 'react'; function LayoutEffectExample() { const divRef = useRef(); useLayoutEffect(() => { const { height } = divRef.current.getBoundingClientRect(); console.log('Height:', height); }); return <div ref={divRef}>Hello, World!</div>; }
10. useDebugValue
useDebugValue
is used to display a label for custom hooks in React DevTools. You can use useDebugValue
when you are developing custom hooks and want to provide a useful label for them in React DevTools.
Here is an example on how to use useDebugValue:
import { useDebugValue, useState, useEffect } from 'react'; function useFriendStatus(friendID) { const [isOnline, setIsOnline] = useState(null); useEffect(() => { const handleStatusChange = (status) => { setIsOnline(status.isOnline); }; // Assume 'ChatAPI' is a module that manages the subscription ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange); return () => { ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange); }; }, [friendID]); useDebugValue(isOnline ? 'Online' : 'Offline'); return isOnline; }
11. useId
useId
is used to generate unique IDs that can be used for accessibility attributes. You can use useId
when you need a unique identifier for elements like form inputs to link them with their labels.
Basic Usage:
import { useId } from 'react'; function Form() { const id = useId(); return ( <div> <label htmlFor={id}>Name: </label> <input id={id} type="text" /> </div> ); }
12. useDeferredValue
useDeferredValue
lets you defer a value until the browser has had a chance to paint, improving performance. You can use useDeferredValue
when you have a state that is expensive to render and you want to defer its update to improve performance.
Here is an example of how to use useDeferredValue:
import { useState, useDeferredValue } from 'react';
function SearchResults({ query }) {
const deferredQuery = useDeferredValue(query);
const results = performExpensiveSearch(deferredQuery);
return <div>{results}</div>;
}
function performExpensiveSearch(query) {
// Perform the search and return the results
}
13. useSyncExternalStore
useSyncExternalStore
lets you subscribe to an external store and update the state in sync with it. Use useSyncExternalStore
when you need to subscribe to an external data source and update the state in your component in sync with it.
Basic Usage:
import { useSyncExternalStore } from 'react'; function useWindowSize() { const getSnapshot = () => ({ width: window.innerWidth, height: window.innerHeight, }); const subscribe = (callback) => { window.addEventListener('resize', callback); return () => window.removeEventListener('resize', callback); }; return useSyncExternalStore(subscribe, getSnapshot); } function WindowSizeComponent() { const size = useWindowSize(); return ( <div> Width: {size.width}, Height: {size.height} </div> ); }
14. useTransition
useTransition
lets you defer state updates that are not urgent, keeping the app responsive during heavy computations. You can use useTransition
when you have non-urgent updates that can be deferred to keep the UI responsive.
import { useState, useTransition } from 'react';
function App() {
const [isPending, startTransition] = useTransition();
const [value, setValue] = useState('');
const handleChange = (e) => {
startTransition(() => {
setValue(e.target.value);
});
};
return (
<div>
<input type="text" onChange={handleChange} />
{isPending ? 'Loading...' : <div>{value}</div>}
</div>
);
}
Conclusion
React Hooks provide a powerful way to build functional components with all the features you need for managing state, side effects, context, refs, and more. Understanding and effectively using these hooks will significantly enhance your ability to build complex and efficient React applications. This guide serves as a comprehensive reference to help you leverage the full potential of React Hooks.