In the world of React, hooks have revolutionized how we manage state and side effects in functional components. However, as applications become complex, we often find ourselves repeating logic across different components. This is where custom hooks come in handy. Custom hooks allow us to extract reusable logic, making our components cleaner and more maintainable.
In this post, we'll explore how to create custom hooks and demonstrate a real-world example that enhances code reusability and abstraction. To get the most out of this post, it's beneficial if you're already familiar with the basics of React, including functional components, the use of built-in hooks like useState
and useEffect
, and fundamental JavaScript concepts such as Promises and asynchronous operations. If you're comfortable with these concepts, you'll find it easier to follow along and implement custom hooks in your projects.
Custom hooks enable you to:
- Encapsulate Logic: Separate logic from UI components, making code more modular and easier to manage.
- Reuse Logic: Share common logic across multiple components, reducing duplication.
- Simplify Components: Keep components focused on rendering UI by offloading business logic to hooks.
Creating a Custom Hook: A Practical Example
Let's consider a common use case: fetching and managing data from an API. We'll create a custom hook, useFetch
, that handles data fetching, loading states, and errors.
Step 1: Setup the Basic Hook Structure
import { useState, useEffect } from 'react';
const 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 result = await response.json();
setData(result);
} catch (error) {
setError(error.message);
} finally {
setLoading(false);
}
};
fetchData();
}, [url]);
return { data, loading, error };
};
export default useFetch;
Step 2: Using the Custom Hook in a Component
Now that we have our custom useFetch
hook, let's use it in a component.
import React from 'react';
import useFetch from './useFetch';
const UserList = () => {
const { data: users, loading, error } = useFetch('https://jsonplaceholder.typicode.com/users');
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
};
export default UserList;
Step 3: Enhancing the Custom Hook
To make our useFetch
hook more versatile, we'll add a feature that allows re-fetching data on demand. This can be useful in scenarios where you need to refresh the data in response to user actions or other events.
To achieve this, we will refactor the fetchData
function out of the useEffect
hook and wrap it in a useCallback
hook. This approach serves two purposes:
Reusability: By extracting
fetchData
, we can call it directly from components using the hook, allowing us to re-fetch data as needed.Memory Optimization: Wrapping
fetchData
inuseCallback
ensures that the function instance remains stable across re-renders unless the dependencies change. This prevents unnecessary re-creation of the function, which can help optimize memory usage and reduce the chances of re-triggering the effect unintentionally.
Hereโs the updated implementation:
import { useState, useEffect, useCallback } from 'react';
const useFetch = (url) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const fetchData = useCallback(async (abortController) => {
setLoading(true);
try {
const response = await fetch(url, {
signal: abortController.signal
});
if (!response.ok) {
throw new Error('Network response was not ok');
}
const result = await response.json();
setData(result);
} catch (error) {
if (error.name === 'AbortError') {
console.log('Request was cancelled');
} else {
setError(error.message);
}
} finally {
setLoading(false);
}
}, [url]);
useEffect(() => {
// AbortController is used to abort ongoing fetch requests when the component unmounts or the URL changes
const abortController = new AbortController();
fetchData(abortController);
// Cleanup function to cancel the request when the component unmounts or the URL changes
return () => {
abortController.abort();
};
}, [fetchData]);
return { data, loading, error, refetch: fetchData };
};
export default useFetch;
Now, the useFetch
hook provides a refetch
function that can be called to re-fetch the data.
Other Use Cases Where Custom Hooks Are Ideal
Here are 3 other common use cases where custom hooks can be particularly beneficial:
-
useLocalStorage
: Managing state that persists across sessions can be challenging. TheuseLocalStorage
custom hook can simplify this by abstracting the logic for storing and retrieving values from thelocalStorage
API. It provides a way to keep the component state in sync with local storage, ensuring that data is saved even if the user closes or refreshes the browser.
Example Implementation:
// https://usehooks.com/useLocalStorage
import { useState } from 'react';
// Hook
function useLocalStorage(key, initialValue) {
// State to store our value
// Pass initial state function to useState so logic is only executed once
const [storedValue, setStoredValue] = useState(() => {
if (typeof window === 'undefined') {
return initialValue;
}
try {
// Get from local storage by key
const item = window.localStorage.getItem(key);
// Parse stored JSON or if none return initialValue
return item ? JSON.parse(item) : initialValue;
} catch (error) {
// If error also return initialValue
console.log(error);
return initialValue;
}
});
// Return a wrapped version of useState's setter function that ...
// ... persists the new value to localStorage.
const setValue = (value) => {
try {
// Allow value to be a function so we have the same API as useState
const valueToStore = value instanceof Function ? value(storedValue) : value;
// Save state
setStoredValue(valueToStore);
// Save to local storage
if (typeof window !== 'undefined') {
window.localStorage.setItem(key, JSON.stringify(valueToStore));
}
} catch (error) {
// A more advanced implementation would handle the error case
console.log(error);
}
};
return [storedValue, setValue];
}
-
useWindowSize
: Handling responsive design often requires tracking the window size to adjust layouts or elements dynamically. TheuseWindowSize
hook abstracts the logic for detecting and reacting to window resize events, making it easy to implement responsive UI elements.
Example Implementation:
import { useState, useEffect } from 'react';
const useWindowSize = () => {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight,
});
useEffect(() => {
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight,
});
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return size;
};
-
useDebounce
: TheuseDebounce
hook is useful for delaying the execution of a function after a certain period, typically to avoid calling an expensive operation multiple times in quick succession. This is particularly useful for handling search inputs, where you want to wait until the user stops typing before making an API call.
Example Implementation:
import { useState, useEffect } from 'react';
const useDebounce = (value, delay) => {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
};
// Use in a component
import { useDebounce } from './useDebounce';
const SearchComponent = () => {
const [searchTerm, setSearchTerm] = useState('')
const debouncedSearchTerm = useDebounce(searchTerm, 500) // Will only trigger API call 500ms after user stops typing!
useEffect(() => {
if(debouncedSearchTerm) {
// Make API call!
}
}, [debouncedSearchTerm])
return <input onChange={e => setSearchTerm(e.target.value)} />
}
Each of these custom hooks addresses a specific problem and can significantly simplify your code by abstracting common logic into reusable components. This not only reduces duplication but also enhances the clarity and maintainability of your React applications.
Best Practices for Custom Hooks
- Use the
use
prefix: Ensure your custom hook's name starts withuse
to follow React's conventions and enable the hook rules. - Keep hooks focused: Custom hooks should do one thing well. Avoid overloading them with too much logic.
- Handle cleanup: Use the
useEffect
cleanup function to handle any necessary cleanup operations, like cancelling network requests.
Conclusion
Custom hooks are a powerful tool in React that can help you write cleaner, more maintainable code. By encapsulating logic into reusable hooks, you can keep your components focused on rendering UI and improve your application's scalability.
Feel free to use and extend the hooks outlined above, and try creating custom hooks in your next project, and see how they can simplify your codebase! Check out useHooks for many other useful hooks. Make sure to keep an eye out on React 19 as well. The 2024 conference happened recently and the new hooks are off the hook. Till next time, useTime
...
Top comments (2)
The conclusion ๐๐
Thanks bro!!