How to Create Custom Hook
How to Create Custom Hook React has revolutionized frontend development by introducing a component-based architecture and the powerful concept of Hooks. Since their introduction in React 16.8, Hooks have become the standard way to manage state, side effects, and lifecycle logic in functional components. While React provides a rich set of built-in Hooks like useState, useEffect, and useContext, rea
How to Create Custom Hook
React has revolutionized frontend development by introducing a component-based architecture and the powerful concept of Hooks. Since their introduction in React 16.8, Hooks have become the standard way to manage state, side effects, and lifecycle logic in functional components. While React provides a rich set of built-in Hooks like useState, useEffect, and useContext, real-world applications often require reusable logic that goes beyond these primitives. This is where custom Hooks come into play.
A custom Hook is a JavaScript function whose name starts with use and that may call other Hooks. It allows developers to extract component logic into reusable functions, promoting code reuse, testability, and maintainability. Unlike higher-order components or render props, custom Hooks dont introduce additional nesting in the component tree, making them cleaner and more intuitive to use.
Creating custom Hooks is not just a coding techniqueits a mindset shift toward modular, declarative, and scalable React applications. Whether youre managing complex form state, integrating with third-party APIs, handling animations, or syncing data across components, custom Hooks empower you to encapsulate logic in a way thats both predictable and composable.
In this comprehensive guide, well walk you through everything you need to know to create effective, production-ready custom Hooks. From the foundational concepts to advanced patterns and real-world examples, youll learn how to design Hooks that are reusable, performant, and aligned with Reacts best practices.
Step-by-Step Guide
Understand the Rules of Hooks
Before writing your first custom Hook, its critical to understand the two fundamental rules that govern all Hookswhether built-in or custom:
- Only call Hooks at the top levelnever inside loops, conditions, or nested functions. This ensures Hooks are called in the same order during every render, preserving Reacts internal state tracking.
- Only call Hooks from React functional components or other custom Hooksnever from regular JavaScript functions. This rule enforces the predictable execution context required for Hooks to work correctly.
These rules are enforced by the ESLint plugin eslint-plugin-react-hooks, which should be installed and configured in every React project. Violating them leads to unpredictable behavior, state corruption, and hard-to-debug errors.
Identify Reusable Logic
Custom Hooks are born out of repetition. Look for patterns in your components where the same logic is duplicated across multiple files. Common candidates include:
- Fetching and managing data from an API
- Handling form inputs and validation
- Tracking user interactions like clicks, scrolls, or keyboard events
- Managing local storage or session state
- Integrating with third-party libraries (e.g., Google Maps, Stripe)
For example, if you have three components that all fetch user profile data using fetch and handle loading and error states similarly, that logic is a perfect candidate for extraction into a custom Hook.
Create the Hook Function
Start by creating a new JavaScript file in your project, typically under a hooks/ directory. Name the file using the use prefix followed by a descriptive name in camelCase. For instance:
useUser.js
Inside this file, define a function that begins with use and returns the values or functions your component needs:
javascript
// hooks/useUser.js
import { useState, useEffect } from 'react';
function useUser(userId) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchUser = async () => {
try {
setLoading(true);
const response = await fetch(/api/users/${userId});
if (!response.ok) {
throw new Error('Failed to fetch user');
}
const data = await response.json();
setUser(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
if (userId) {
fetchUser();
}
}, [userId]);
return { user, loading, error };
}
export default useUser;
This Hook encapsulates the entire data-fetching workflow: state management for user data, loading status, and error handlingall in a single, reusable unit.
Use the Hook in a Component
Now that the Hook is defined, you can use it in any functional component. Import it like any other module:
javascript
// components/UserProfile.js
import React from 'react';
import useUser from '../hooks/useUser';
const UserProfile = ({ userId }) => {
const { user, loading, error } = useUser(userId); if (loading) return
Loading user profile...; if (error) return
Error: {error}; if (!user) return
No user found.;
return (
{user.name}
{user.email}
);
};
export default UserProfile;
Notice how the component is now simpler, cleaner, and focused solely on rendering. The complexity of data fetching is abstracted away into the Hook.
Accept Parameters and Return Values
Custom Hooks can accept any number of parameters, just like regular functions. These parameters allow the Hook to be dynamic and context-aware. In the example above, userId is a parameter that changes the behavior of the Hook.
Custom Hooks can also return objects, arrays, or single values depending on your needs. Returning an object is often preferred because it makes the returned values explicit and self-documenting:
javascript
return { data, loading, error, refetch };
Alternatively, if you need to return multiple related values, you can use destructuring with an array:
javascript
return [data, loading, error];
However, object returns are more maintainable when the number of returned values grows, as they avoid dependency on order and support optional fields.
Handle Dependencies Correctly
When your custom Hook uses useEffect, useCallback, or useMemo, you must provide a dependency array. This array tells React which values the effect or memoized value depends on. Omitting dependencies can lead to stale closures and bugs.
Always include all values used inside the effect that come from outside the Hooks scope. For example:
javascript
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];
}
This Hook doesnt need a dependency array in useEffect because it doesnt use one. But if it did, say, to respond to changes in key, youd add it:
javascript
useEffect(() => {
// re-sync when key changes
}, [key]);
Test Your Hook
Testing custom Hooks is crucial to ensure reliability. React provides the @testing-library/react-hooks library to help you test Hooks in isolation.
Install it:
npm install @testing-library/react-hooks
Then write a test:
javascript
// hooks/__tests__/useUser.test.js
import { renderHook, act } from '@testing-library/react-hooks';
import useUser from '../useUser';
describe('useUser', () => {
global.fetch = jest.fn();
beforeEach(() => {
fetch.mockClear();
});
it('returns loading true initially', async () => {
const { result } = renderHook(() => useUser(123));
expect(result.current.loading).toBe(true);
});
it('fetches user data and sets state', async () => {
const mockUser = { id: 123, name: 'Jane Doe', email: 'jane@example.com' };
fetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(mockUser),
});
const { result, waitForNextUpdate } = renderHook(() => useUser(123));
await waitForNextUpdate();
expect(result.current.user).toEqual(mockUser);
expect(result.current.loading).toBe(false);
expect(result.current.error).toBeNull();
});
});
This approach lets you verify that your Hook behaves correctly under different conditions without rendering a full component.
Refactor Existing Components
Once youve created a custom Hook, look for opportunities to refactor existing components that contain similar logic. Replace duplicated state and effect logic with calls to your new Hook.
For example, if you previously had two components fetching different types of data with nearly identical patterns, you can now abstract the common logic into a generic useApi Hook:
javascript
// hooks/useApi.js
import { useState, useEffect } from 'react';
function useApi(apiFunction, dependencies = []) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
const result = await apiFunction();
setData(result);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchData();
}, dependencies);
return { data, loading, error };
}
export default useApi;
Now, both user and product data fetching can use the same Hook:
javascript
const { data: user, loading: userLoading } = useApi(() => fetch('/api/user').then(r => r.json()));
const { data: products, loading: productsLoading } = useApi(() => fetch('/api/products').then(r => r.json()));
This dramatically reduces code duplication and centralizes error handling and loading states.
Best Practices
Follow Naming Conventions
Always prefix your custom Hook with use. This is not just a conventionits a signal to other developers (and to ESLint) that this function is a Hook and must be used according to the rules. Avoid names like fetchData or getLocalStorage. Instead, use useFetchData and useLocalStorage.
Keep Hooks Focused and Single-Purpose
A good custom Hook does one thing well. Avoid creating kitchen sink Hooks that handle multiple unrelated concerns. For example, dont create a useUserAndSettings Hook that fetches both user profile and theme preferences. Instead, create two separate Hooks: useUser and useTheme.
This promotes composability. Components can then combine Hooks as needed:
javascript
const { user, loading: userLoading } = useUser(userId);
const { theme, toggleTheme } = useTheme();
Each Hook remains testable, reusable, and understandable in isolation.
Use TypeScript for Type Safety
If youre using TypeScript, define types for your Hooks inputs and outputs. This improves developer experience, catches bugs at compile time, and makes your code self-documenting.
typescript
// hooks/useUser.ts
import { useState, useEffect } from 'react';
interface User {
id: number;
name: string;
email: string;
}
interface UseUserReturn {
user: User | null;
loading: boolean;
error: string | null;
}
function useUser(userId: number | null): UseUserReturn {
const [user, setUser] = useState
const [loading, setLoading] = useState
const [error, setError] = useState
useEffect(() => {
const fetchUser = async () => {
if (!userId) return;
try {
setLoading(true);
const response = await fetch(/api/users/${userId});
if (!response.ok) throw new Error('Failed to fetch user');
const data: User = await response.json();
setUser(data);
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
} finally {
setLoading(false);
}
};
fetchUser();
}, [userId]);
return { user, loading, error };
}
export default useUser;
With TypeScript, your IDE will provide autocomplete and type checking when the Hook is used, reducing runtime errors.
Avoid Side Effects in Render
Custom Hooks should never trigger side effects during rendering. All side effectsAPI calls, DOM manipulations, subscriptionsmust be contained within useEffect, useLayoutEffect, or similar. Never call fetch or localStorage.setItem directly in the Hook body outside of effects.
Handle Edge Cases Gracefully
Consider what happens when inputs change unexpectedly. For example, if a user navigates away from a page before an API call completes, you should cancel the request to avoid state updates on unmounted components.
Use an AbortController for fetch requests:
javascript
useEffect(() => {
const controller = new AbortController();
const fetchUser = async () => {
try {
setLoading(true);
const response = await fetch(/api/users/${userId}, { signal: controller.signal });
if (!response.ok) throw new Error('Failed to fetch user');
const data = await response.json();
setUser(data);
} catch (err) {
if (err.name === 'AbortError') return; // Ignore abort errors
setError(err.message);
} finally {
setLoading(false);
}
};
if (userId) {
fetchUser();
}
return () => {
controller.abort(); // Clean up on unmount
};
}, [userId]);
This prevents memory leaks and unnecessary state updates.
Document Your Hooks
Just like any public API, custom Hooks should be documented. Add JSDoc comments to explain:
- What the Hook does
- Expected parameters
- Returned values
- Any side effects or requirements (e.g., Must be called inside a component)
javascript
/**
* Fetches a user by ID from the API.
* @param {number | null} userId - The ID of the user to fetch. If null, no request is made.
* @returns {{ user: User | null, loading: boolean, error: string | null }} - Current user state and loading/error flags.
*/
function useUser(userId) { ... }
This documentation helps other developers use your Hook correctly and reduces onboarding time.
Use Composition Over Inheritance
React Hooks encourage composition. Instead of trying to build a complex Hook that handles every possible scenario, build small, focused Hooks and combine them.
For example, instead of creating a useFormWithValidationAndSubmit Hook, create:
useFormmanages input stateuseValidationvalidates fieldsuseSubmithandles form submission
Then compose them:
javascript
const { values, handleChange } = useForm(initialValues);
const { errors, isValid } = useValidation(values, validationSchema);
const { handleSubmit, submitting } = useSubmit(onSubmit, isValid);
This modular approach makes each part testable, reusable, and easier to debug.
Tools and Resources
Essential Libraries
Several libraries enhance the development and testing of custom Hooks:
- @testing-library/react-hooks Enables testing of Hooks in isolation without requiring a component wrapper.
- react-use A collection of over 100 well-tested, production-ready custom Hooks for common use cases (e.g.,
useLocalStorage,useMediaQuery,useAsync). - axios A popular HTTP client that integrates well with custom Hooks for API calls, with built-in cancellation and interceptors.
- React Query A powerful data-fetching library that provides Hooks like
useQueryanduseMutation. Consider using it instead of writing your own data-fetching Hooks from scratch. - Zod A TypeScript-first schema validation library that pairs excellently with form validation Hooks.
Development Tools
- ESLint with react-hooks plugin Enforces the Rules of Hooks and catches violations early. Install with:
npm install eslint-plugin-react-hooks --save-dev - React Developer Tools Browser extension that lets you inspect Hook state and component hierarchy in the DevTools.
- TypeScript Provides type safety and autocompletion, essential for maintaining large-scale Hook libraries.
- Vite or Create React App Modern toolchains that support Hot Module Replacement and fast builds, improving Hook iteration speed.
Learning Resources
- React Documentation: Reusing Logic with Custom Hooks Official guide from the React team.
- Kent C. Dodds: How to Write a Custom React Hook A widely respected tutorial with deep insights.
- Epic React: Custom Hooks Full course module by Kent C. Dodds.
- react-use GitHub Repository Study real-world examples of well-designed Hooks.
Real Examples
Example 1: useLocalStorage
Managing browser localStorage is a common requirement. Heres a robust, type-safe implementation:
javascript
// hooks/useLocalStorage.js
import { useState, useEffect } from 'react';
/**
* Manages state synchronized with localStorage.
* @param {string} key - The localStorage key.
* @param {*} initialValue - The initial value if key doesn't exist.
* @returns {[*, function]} - Current value and setter function.
*/
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;
Usage:
javascript
const [theme, setTheme] = useLocalStorage('theme', 'light');
Example 2: useDebounce
Debouncing input events (e.g., search boxes) prevents excessive API calls:
javascript
// hooks/useDebounce.js
import { useState, useEffect } from 'react';
/**
* Returns a debounced version of a value.
* @param {*} value - The value to debounce.
* @param {number} delay - Delay in milliseconds.
* @returns {*} - The debounced value.
*/
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
export default useDebounce;
Usage in a search component:
javascript
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearchTerm = useDebounce(searchTerm, 500);
useEffect(() => {
if (debouncedSearchTerm) {
fetch(/api/search?q=${debouncedSearchTerm});
}
}, [debouncedSearchTerm]);
Example 3: useWindowScrollPosition
Track scroll position for features like back to top buttons:
javascript
// hooks/useWindowScrollPosition.js
import { useState, useEffect } from 'react';
/**
* Tracks the current scroll position of the window.
* @returns {{ x: number, y: number }} - Current scroll coordinates.
*/
function useWindowScrollPosition() {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleScroll = () => {
setPosition({ x: window.pageXOffset, y: window.pageYOffset });
};
window.addEventListener('scroll', handleScroll, { passive: true });
return () => window.removeEventListener('scroll', handleScroll);
}, []);
return position;
}
export default useWindowScrollPosition;
Example 4: useClickOutside
Close dropdowns or modals when clicking outside:
javascript
// hooks/useClickOutside.js
import { useRef, useEffect } from 'react';
/**
* Calls a callback when a click occurs outside the specified ref.
* @param {React.RefObject} ref - The ref to detect clicks outside of.
* @param {Function} callback - Function to execute on outside click.
*/
function useClickOutside(ref, callback) {
useEffect(() => {
const handleClickOutside = (event) => {
if (ref.current && !ref.current.contains(event.target)) {
callback();
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [ref, callback]);
}
export default useClickOutside;
Usage:
javascript
const dropdownRef = useRef();
useClickOutside(dropdownRef, () => setIsOpen(false));
FAQs
Can I use a custom Hook in a class component?
No. Custom Hooks can only be called inside functional components or other custom Hooks. If you need to reuse logic in a class component, consider converting the class component to a functional one or extract the logic into a plain JavaScript utility function that doesnt rely on Hooks.
Whats the difference between a custom Hook and a utility function?
A utility function is a regular JavaScript function that performs a task but doesnt use React Hooks. A custom Hook is a function that calls one or more Hooks and must be used within a React component. Custom Hooks manage state and side effects; utility functions handle pure logic like formatting or calculations.
Can I create a custom Hook that returns JSX?
Technically yes, but its discouraged. Custom Hooks should return data or functions, not UI. Returning JSX breaks the separation of concerns and makes the Hook less reusable. Instead, return data and let the component decide how to render it.
How do I handle multiple instances of the same Hook?
Each time you call a custom Hook, React creates an independent instance of its internal state. So if you call useLocalStorage('theme') in two different components, each will have its own isolated localStorage key and state. This is intentional and desired behavior.
Do custom Hooks affect performance?
Custom Hooks themselves have negligible performance impact. However, if they contain expensive computations or unnecessary re-renders, they can. Use useMemo and useCallback inside your Hook to memoize values and functions when appropriate. Always profile with React DevTools to identify bottlenecks.
Can I use async/await inside a custom Hook?
Yes, but only inside effects like useEffect. You cannot use async directly in the Hook body. Wrap asynchronous logic in a function called from within useEffect.
How do I share a custom Hook across multiple projects?
Package your Hook as an npm module. Create a new project with a package.json, export your Hook from an index file, and publish it. Alternatively, use a monorepo with tools like Turborepo or Nx to share Hooks internally across applications.
Is it okay to call multiple Hooks in one component?
Yes. In fact, its encouraged. React Hooks are designed to be composed. A component can use useState, useEffect, useContext, and multiple custom Hooks simultaneously. Just ensure each Hook is called at the top level and in the same order every render.
Conclusion
Custom Hooks are one of Reacts most powerful features, transforming how we structure and share logic in modern applications. By encapsulating reusable stateful behavior into self-contained functions, they eliminate code duplication, improve readability, and make components more maintainable.
In this guide, weve explored the foundational principles of custom Hooksfrom understanding the Rules of Hooks to writing, testing, and documenting production-ready implementations. Weve examined best practices for modularity, type safety, and performance, and walked through real-world examples that demonstrate their versatility.
Whether youre building a simple form, integrating with an API, or managing complex UI interactions, custom Hooks give you the tools to write cleaner, more scalable code. The key is to start small: identify repetitive logic, extract it into a focused Hook, test it thoroughly, and then reuse it across your application.
As you continue to build with React, challenge yourself to ask: Can this logic be reused elsewhere? If the answer is yes, turn it into a custom Hook. Over time, youll build a library of reliable, battle-tested utilities that become the backbone of your React applications.
Remember: the goal isnt to create Hooks for the sake of itbut to write better, more maintainable code. With thoughtful design and disciplined practices, custom Hooks will elevate your development workflow and empower your team to build faster, smarter, and more consistently.