React Custom Hooks

React Custom Hooks

Custom hooks are a powerful feature in React that lets you extract component logic into reusable functions. They follow the same rules as built-in hooks and give you a way to share stateful behavior between components without changing your component hierarchy.

If you’re new to hooks, you might want to first learn about the useState hook and useEffect hook.

What Makes a Custom Hook?

A custom hook is just a JavaScript function whose name starts with “use” and that can call other hooks. That’s it! The “use” convention is important because it lets React automatically check for violations of the rules of hooks.

// This is a custom hook
function useMyCustomLogic() {
  const [value, setValue] = useState(0);
  
  // Your custom logic here
  
  return value;
}

Creating Your First Custom Hook

Let’s create a simple custom hook that manages a counter. This demonstrates how to extract state logic into a reusable function.

// useCounter.js
import { useState } from 'react';

function useCounter(initialValue = 0) {
  const [count, setCount] = useState(initialValue);
  
  const increment = () => setCount(count + 1);
  const decrement = () => setCount(count - 1);
  const reset = () => setCount(initialValue);
  
  return {
    count,
    increment,
    decrement,
    reset
  };
}

export default useCounter;

Now you can use this hook in any component:

// CounterComponent.jsx
import useCounter from './useCounter';

function CounterComponent() {
  const { count, increment, decrement, reset } = useCounter(10);
  
  return (
    <div>
      <h2>Count: {count}</h2>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
      <button onClick={reset}>Reset</button>
    </div>
  );
}

Real-World Example: API Data Fetching

One of the most common uses for custom hooks is fetching data from APIs. Here’s a comprehensive data fetching hook:

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

function useApi(url, options = {}) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    let isMounted = true;
    
    const fetchData = async () => {
      try {
        setLoading(true);
        setError(null);
        
        const response = await fetch(url, options);
        
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        
        const result = await response.json();
        
        if (isMounted) {
          setData(result);
        }
      } catch (err) {
        if (isMounted) {
          setError(err.message);
        }
      } finally {
        if (isMounted) {
          setLoading(false);
        }
      }
    };
    
    if (url) {
      fetchData();
    }
    
    return () => {
      isMounted = false;
    };
  }, [url]);
  
  return { data, loading, error };
}

export default useApi;

Using this hook in a component:

// UserComponent.jsx
import useApi from './useApi';

function UserComponent({ userId }) {
  const { data: user, loading, error } = useApi(
    userId ? `https://jsonplaceholder.typicode.com/users/${userId}` : null
  );
  
  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  if (!user) return <div>Select a user</div>;
  
  return (
    <div>
      <h2>{user.name}</h2>
      <p>Email: {user.email}</p>
      <p>Phone: {user.phone}</p>
    </div>
  );
}

Custom Hook for Local Storage

Here’s a hook that synchronizes state with localStorage:

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

function useLocalStorage(key, initialValue) {
  // Get from local storage then parse stored json or return initialValue
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.error(`Error reading localStorage key "${key}":`, 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 same API as useState
      const valueToStore = value instanceof Function ? value(storedValue) : value;
      setStoredValue(valueToStore);
      window.localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      console.error(`Error setting localStorage key "${key}":`, error);
    }
  };
  
  return [storedValue, setValue];
}

export default useLocalStorage;

Using the localStorage hook:

// SettingsComponent.jsx
import useLocalStorage from './useLocalStorage';

function SettingsComponent() {
  const [theme, setTheme] = useLocalStorage('theme', 'light');
  const [fontSize, setFontSize] = useLocalStorage('fontSize', 'medium');
  
  return (
    <div>
      <h2>Settings</h2>
      <div>
        <label>
          Theme:
          <select value={theme} onChange={(e) => setTheme(e.target.value)}>
            <option value="light">Light</option>
            <option value="dark">Dark</option>
          </select>
        </label>
      </div>
      <div>
        <label>
          Font Size:
          <select value={fontSize} onChange={(e) => setFontSize(e.target.value)}>
            <option value="small">Small</option>
            <option value="medium">Medium</option>
            <option value="large">Large</option>
          </select>
        </label>
      </div>
      <p>Current theme: {theme}</p>
      <p>Current font size: {fontSize}</p>
    </div>
  );
}

Debounced Input Hook

This hook is useful for search inputs where you want to wait until the user stops typing:

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

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;

Combining with the API hook for a search component:

// SearchComponent.jsx
import { useState } from 'react';
import useApi from './useApi';
import useDebounce from './useDebounce';

function SearchComponent() {
  const [searchTerm, setSearchTerm] = useState('');
  const debouncedSearchTerm = useDebounce(searchTerm, 500);
  
  const { data: results, loading, error } = useApi(
    debouncedSearchTerm 
      ? `https://jsonplaceholder.typicode.com/users?q=${debouncedSearchTerm}`
      : null
  );
  
  return (
    <div>
      <h2>User Search</h2>
      <input
        type="text"
        placeholder="Search users..."
        value={searchTerm}
        onChange={(e) => setSearchTerm(e.target.value)}
      />
      {loading && <div>Searching...</div>}
      {error && <div>Error: {error}</div>}
      {results && (
        <div>
          <h3>Results:</h3>
          <ul>
            {results.map(user => (
              <li key={user.id}>{user.name} - {user.email}</li>
            ))}
          </ul>
        </div>
      )}
    </div>
  );
}

Window Size Hook

This hook tracks the browser window size:

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

function useWindowSize() {
  const [windowSize, setWindowSize] = useState({
    width: undefined,
    height: undefined,
  });
  
  useEffect(() => {
    function handleResize() {
      setWindowSize({
        width: window.innerWidth,
        height: window.innerHeight,
      });
    }
    
    // Add event listener
    window.addEventListener('resize', handleResize);
    
    // Call handler right away so state gets updated with initial window size
    handleResize();
    
    // Remove event listener on cleanup
    return () => window.removeEventListener('resize', handleResize);
  }, []);
  
  return windowSize;
}

export default useWindowSize;

Using it for responsive design:

// ResponsiveComponent.jsx
import useWindowSize from './useWindowSize';

function ResponsiveComponent() {
  const { width } = useWindowSize();
  
  return (
    <div>
      <h2>Responsive Design</h2>
      {width < 768 ? (
        <div>Mobile layout (width: {width}px)</div>
      ) : width < 1024 ? (
        <div>Tablet layout (width: {width}px)</div>
      ) : (
        <div>Desktop layout (width: {width}px)</div>
      )}
    </div>
  );
}

Best Practices for Custom Hooks

  1. Follow naming conventions: Always start with “use”
  2. Keep them focused: Each hook should do one thing well
  3. Make them reusable: Design hooks to work in different contexts
  4. Handle edge cases: Consider loading states, errors, and cleanup
  5. Document your hooks: Explain what they do and how to use them
  6. Test your hooks: Write tests to ensure they work correctly

Testing Custom Hooks

You can test custom hooks using React Testing Library’s renderHook function:

// useCounter.test.js
import { renderHook, act } from '@testing-library/react';
import useCounter from './useCounter';

test('should increment counter', () => {
  const { result } = renderHook(() => useCounter());
  
  act(() => {
    result.current.increment();
  });
  
  expect(result.current.count).toBe(1);
});

test('should reset to initial value', () => {
  const { result } = renderHook(() => useCounter(5));
  
  act(() => {
    result.current.increment();
    result.current.reset();
  });
  
  expect(result.current.count).toBe(5);
});

When to Create Custom Hooks

Create custom hooks when you:

  • Find yourself copying and pasting the same stateful logic between components
  • Want to share stateful logic without changing component structure
  • Need to abstract complex behavior into a simple interface
  • Want to make your components more readable and focused

Custom hooks are a fundamental part of modern React development. They help you write cleaner, more reusable code and make your components easier to understand and maintain.

For more advanced React patterns, check out our guide on React Context API or learn about performance optimization.

The React documentation provides excellent examples and guidelines for building custom hooks, and the community has created many useful hooks you can explore on GitHub.

Last updated on