React Hooks are a powerful feature introduced in React 18 that allows developers to add state and other React features to functional components. This not only simplifies the code but also makes it more reusable and easier to test.
In this comprehensive guide, we will cover everything you need to know to master React Hooks, including the different types of hooks, when and where to use them, and some real-world examples.
What are React Hooks?
React Hooks are functions that allow you to use state and other React features in functional components. Prior to Hooks, functional components were “stateless” and could not manage state or lifecycle methods. Hooks change that by introducing new functions that allow functional components to manage state and use other React features.
Hooks come in different types and have different use cases. The most common hooks include useState, useEffect, useContext, useReducer, useCallback, and useMemo.
Different Types of React Hooks
useState: Adds state to functional components. Returns an array with the current state value and a function to update that state.
useEffect: Performs side effects in functional components. Runs a function after every render, and takes an optional array of dependencies to control when the effect should run.
useContext: Allows you to consume a context value created by a parent component.
useReducer: An alternative to useState that manages state with a reducer function. Returns the current state value and a dispatch function to update that state.
useCallback: Memoizes a function, preventing it from re-rendering unless its dependencies have changed.
useMemo: Memoizes a value, preventing it from recomputing unless its dependencies have changed.
useRef: Returns a mutable ref object that persists for the lifetime of the component.
useLayoutEffect: Runs a function after every render, but before the browser paints to the screen.
useImperativeHandle: Allows a parent component to access a child component’s imperative methods.
useDebugValue: Displays a label for custom hooks in React DevTools.
Custom Hooks: Custom hooks are functions that use one or more of the built-in React hooks to create reusable logic for a specific task or feature.
Real-World Examples
1. Managing State with useState
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
function handleIncrement() {
setCount(count + 1);
}
function handleDecrement() {
setCount(count - 1);
}
return (
<div>
<h1>Count: {count}</h1>
<button onClick={handleIncrement}>Increment</button>
<button onClick={handleDecrement}>Decrement</button>
</div>
);
}
In this example, we use the useState hook to manage a count state variable in a functional component. The useState hook returns an array with two values: the current state value (count) and a function to update the state (setCount). We define two event handler functions (handleIncrement and handleDecrement) that update the count state using the setCount function.
2. Fetching Data with useEffect
import React, { useState, useEffect } from 'react';
function UserList() {
const [users, setUsers] = useState([]);
useEffect(() => {
async function fetchUsers() {
const response = await fetch('https://jsonplaceholder.typicode.com/users');
const data = await response.json();
setUsers(data);
}
fetchUsers();
}, []);
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
In this example, we use the useEffect hook to fetch data from an API and update the state using the setUsers function. We pass an empty array as the second argument to useEffect, which means that the effect will only run once when the component is mounted.
3. Using useContext to Share Data
import React, { useContext } from 'react';
const ThemeContext = React.createContext('light');
function ThemeButton() {
const theme = useContext(ThemeContext);
return <button className={theme}>Theme Button</button>;
}
function App() {
return (
<ThemeContext.Provider value="dark">
<ThemeButton />
</ThemeContext.Provider>
);
}
In this example, we use the useContext hook to access the value of a context object in a functional component. We create a context object using the createContext function and provide a default value of ‘light’. We then create a ThemeButton component that uses the useContext hook to access the current theme value. Finally, we render the ThemeButton component within a ThemeContext.Provider component that provides a value of ‘dark’.
4. Managing Complex State with useReducer
import React, { useReducer } from 'react';
const initialState = { count: 0 };
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, initialState);
function handleIncrement() {
dispatch({ type: 'increment' });
}
function handleDecrement() {
dispatch({ type: 'decrement' });
}
return (
<div>
<h1>Count: {state.count}</h1>
<button onClick={handleIncrement}>Increment</button>
<button onClick={handleDecrement}>Decrement</button>
</div>
);
}
In this example, we use the useReducer hook to manage a more complex state object that includes a count property. We define a reducer function that takes the current state and an action object, and returns a new state based on the action type. We then use the useReducer hook to create a state object (state) and a dispatch function (dispatch) that we can use to update the state by dispatching actions.
We define two event handler functions (handleIncrement and handleDecrement) that dispatch the appropriate action to update the count state. Finally, we render the current count state value and the two buttons that trigger the event handlers.
5. Using useCallback To Render SearchBar Component
import React, { useState, useCallback } from 'react';
import SearchResults from './SearchResults';
function SearchBar() {
const [searchTerm, setSearchTerm] = useState('');
const [searchResults, setSearchResults] = useState([]);
const handleSearch = useCallback(() => {
fetch(`https://api.example.com/search?q=${searchTerm}`)
.then(response => response.json())
.then(data => setSearchResults(data.results))
.catch(error => console.error(error));
}, [searchTerm]);
const handleInputChange = useCallback(event => {
setSearchTerm(event.target.value);
}, []);
return (
<div>
<input type="text" value={searchTerm} onChange={handleInputChange} />
<button onClick={handleSearch}>Search</button>
<SearchResults results={searchResults} />
</div>
);
}
export default SearchBar;
In this example, we have a SearchBar component that renders an input field and a search button. When the user types a search query into the input field and clicks the search button, we use the useCallback hook to define a memoized version of the handleSearch function. This function fetches search results from an API and updates the searchResults state with the returned data.
By using useCallback to memoize the handleSearch function, we can ensure that it is only re-created when the searchTerm state changes, rather than every time the component re-renders. This can lead to better performance and reduce unnecessary re-renders.
We also use useCallback to memoize the handleInputChange function, which updates the searchTerm state when the user types into the input field. By memorizing this function, we can avoid creating a new function instance on every re-render, which can improve performance.
Finally, we pass the searchResults state down to a SearchResults component that renders the search results in a list.
6. Using useMemo Display List of Orders
import React, { useState, useMemo } from 'react';
import OrderList from './OrderList';
function Orders() {
const [orders, setOrders] = useState([]);
const [filter, setFilter] = useState('');
// This function filters the orders based on the current filter value
const filteredOrders = useMemo(() => {
return orders.filter(order => order.status === filter);
}, [orders, filter]);
function handleFilterChange(event) {
setFilter(event.target.value);
}
function handleOrderUpdate(updatedOrder) {
setOrders(prevOrders =>
prevOrders.map(order => (order.id === updatedOrder.id ? updatedOrder : order))
);
}
return (
<div>
<h2>Orders</h2>
<div>
<label htmlFor="filter">Filter:</label>
<select id="filter" value={filter} onChange={handleFilterChange}>
<option value="">All</option>
<option value="pending">Pending</option>
<option value="completed">Completed</option>
</select>
</div>
<OrderList orders={filteredOrders} onOrderUpdate={handleOrderUpdate} />
</div>
);
}
export default Orders;
In this example, we have an Orders component that displays a list of orders and allows the user to filter them by status (either “pending” or “completed”). We use the useMemo hook to memoize a function that filters the orders based on the current filter value. By memorizing this function, we can avoid filtering the orders on every re-render, which can improve performance.
The filteredOrders variable is updated whenever the orders or filter state changes. This ensures that the filtered orders are always up to date with the latest state.
We also define a handleFilterChange function that updates the filter state when the user selects a new filter value. When the filter state changes, the filteredOrders variable is re-evaluated using the memoized function.
Finally, we pass the filteredOrders state down to an OrderList component that renders the list of orders. We also define a handleOrderUpdate function that is called when an order is updated. This function updates the orders state with the updated order.
7. Using useRef To Submit Comment
import React, { useState, useRef } from 'react';
function CommentForm({ onSubmit }) {
const [comment, setComment] = useState('');
const nameInputRef = useRef(null);
function handleSubmit(event) {
event.preventDefault();
const name = nameInputRef.current.value;
onSubmit({ name, comment });
setComment('');
}
function handleCommentChange(event) {
setComment(event.target.value);
}
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="name">Name:</label>
<input id="name" type="text" ref={nameInputRef} />
</div>
<div>
<label htmlFor="comment">Comment:</label>
<textarea id="comment" value={comment} onChange={handleCommentChange} />
</div>
<button type="submit">Submit</button>
</form>
);
}
export default CommentForm;
In this example, we have a CommentForm component that allows the user to submit a comment with their name. We use the useRef hook to create a reference to the name input field so that we can retrieve its value in the handleSubmit function.
When the user submits the form, the handleSubmit function is called. This function retrieves the current value of the name input field using the nameInputRef reference and passes the name and comment to the onSubmit prop. We also reset the comment state to an empty string.
We use the handleCommentChange function to update the comment state whenever the user types into the comment textarea.
Using a ref to retrieve the value of a form field is a common use case for the useRef hook. It allows us to access the value of the field without needing to store it in state. Additionally, the ref can be used to access other properties of the input field, such as its width or height, if needed.
8. Using useLayoutEffect To Update The State
import React, { useState, useLayoutEffect } from 'react';
function App() {
const [count, setCount] = useState(0);
const [width, setWidth] = useState(0);
useLayoutEffect(() => {
setWidth(document.documentElement.clientWidth);
}, [count]);
function handleClick() {
setCount(count + 1);
}
return (
<div>
<h1>Count: {count}</h1>
<h2>Width: {width}px</h2>
<button onClick={handleClick}>Increment Count</button>
</div>
);
}
export default App;
In this example, we use useLayoutEffect to update the width state whenever the count state changes. The useLayoutEffect hook runs synchronously after all DOM mutations, which makes it a good choice for updating the layout based on changes in state.
In the useLayoutEffect callback, we set the width state to the current width of the document element. We use document.documentElement.clientWidth to get the width of the viewport. This code runs after every update to the count state because we passed [count] as the second argument to useLayoutEffect.
We use the count state to trigger a re-render of the component when the user clicks the Increment Count button. This causes the useLayoutEffect hook to run again and update the width state based on the new count value.
We then display the current count and width in the component’s UI.
This example demonstrates how useLayoutEffect can be used to update the layout of a component in response to changes in state.
9. Using useImperativeHandle Expose Two Methods
import React, { useRef, useImperativeHandle, forwardRef } from 'react';
const FancyInput = forwardRef((props, ref) => {
const inputRef = useRef(null);
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
},
getValue: () => {
return inputRef.current.value;
}
}));
return <input type="text" ref={inputRef} />;
});
export default FancyInput;
In this example, we create a FancyInput component that uses the useImperativeHandle hook to expose two methods on its ref: focus and getValue.
The focus method allows the parent component to programmatically set the focus on the input field when needed. The getValue method allows the parent component to retrieve the current value of the input field.
We use the useRef hook to create a reference to the input field. We then use the useImperativeHandle hook to define an object that contains the focus and getValue methods. We pass this object as the second argument to useImperativeHandle, along with the inputRef reference. This makes the methods available on the ref that’s passed to the FancyInput component.
The parent component can then use these methods to interact with the FancyInput component. For example:
import React, { useRef } from 'react';
import FancyInput from './FancyInput';
function App() {
const inputRef = useRef(null);
function handleClick() {
inputRef.current.focus();
}
function handleGetValue() {
console.log(inputRef.current.getValue());
}
return (
<div>
<FancyInput ref={inputRef} />
<button onClick={handleClick}>Focus Input</button>
<button onClick={handleGetValue}>Get Value</button>
</div>
);
}
export default App;
In this example, we create an App component that renders a FancyInput component and two buttons. We use the useRef hook to create a reference to the FancyInput component.
When the Focus Input button is clicked, we call the focus method on the inputRef reference, which sets the focus on the input field.
When the Get Value button is clicked, we log the current value of the input field to the console using the getValue method on the inputRef reference.
This example demonstrates how useImperativeHandle can be used to expose methods on a component’s ref, allowing the parent component to interact with the component in a more flexible and powerful way.
10. Using useDebugValue Hook To Provide Meaningful Labels For a Custom Hook
The useDebugValue hook is primarily used for debugging purposes, as it allows you to display custom labels for your custom hooks in the React DevTools. Here’s an example of how you can use the useDebugValue hook to provide more meaningful labels for a custom hook:
import { useState, useEffect, useDebugValue } from 'react';
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch(url);
const json = await response.json();
setData(json);
} catch (error) {
setError(error);
} finally {
setLoading(false);
}
};
fetchData();
}, [url]);
useDebugValue({ data, loading, error }, (data) => {
return data ? `Data (${Object.keys(data).length})` : 'Loading...';
});
return { data, loading, error };
}
In this example, we create a custom hook called useFetch that fetches data from a given URL and returns the data, loading state, and error state.
We use the useDebugValue hook to provide a custom label for this hook when it’s displayed in the React DevTools. We pass an object that contains the data, loading, and error states to useDebugValue, along with a function that returns a label based on the current state.
In this case, we return a label that displays the number of keys in the data object if data is not null, or “Loading…” if loading is true. This makes it easier to identify the hook in the React DevTools and see the current state of the hook at a glance.
Overall, useDebugValue is a powerful tool for improving the debugging experience in React, as it allows you to provide custom labels for your custom hooks and display meaningful information about their current state in the React DevTools.
Custom Hooks
Custom hooks are functions that use one or more of the built-in React hooks to create reusable logic for a specific task or feature. They allow you to abstract away complex logic and state management into a single function that can be used across multiple components.
Here is an example of how to create and use a custom hook:
import { useState, useEffect } from 'react';
function useFetch(url) {
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
async function fetchData() {
setIsLoading(true);
try {
const response = await fetch(url);
const json = await response.json();
setData(json);
} catch (error) {
setError(error);
} finally {
setIsLoading(false);
}
}
fetchData();
}, [url]);
return { data, isLoading, error };
}
function MyComponent() {
const { data, isLoading, error } = useFetch('https://jsonplaceholder.typicode.com/todos/1');
if (isLoading) {
return <div>Loading...</div>;
}
if (error) {
return <div>{error.message}</div>;
}
return <div>{data.title}</div>;
}
In this example, we created a custom hook called useFetch
that fetches data from a given URL and returns an object with three properties: data
, isLoading
, and error
. We then use this custom hook in the MyComponent
function to fetch data from the JSONPlaceholder API and display the title of the first todo item.
By creating a custom hook, we can reuse the logic for fetching data across different components without having to repeat the same code. This makes our code more modular and easier to maintain.
Conclusion
Mastering React hooks is essential for building complex and performant React applications. This comprehensive guide has covered all the major React hooks with real-world examples to help you understand their purpose, syntax, and best use cases.
Remember to always use hooks in accordance with the React rules of hooks, and to test and optimize your code for performance. With practice and experience, you can become a master of React hooks and take your React skills to the next level.