How to Create Custom Hook

How to Create Custom Hook Custom hooks are one of the most powerful features introduced in React 16.8 with the advent of Hooks. They allow developers to extract and reuse stateful logic across components in a clean, maintainable, and testable way. Unlike traditional patterns such as higher-order components (HOCs) or render props, custom hooks provide a more intuitive and flexible approach to shari

Nov 10, 2025 - 08:27
Nov 10, 2025 - 08:27
 3

How to Create Custom Hook

Custom hooks are one of the most powerful features introduced in React 16.8 with the advent of Hooks. They allow developers to extract and reuse stateful logic across components in a clean, maintainable, and testable way. Unlike traditional patterns such as higher-order components (HOCs) or render props, custom hooks provide a more intuitive and flexible approach to sharing logic without introducing additional nesting or complexity.

Creating a custom hook isnt just about code reuseits about structuring your applications logic in a way that enhances readability, promotes separation of concerns, and improves developer experience. Whether youre managing complex form state, handling real-time data subscriptions, or orchestrating animations, custom hooks empower you to encapsulate behavior and make your components leaner and more focused on rendering UI.

In this comprehensive guide, youll learn exactly how to create custom hooks from the ground up. Well walk through practical implementation steps, explore industry best practices, review essential tools, analyze real-world examples, and answer common questions. By the end, youll be equipped to design custom hooks that are reusable, reliable, and scalablecornerstones of modern React development.

Step-by-Step Guide

Understand the Rules of Hooks

Before creating a custom hook, its critical to understand the two fundamental rules that govern all hookswhether built-in or custom:

  1. Only call hooks at the top levelnever inside loops, conditions, or nested functions.
  2. Only call hooks from React function components or other custom hooksnever from regular JavaScript functions.

These rules ensure that React can maintain the correct order of hook calls during renders. Violating them leads to unpredictable behavior and runtime errors. Custom hooks are simply JavaScript functions that call other hooks internally, so they inherit these constraints. Always name your custom hooks with the prefix use (e.g., useLocalStorage, useFetch) so that linters and developers can easily identify them as hooks.

Identify Reusable Logic

Not every piece of logic needs to be turned into a custom hook. Start by identifying patterns that repeat across multiple components. Common candidates include:

  • Fetching data from an API
  • Managing local storage
  • Handling form inputs and validation
  • Listening to window events (resize, scroll, orientation change)
  • Tracking user activity or session state
  • Managing complex animations or transitions

For example, if you find yourself writing the same useEffect and useState logic to fetch user data in three different components, thats a strong signal that the logic should be abstracted into a custom hook.

Create a Basic Custom Hook

Lets begin with a simple example: a custom hook that manages local storage.

First, create a new file named useLocalStorage.js:

javascript

import { useState } 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;

This hook does three things:

  1. Initializes state using the value stored in localStorage (or a fallback initialValue)
  2. Provides a setter function that updates both state and localStorage
  3. Wraps all operations in try-catch blocks to prevent crashes from malformed data

Now, use it in any component:

javascript

import React from 'react';

import useLocalStorage from './useLocalStorage';

function UserProfile() {

const [name, setName] = useLocalStorage('userName', '');

return (

type="text"

value={name}

onChange={(e) => setName(e.target.value)}

placeholder="Enter your name"

/>

Hello, {name || 'Guest'}

);

}

export default UserProfile;

Notice how the component no longer handles localStorage logic directly. It simply uses the hookclean, readable, and testable.

Add Dependencies and Side Effects

Many custom hooks rely on side effects. For example, lets create a hook that fetches data from an API.

Create useFetch.js:

javascript

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);

setError(null);

try {

const response = await fetch(url);

if (!response.ok) {

throw new Error(HTTP error! status: ${response.status});

}

const result = await response.json();

setData(result);

} catch (err) {

setError(err.message);

} finally {

setLoading(false);

}

};

if (url) {

fetchData();

}

}, [url]); // Re-run effect only when url changes

return { data, loading, error };

}

export default useFetch;

This hook:

  • Tracks loading, data, and error states
  • Uses useEffect to trigger the fetch when the URL changes
  • Includes a guard clause to prevent fetching if URL is falsy
  • Handles network and HTTP errors gracefully

Usage in a component:

javascript

import React from 'react';

import useFetch from './useFetch';

function UserList() {

const { data, loading, error } = useFetch('https://jsonplaceholder.typicode.com/users'); if (loading) return

Loading users...; if (error) return

Error: {error};

return (

    {data.map((user) => (

  • {user.name}
  • ))}

);

}

export default UserList;

This pattern can be reused across any component that needs to fetch datawhether its for products, comments, or settings.

Return Multiple Values and Destructure Smartly

Custom hooks can return arrays or objects. Returning an object is often clearer and more maintainable than returning an array, especially when the number of returned values grows.

For example, a hook that tracks mouse position:

javascript

import { useState, useEffect } from 'react';

function useMousePosition() {

const [position, setPosition] = useState({ x: 0, y: 0 });

useEffect(() => {

const handleMouseMove = (event) => {

setPosition({ x: event.clientX, y: event.clientY });

};

window.addEventListener('mousemove', handleMouseMove);

return () => {

window.removeEventListener('mousemove', handleMouseMove);

};

}, []);

return position;

}

export default useMousePosition;

Usage:

javascript

import useMousePosition from './useMousePosition';

function MouseTracker() {

const { x, y } = useMousePosition();

return (

Mouse position: X: {x}, Y: {y}

);

}

By returning an object, you avoid confusion about the order of returned values and make it easier to add new properties later without breaking existing components.

Accept Parameters and Configure Behavior

Custom hooks can accept arguments to make them configurable. For example, a debounced input hook:

javascript

import { useState, useEffect } from 'react';

function useDebouncedValue(value, delay) {

const [debouncedValue, setDebouncedValue] = useState(value);

useEffect(() => {

const handler = setTimeout(() => {

setDebouncedValue(value);

}, delay);

return () => {

clearTimeout(handler);

};

}, [value, delay]);

return debouncedValue;

}

export default useDebouncedValue;

Use it to delay search queries:

javascript

import React, { useState } from 'react';

import useDebouncedValue from './useDebouncedValue';

function SearchInput() {

const [input, setInput] = useState('');

const debouncedInput = useDebouncedValue(input, 500);

// This effect runs only when the debounced value changes

useEffect(() => {

console.log('Searching for:', debouncedInput);

// Call API or filter data here

}, [debouncedInput]);

return (

type="text"

value={input}

onChange={(e) => setInput(e.target.value)}

placeholder="Search..."

/>

);

}

By accepting a delay parameter, the hook becomes flexible enough to be used in different contextssearch bars, form validation, real-time previewswithout duplicating code.

Combine Multiple Hooks

One of the greatest strengths of custom hooks is their ability to compose other hooks. For example, create a hook that combines local storage and debounced values:

javascript

import { useState } from 'react';

import useDebouncedValue from './useDebouncedValue';

import useLocalStorage from './useLocalStorage';

function useDebouncedLocalStorage(key, initialValue, delay = 500) {

const [storedValue, setStoredValue] = useLocalStorage(key, initialValue);

const debouncedValue = useDebouncedValue(storedValue, delay);

const setValue = (value) => {

setStoredValue(value);

};

return [debouncedValue, setValue];

}

export default useDebouncedLocalStorage;

This hook returns the debounced version of the stored value (useful for performance) while still allowing immediate writes to localStorage. Components using this hook get the benefit of both features without needing to manage them separately.

Test Your Custom Hook

Testing custom hooks is essential for reliability. Use libraries like @testing-library/react-hooks to test hook behavior in isolation.

Install the library:

npm install @testing-library/react-hooks

Write a test for useLocalStorage:

javascript

import { renderHook, act } from '@testing-library/react-hooks';

import useLocalStorage from './useLocalStorage';

describe('useLocalStorage', () => {

beforeEach(() => {

localStorage.clear();

});

test('returns default value if no item exists', () => {

const { result } = renderHook(() => useLocalStorage('testKey', 'default'));

expect(result.current[0]).toBe('default');

});

test('persists value to localStorage', () => {

const { result } = renderHook(() => useLocalStorage('testKey', ''));

act(() => {

result.current[1]('Hello World');

});

expect(localStorage.getItem('testKey')).toBe('\"Hello World\"');

});

test('reads value from localStorage on re-render', () => {

localStorage.setItem('testKey', '\"Existing Value\"');

const { result } = renderHook(() => useLocalStorage('testKey', 'default'));

expect(result.current[0]).toBe('Existing Value');

});

});

Testing ensures your hooks behave correctly under different conditions and prevents regressions as your codebase evolves.

Best Practices

Name Hooks with the use Prefix

Always begin your custom hook names with use. This is not just a conventionits a requirement for Reacts ESLint plugin to correctly identify and validate hooks. Tools like React DevTools and IDEs also rely on this naming pattern to provide accurate debugging information and autocomplete suggestions.

Keep Hooks Focused and Single-Purpose

A custom hook should do one thing well. Avoid creating mega hooks that handle form validation, API calls, and local storage all at once. Instead, break logic into smaller, composable hooks. For example:

  • useForm handles input state and validation
  • useApi manages API requests and responses
  • useLocalStorage persists data to localStorage

Then combine them as needed:

javascript

function useUserForm() {

const [form, setForm] = useForm({ name: '', email: '' });

const { data: user, loading, error } = useApi(/api/users/${form.id});

const [saved, setSaved] = useLocalStorage('userForm', form);

return { form, setForm, user, loading, error, saved, setSaved };

}

This approach improves reusability and makes debugging easier.

Handle Edge Cases Gracefully

Always anticipate failure points:

  • What if the API is unreachable?
  • What if localStorage is full or disabled?
  • What if the URL is undefined or null?

Use defensive programming: validate inputs, wrap operations in try-catch, provide fallbacks, and log errors without crashing the app. For example:

javascript

const [data, setData] = useState(null);

useEffect(() => {

if (!url) return;

const controller = new AbortController();

fetch(url, { signal: controller.signal })

.then(res => res.json())

.then(setData)

.catch(err => {

if (err.name !== 'AbortError') {

setError('Failed to fetch data');

}

});

return () => controller.abort();

}, [url]);

This prevents memory leaks and ensures clean cleanup.

Use Dependency Arrays Correctly

When using useEffect or useCallback inside a custom hook, ensure your dependency arrays are accurate. Missing dependencies can cause stale closures; including unnecessary ones can cause excessive re-renders.

Use the react-hooks/exhaustive-deps ESLint rule to catch mistakes. If you intentionally want to skip a dependency (e.g., a function that wont change), document why:

javascript

useEffect(() => {

const handler = () => {

console.log('Event triggered');

};

window.addEventListener('click', handler);

return () => window.removeEventListener('click', handler);

}, []); // Intentionally empty because handler is static

Avoid Side Effects in Render

Never perform side effects (like API calls, DOM manipulations, or timers) directly in the render phase. Always use useEffect or useLayoutEffect for these. Custom hooks should encapsulate these effects so components remain pure.

Document Your Hooks

Just like public APIs, custom hooks should be documented. Use JSDoc comments to explain parameters, return values, and usage examples:

javascript

/**

* Manages a value in localStorage with optional debouncing.

* @param {string} key - The localStorage key to use.

* @param {*} initialValue - The default value if no item exists.

* @param {number} delay - Debounce delay in milliseconds.

* @returns {[*, Function]} - The debounced value and a setter function.

*/

function useDebouncedLocalStorage(key, initialValue, delay = 500) {

// ...

}

This helps other developers understand how to use your hook correctly.

Export and Organize Hooks Logically

Structure your project with a dedicated hooks/ directory:

src/

??? hooks/

? ??? useLocalStorage.js

? ??? useFetch.js

? ??? useDebouncedValue.js

? ??? index.js

??? components/

??? utils/

Export all hooks from an index.js file for easy imports:

javascript

// hooks/index.js

export { default as useLocalStorage } from './useLocalStorage';

export { default as useFetch } from './useFetch';

export { default as useDebouncedValue } from './useDebouncedValue';

Then import them cleanly:

javascript

import { useFetch, useLocalStorage } from '@/hooks';

Tools and Resources

React DevTools

React DevTools is indispensable for debugging hooks. It displays all hooks used in a component, their current values, and the component tree. Install the browser extension for Chrome or Firefox and inspect your custom hooks in real time.

ESLint with react-hooks Plugin

Enable the eslint-plugin-react-hooks plugin to enforce the rules of hooks. It will warn you if you call hooks conditionally or inside loops.

Install it:

npm install eslint-plugin-react-hooks --save-dev

Add to your ESLint config:

json

{

"plugins": ["react-hooks"],

"rules": {

"react-hooks/rules-of-hooks": "error",

"react-hooks/exhaustive-deps": "warn"

}

}

Testing Libraries

  • @testing-library/react-hooks For testing hooks in isolation
  • Jest For mocking dependencies like fetch or localStorage

Example of mocking localStorage in tests:

javascript

const localStorageMock = (() => {

let store = {};

return {

getItem(key) {

return store[key] || null;

},

setItem(key, value) {

store[key] = value.toString();

},

clear() {

store = {};

},

removeItem(key) {

delete store[key];

}

};

})();

Object.defineProperty(window, 'localStorage', {

value: localStorageMock

});

Code Snippets and Templates

Create custom VS Code or WebStorm snippets for common hook patterns. For example:

json

{

"Use Local Storage Hook": {

"prefix": "usels",

"body": [

"import { useState } from 'react';",

"",

"function useLocalStorage($1, $2) {",

" const [storedValue, setStoredValue] = useState(() => {",

" try {",

" const item = window.localStorage.getItem($1);",

" return item ? JSON.parse(item) : $2;",

" } catch (error) {",

" console.error(error);",

" return $2;",

" }",

" });",

"",

" const setValue = (value) => {",

" try {",

" const valueToStore = value instanceof Function ? value(storedValue) : value;",

" setStoredValue(valueToStore);",

" window.localStorage.setItem($1, JSON.stringify(valueToStore));",

" } catch (error) {",

" console.error(error);",

" }",

" };",

"",

" return [storedValue, setValue];",

"}",

"",

"export default useLocalStorage;"

],

"description": "Generate a useLocalStorage custom hook"

}

}

Open Source Libraries for Inspiration

Study well-maintained open-source hook libraries:

These libraries demonstrate how to handle edge cases, support TypeScript, write tests, and structure code for scalability.

Real Examples

Example 1: useMediaQuery Responsive UI Logic

Many components need to adapt to screen size. Instead of repeating window.matchMedia logic, create a reusable hook:

javascript

import { useState, useEffect } from 'react';

function useMediaQuery(query) {

const [matches, setMatches] = useState(false);

useEffect(() => {

const media = window.matchMedia(query);

if (media.matches !== matches) {

setMatches(media.matches);

}

const listener = () => setMatches(media.matches);

media.addEventListener('change', listener);

return () => media.removeEventListener('change', listener);

}, [matches, query]);

return matches;

}

export default useMediaQuery;

Use it to conditionally render mobile layouts:

javascript

import useMediaQuery from './useMediaQuery';

function Header() {

const isMobile = useMediaQuery('(max-width: 768px)');

return (

{isMobile ? : }

);

}

Example 2: useClickOutside Detecting Clicks Outside a Component

Common in modals, dropdowns, and tooltips:

javascript

import { useRef, useEffect } from 'react';

function useClickOutside(ref, handler) {

useEffect(() => {

const listener = (event) => {

if (!ref.current || ref.current.contains(event.target)) {

return;

}

handler(event);

};

document.addEventListener('mousedown', listener);

document.addEventListener('touchstart', listener);

return () => {

document.removeEventListener('mousedown', listener);

document.removeEventListener('touchstart', listener);

};

}, [ref, handler]);

}

export default useClickOutside;

Use it in a modal:

javascript

import React, { useRef } from 'react';

import useClickOutside from './useClickOutside';

function Modal({ onClose }) {

const ref = useRef();

useClickOutside(ref, onClose);

return (

Modal Content

);

}

Example 3: useScrollPosition Track Scroll Position

Useful for animations, sticky headers, or progress indicators:

javascript

import { useState, useEffect } from 'react';

function useScrollPosition() {

const [scrollPosition, setScrollPosition] = useState(0);

useEffect(() => {

const handleScroll = () => {

const position = window.pageYOffset;

setScrollPosition(position);

};

window.addEventListener('scroll', handleScroll);

return () => {

window.removeEventListener('scroll', handleScroll);

};

}, []);

return scrollPosition;

}

export default useScrollPosition;

Use it to fade in a back-to-top button:

javascript

import useScrollPosition from './useScrollPosition';

function BackToTop() {

const scrollPosition = useScrollPosition();

if (scrollPosition

return (

);

}

Example 4: useAsync Reusable Async State Management

Enhance the useFetch hook into a generic async state handler:

javascript

import { useState, useEffect } from 'react';

function useAsync(asyncFunction, immediate = true) {

const [status, setStatus] = useState('idle');

const [value, setValue] = useState(null);

const [error, setError] = useState(null);

const execute = async (...args) => {

setStatus('pending');

setValue(null);

setError(null);

try {

const result = await asyncFunction(...args);

setValue(result);

setStatus('success');

} catch (err) {

setError(err);

setStatus('error');

}

};

useEffect(() => {

if (immediate) {

execute();

}

}, [immediate]);

return { execute, status, value, error };

}

export default useAsync;

Use it for any async operation:

javascript

function Login() {

const { execute, status, value, error } = useAsync(

(email, password) => fetch('/api/login', { method: 'POST', body: JSON.stringify({ email, password }) }).then(res => res.json()),

false

);

const handleSubmit = async (e) => {

e.preventDefault();

const formData = new FormData(e.target);

await execute(formData.get('email'), formData.get('password'));

};

return (

{status === 'pending' ? 'Logging in...' : 'Login'}

{error &&

{error.message}} {value &&

Login successful!}

);

}

This hook is now reusable for any form submission, API call, or async operationmaking it a cornerstone of your applications state management.

FAQs

Can I use custom hooks in class components?

No. Custom hooks are designed to work only within functional components or other custom hooks. If you need to reuse logic in a class component, consider converting the component to a function component or extract the logic into a standalone utility function.

Do custom hooks affect performance?

Properly written custom hooks do not harm performance. In fact, they can improve it by reducing component bloat and avoiding redundant logic. However, avoid creating unnecessary re-renders by memoizing values with useMemo or useCallback when appropriate.

Can I use multiple custom hooks in one component?

Yes. In fact, its encouraged. A single component can use several hooks to manage different concernsform state, data fetching, event listeners, etc. This promotes separation of concerns and keeps components focused.

Should I write custom hooks for every piece of logic?

No. Only extract logic into hooks when its reused across multiple components or becomes complex enough to warrant isolation. Simple, one-off logic should remain in the component for clarity.

Are custom hooks compatible with TypeScript?

Yes. Custom hooks work seamlessly with TypeScript. Always type your hooks parameters and return values for better IntelliSense and compile-time safety:

typescript

interface FetchResult {

data: T | null;

loading: boolean;

error: string | null;

}

function useFetch(url: string): FetchResult {

// ...

}

How do I test a hook that uses context?

Wrap your hook in a test renderer that provides the required context. For example:

javascript

import { renderHook, act } from '@testing-library/react-hooks';

import { MyContext } from './MyContext';

test('uses context value', () => {

const wrapper = ({ children }) => (

{children}

);

const { result } = renderHook(() => useMyContextHook(), { wrapper });

expect(result.current.user).toBe('test');

});

Can custom hooks cause memory leaks?

Potentially, if you forget to clean up event listeners, timers, or subscriptions. Always return a cleanup function from useEffect when necessary. Use tools like React DevTools to monitor mounted components and detect lingering hooks.

Whats the difference between a custom hook and a utility function?

A utility function is a plain JavaScript function that doesnt use React hooks. A custom hook is a JavaScript function that calls one or more React hooks internally. Only custom hooks can manage state, side effects, or lifecycle behavior within Reacts rendering model.

Conclusion

Custom hooks are not just a conveniencethey are a fundamental shift in how we structure and share logic in React applications. By encapsulating stateful behavior into reusable, testable, and composable units, custom hooks enable developers to build scalable, maintainable, and performant applications with greater clarity and confidence.

In this guide, youve learned how to identify patterns worth abstracting, implement hooks following Reacts rules, handle edge cases gracefully, test thoroughly, and organize your code for long-term success. Youve seen real-world examples that demonstrate the power and flexibility of custom hooksfrom managing local storage to detecting clicks outside a component to orchestrating complex async flows.

As you continue building React applications, make custom hooks your default tool for logic reuse. Start small, iterate often, and prioritize simplicity over complexity. The more you use them, the more natural theyll becomeand the more your codebase will reflect clean, intentional architecture.

Remember: a great custom hook doesnt just save lines of codeit elevates the entire applications structure, making it easier for teams to collaborate, debug, and extend. Now that you know how to create them, go build something powerful.