Skip to main content

Custom Hooks in React: Reusable Stateful Logic

Custom hooks are JavaScript functions that start with use and can call other hooks to extract and share stateful logic between React components. They solve the problem of code reuse by eliminating wrapper hell (deeply nested render props and higher-order components) and enabling clean, composable logic separation. Custom hooks follow two simple rules: call hooks only at the top level and only from React functions or other custom hooks.

Key Takeaways

  • Custom hooks are functions starting with use that encapsulate reusable stateful logic
  • They eliminate wrapper hell caused by render props and higher-order components (HOCs)
  • The two rules of hooks: call them at the top level (not in loops/conditions) and only from React functions
  • Custom hooks can combine multiple built-in hooks (useState, useEffect, etc.) to create powerful abstractions
  • A single custom hook can be used in multiple components, promoting code reuse and maintainability
  • Custom hooks improve code readability by naming logic descriptively (e.g., useFetch, useLocalStorage)

Prerequisites

Before we begin, ensure you have a solid grasp of the following concepts:

  • React Hooks: Deep familiarity with useState and useEffect
  • Functional Components: Comfort defining and working with function-based components
  • JavaScript Functions: Understanding function parameters, return values, and destructuring
  • Async/Await: Basic knowledge of asynchronous operations and promises
  • Component State: Understanding how state triggers re-renders in React

The Problem Custom Hooks Solve

Before hooks were introduced in React 16.8, sharing stateful logic between components was challenging. The common patterns were render props and higher-order components (HOCs), which often led to complex, deeply nested component trees—a problem nicknamed "wrapper hell."

Consider a simple scenario: multiple components need to fetch data from an API. Without custom hooks, you'd repeat the fetching logic in each component, or use an HOC that wraps your component and passes the fetched data as props. This creates a tangled component hierarchy that is hard to understand and debug.

Custom hooks solve this problem by allowing you to extract component logic into reusable functions. You write the logic once in a hook, then use it in as many components as you need. The hook handles state management, side effects, and logic; your component handles only the UI.


The Two Rules of Hooks

When creating and using custom hooks, there are two essential rules you must follow:

Rule 1: Only Call Hooks at the Top Level

Never call hooks inside loops, conditions, or nested functions. Hooks must always be called at the top level of your component or custom hook. This ensures that React can properly track which state belongs to which component instance.

// Correct: Hook at top level
function MyComponent() {
const [count, setCount] = useState(0);
// ... rest of component
}

// Incorrect: Hook inside a condition
function MyComponent() {
if (someCondition) {
const [count, setCount] = useState(0); // Error!
}
}

// Incorrect: Hook inside a loop
function MyComponent() {
for (let i = 0; i < 10; i++) {
const [count, setCount] = useState(0); // Error!
}
}

Rule 2: Only Call Hooks from React Functions

Call hooks only from React functional components or other custom hooks, not from regular JavaScript functions. This ensures that stateful logic is properly tracked and updated.

// Correct: Hook in a React functional component
function MyComponent() {
const [count, setCount] = useState(0);
return <div>{count}</div>;
}

// Correct: Hook in a custom hook
function useCustom() {
const [count, setCount] = useState(0);
return count;
}

// Incorrect: Hook in a regular function
function regularFunction() {
const [count, setCount] = useState(0); // Error!
}

Creating Your First Custom Hook: useFetch

Let's create a practical custom hook to handle API data fetching. This is one of the most commonly used custom hooks because fetching logic is repeated across many applications.

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 () => {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const json = await response.json();
setData(json);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
};

fetchData();
}, [url]);

return { data, loading, error };
}

export default useFetch;

How it works:

  1. useState for state: The hook manages three pieces of state: data (API response), loading (fetch in progress), and error (any errors).
  2. useEffect for side effect: The hook runs the fetch operation when the component mounts or the url changes.
  3. Error handling: A try-catch block handles network errors and HTTP errors.
  4. Return object: The hook returns an object containing all state values that components can destructure.

Using Your Custom Hook

Once created, a custom hook is used like any built-in hook. Import it and call it in your functional component:

import useFetch from './useFetch';

function UserProfile({ userId }) {
const { data, loading, error } = useFetch(`https://api.example.com/users/${userId}`);

if (loading) {
return <div>Loading user data...</div>;
}

if (error) {
return <div>Error loading user: {error.message}</div>;
}

return (
<div>
<h1>{data.name}</h1>
<p>Email: {data.email}</p>
<p>Location: {data.location}</p>
</div>
);
}

Your component is now clean and focused entirely on rendering. All the complexity of fetching, loading states, and error handling is encapsulated in the useFetch hook. You can use this hook in any other component that needs to fetch data.


Composing Multiple Hooks in a Custom Hook

Custom hooks can call other hooks (built-in or custom) to create powerful abstractions. Here's an example combining useState, useEffect, and the useFetch hook:

import useFetch from './useFetch';
import { useState, useEffect } from 'react';

function useFilteredData(url, filterKey) {
const { data, loading, error } = useFetch(url);
const [filtered, setFiltered] = useState([]);

useEffect(() => {
if (data && Array.isArray(data)) {
const result = data.filter(item => item[filterKey] !== null);
setFiltered(result);
}
}, [data, filterKey]);

return { filtered, loading, error };
}

export default useFilteredData;

This hook combines useFetch with additional filtering logic, creating a higher-level abstraction that encapsulates both data fetching and filtering.


Best Practices for Custom Hooks

Name hooks descriptively: Use the use prefix and choose names that describe what the hook does (useLocalStorage, useFetch, useWindowSize).

Return values clearly: Return an object or array with descriptive names so consumers know what they're getting.

Handle cleanup: If your hook subscribes to events, databases, or APIs, clean up those subscriptions in a useEffect cleanup function.

Keep hooks focused: Each hook should have one clear purpose. Avoid creating "Swiss Army knife" hooks that do too much.

Document parameters: Add JSDoc comments explaining what parameters the hook expects and what it returns.


Frequently Asked Questions

Can I use custom hooks in class components?

No. Custom hooks only work with functional components. If you're using class components, you'll need to use render props or higher-order components for logic sharing, or refactor to functional components.

How do I debug a custom hook?

Use React DevTools, which displays custom hooks by name in the Components tab. You can also add console.log statements to trace hook execution and state updates.

Can a custom hook call another custom hook?

Yes, absolutely. Custom hooks can call built-in hooks and other custom hooks, enabling powerful composition and reuse patterns.

What's the difference between a custom hook and a component?

A custom hook is a function that uses hooks internally and returns values. A component is a function that returns JSX. Both can call hooks, but only components produce UI output.

Do I need to export custom hooks from a file?

It's a good practice to export each custom hook from its own file, but you can also define multiple hooks in one file and export them. The important thing is that each hook is accessible where it's needed.

Can I pass a custom hook to another function?

No. Hooks are not values—they're functions that must be called directly. You can't pass a hook as a prop; instead, call the hook in a component and pass the returned values.


Further Reading