Advanced30 min read

Custom Hooks

Learn how to extract reusable stateful logic into custom hooks, follow naming conventions, and build real-world hooks like useLocalStorage, useToggle, and useDebounce.

Why Custom Hooks?

As your React application grows, you will notice that different components often share the same stateful logic. For example, multiple components might need to track whether a modal is open, subscribe to window resize events, or debounce user input.

Before hooks, the only ways to share stateful logic were Higher-Order Components (HOCs) and Render Props — both of which led to deeply nested component trees ("wrapper hell") and made code harder to follow.

Custom hooks solve this problem elegantly. A custom hook is simply a JavaScript function whose name starts with use and that calls other hooks inside it. You extract the shared logic into the custom hook, and any component that needs it can call it directly — no wrappers, no nesting.

jsx
// Before: duplicated logic in two components
function ComponentA() {
  const [isOpen, setIsOpen] = React.useState(false);
  const toggle = () => setIsOpen(prev => !prev);
  // ...
}

function ComponentB() {
  const [isOpen, setIsOpen] = React.useState(false);
  const toggle = () => setIsOpen(prev => !prev);
  // ...
}

// After: shared logic in a custom hook
function useToggle(initial = false) {
  const [value, setValue] = React.useState(initial);
  const toggle = () => setValue(prev => !prev);
  return [value, toggle];
}

function ComponentA() {
  const [isOpen, toggle] = useToggle();
  // ...
}

Each component that calls useToggle gets its own independent copy of the state. Custom hooks share logic, not state.

The Naming Convention and Rules

Custom hooks must follow a strict naming convention and the same rules as built-in hooks.

The use prefix

Every custom hook must start with the word use, followed by a capital letter. This is not just a convention — React's linter relies on it to enforce the rules of hooks.

javascript
// Good — recognized as a hook
function useCounter() { ... }
function useLocalStorage(key) { ... }
function useWindowSize() { ... }

// Bad — NOT recognized as a hook
function getCounter() { ... }   // linter won't check hook rules
function counter() { ... }      // same problem

Rules of Hooks (recap)

Custom hooks must follow the same two rules as all hooks:

  1. Only call hooks at the top level. Do not call hooks inside loops, conditions, or nested functions. React relies on the order of hook calls being the same on every render.
jsx
// WRONG — conditional hook call
function useBadHook(condition) {
  if (condition) {
    const [value, setValue] = React.useState(0); // breaks!
  }
}

// RIGHT — always call the hook, use the condition for behavior
function useGoodHook(condition) {
  const [value, setValue] = React.useState(0);
  // use condition in the return or in an effect
}
  1. Only call hooks from React function components or other custom hooks. Do not call hooks from regular JavaScript functions, class components, or event handlers outside of components.

Hooks calling hooks

Custom hooks can call other custom hooks. This is one of the most powerful patterns — you can compose hooks together:

jsx
function useDebounce(value, delay) {
  const [debounced, setDebounced] = React.useState(value);
  React.useEffect(() => {
    const timer = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);
  return debounced;
}

function useDebouncedSearch(query) {
  const debouncedQuery = useDebounce(query, 300);
  const [results, setResults] = React.useState([]);
  // ... fetch based on debouncedQuery
  return results;
}

Building Real-World Custom Hooks

Let us build three practical custom hooks that you will use regularly in real projects.

useLocalStorage

Persist state to localStorage so it survives page refreshes:

jsx
function useLocalStorage(key, initialValue) {
  const [value, setValue] = React.useState(() => {
    try {
      const stored = localStorage.getItem(key);
      return stored !== null ? JSON.parse(stored) : initialValue;
    } catch {
      return initialValue;
    }
  });

  React.useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);

  return [value, setValue];
}

// Usage
function App() {
  const [theme, setTheme] = useLocalStorage('theme', 'light');
  // theme persists across page refreshes!
}

Note the lazy initializer passed to useState — the arrow function () => { ... } runs only once on the first render, avoiding a localStorage.getItem call on every render.

useToggle

A simple but extremely common hook for boolean state:

jsx
function useToggle(initial = false) {
  const [value, setValue] = React.useState(initial);
  const toggle = React.useCallback(() => {
    setValue(prev => !prev);
  }, []);
  return [value, toggle];
}

// Usage
function Modal() {
  const [isOpen, toggleOpen] = useToggle();
  return (
    <>
      <button onClick={toggleOpen}>
        {isOpen ? 'Close' : 'Open'}
      </button>
      {isOpen && <div className="modal">Modal content</div>}
    </>
  );
}

useDebounce

Delay updating a value until the user stops changing it. Essential for search inputs:

jsx
function useDebounce(value, delay = 300) {
  const [debounced, setDebounced] = React.useState(value);

  React.useEffect(() => {
    const timer = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);

  return debounced;
}

// Usage — only searches after user stops typing for 300ms
function SearchBar() {
  const [query, setQuery] = React.useState('');
  const debouncedQuery = useDebounce(query, 300);

  React.useEffect(() => {
    if (debouncedQuery) {
      // fetch search results
    }
  }, [debouncedQuery]);

  return <input value={query} onChange={e => setQuery(e.target.value)} />;
}

What to Return from a Custom Hook

Custom hooks can return anything: a single value, an array, or an object. The convention depends on the use case.

Return an array — when there are 1-2 values

Following the useState pattern, return an array when callers will likely want to rename the values:

jsx
function useToggle(initial) {
  // ...
  return [value, toggle]; // array
}

// Caller can name them anything:
const [isOpen, toggleOpen] = useToggle(false);
const [isDark, toggleDark] = useToggle(true);

Return an object — when there are 3+ values

When you return many related values, use an object so callers can destructure only what they need:

jsx
function useCounter(initial = 0) {
  const [count, setCount] = React.useState(initial);
  const increment = () => setCount(c => c + 1);
  const decrement = () => setCount(c => c - 1);
  const reset = () => setCount(initial);

  return { count, increment, decrement, reset }; // object
}

// Caller destructures by name:
const { count, increment, reset } = useCounter(10);

Return a single value

When the hook computes a single derived value:

jsx
function useDebounce(value, delay) {
  // ...
  return debounced; // single value
}

const debouncedSearch = useDebounce(searchTerm, 500);

Choose the return shape that makes the hook most ergonomic for its callers.

Custom Hooks in Action

html
<div id="root"></div>
<script src="https://unpkg.com/react@19/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@19/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
  // Custom hook: useCounter
  function useCounter(initial = 0) {
    const [count, setCount] = React.useState(initial);
    const increment = () => setCount(c => c + 1);
    const decrement = () => setCount(c => c - 1);
    const reset = () => setCount(initial);
    return { count, increment, decrement, reset };
  }

  // Custom hook: useToggle
  function useToggle(initial = false) {
    const [value, setValue] = React.useState(initial);
    const toggle = () => setValue(prev => !prev);
    return [value, toggle];
  }

  function App() {
    const { count, increment, decrement, reset } = useCounter(0);
    const [isVisible, toggleVisible] = useToggle(true);

    return (
      <div>
        <h2>useCounter Demo</h2>
        <p>Count: {count}</p>
        <button onClick={increment}>+1</button>
        <button onClick={decrement}>-1</button>
        <button onClick={reset}>Reset</button>

        <h2>useToggle Demo</h2>
        <button onClick={toggleVisible}>
          {isVisible ? "Hide" : "Show"} Content
        </button>
        {isVisible && <p>This content is toggled!</p>}
      </div>
    );
  }

  ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>

What is the primary naming requirement for a custom hook?

Ready to practice?

Create your free account to access the interactive code editor, run challenges, and track your progress.