Building Custom Hooks in React: Best Practices and Examples
React Hooks have revolutionized the way developers build components, allowing for the easy encapsulation and reuse of stateful logic across multiple components. While React provides several built-in hooks like useState, useEffect, and useContext, you can also create your own custom hooks to further streamline your code and enhance its reusability.
M Zeeshan
8/16/20244 min read
What Are Custom Hooks?
Custom hooks are JavaScript functions that utilize one or more of React's built-in hooks to encapsulate reusable logic. They allow you to extract component logic into a reusable function, making your components cleaner and more focused on rendering UI. Custom hooks can manage state, handle side effects, or perform any other tasks that a regular function can, but with the added benefit of React's hook capabilities.
Why Use Custom Hooks?
Code Reusability: Custom hooks allow you to extract and reuse logic across multiple components, reducing duplication and making your codebase more DRY (Don't Repeat Yourself).
Separation of Concerns: By isolating specific logic into custom hooks, your components become more focused on their primary responsibility—rendering UI—while the hooks manage the underlying logic.
Improved Readability: Custom hooks can encapsulate complex logic, making your components easier to read and understand.
Testability: Custom hooks can be tested independently, which simplifies unit testing and improves code reliability.
How to Build a Custom Hook
Let's walk through the process of building a simple custom hook and discuss best practices along the way.
Example: useFetch - A Custom Hook for Data Fetching
Fetching data from an API is a common task in many React applications. Instead of duplicating the same useEffect and useState logic across multiple components, you can encapsulate this logic in a custom hook.
javascript
Copy code
import { useState, useEffect } from 'react'; function useFetch(url) { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { const fetchData = async () => { setLoading(true); try { const response = await fetch(url); if (!response.ok) { throw new Error('Network response was not ok'); } const data = await response.json(); setData(data); } catch (error) { setError(error.message); } finally { setLoading(false); } }; fetchData(); }, [url]); return { data, loading, error }; } export default useFetch;
In this example, the useFetch hook takes a URL as an argument and handles the logic for fetching data, managing loading state, and error handling. It returns the fetched data, the loading state, and any errors, which can then be used in a component.
Example Usage of useFetch Hook:
javascript
Copy code
import React from 'react'; import useFetch from './useFetch'; function App() { const { data, loading, error } = useFetch('https://api.example.com/data'); if (loading) return <p>Loading...</p>; if (error) return <p>Error: {error}</p>; return ( <div> <h1>Data</h1> <pre>{JSON.stringify(data, null, 2)}</pre> </div> ); } export default App;
The App component uses the useFetch hook to fetch data from an API, and handles the UI based on the loading, error, and data states returned by the hook.
Best Practices for Building Custom Hooks
Prefix with use: Always prefix your custom hooks with use (e.g., useFetch, useAuth). This is not just a naming convention; it ensures that React can correctly identify your function as a hook and enforce the rules of hooks, such as only calling them at the top level of a component or another hook.
Keep Hooks Focused: A custom hook should have a single responsibility. If your hook is doing too much, consider breaking it down into smaller, more focused hooks. For example, instead of creating a monolithic useAuth hook that handles both authentication and user data fetching, you could create separate hooks like useLogin and useUserData.
Leverage Built-in Hooks: Utilize React's built-in hooks like useState, useEffect, useContext, useReducer, etc., within your custom hooks to manage state, side effects, and context. This allows you to build on top of React's robust hook architecture while keeping your logic modular and reusable.
Avoid Side Effects in Return Values: A custom hook should return values or functions that don't directly trigger side effects. For instance, avoid returning functions that immediately initiate data fetching or state changes upon being called. Instead, return values or functions that the component can use to trigger actions when needed.
Handle Dependencies Correctly: When using useEffect or other hooks with dependencies, ensure you pass all necessary dependencies in the dependency array. This prevents potential bugs caused by stale closures or missed updates.
Document Your Hooks: Provide clear documentation for your custom hooks, including what arguments they take, what they return, and any side effects or assumptions they make. This helps other developers (or your future self) understand how to use them effectively.
Test Your Hooks: Custom hooks, like any other piece of logic, should be thoroughly tested. Use React's testing utilities, such as react-testing-library, to create tests that verify the hook's behavior under different conditions.
Advanced Example: useLocalStorage - Persisting State with Local Storage
Here’s a more advanced example where we build a custom hook that manages state and persists it in the browser’s local storage.
javascript
Copy code
import { useState, useEffect } from 'react'; function useLocalStorage(key, initialValue) { const [storedValue, setStoredValue] = useState(() => { try { const item = window.localStorage.getItem(key); return item ? JSON.parse(item) : initialValue; } catch (error) { console.error(error); return initialValue; } }); const setValue = (value) => { try { const valueToStore = value instanceof Function ? value(storedValue) : value; setStoredValue(valueToStore); window.localStorage.setItem(key, JSON.stringify(valueToStore)); } catch (error) { console.error(error); } }; return [storedValue, setValue]; } export default useLocalStorage;
The useLocalStorage hook synchronizes state with the browser’s local storage. It stores the state in local storage whenever it changes and initializes the state with the stored value (or a default value) on component mount.
Example Usage of useLocalStorage Hook:
javascript
import React from 'react'; import useLocalStorage from './useLocalStorage'; function App() { const [name, setName] = useLocalStorage('name', 'John Doe'); return ( <div> <input type="text" value={name} onChange={(e) => setName(e.target.value)} /> <p>Your name is: {name}</p> </div> ); } export default App;
In this example, the useLocalStorage hook is used to persist the name state in local storage. The value will be retained even after the page is refreshed, providing a seamless user experience.