React Custom Hooks: Reuse Logic Between Components

React.js Full Course: Beginner to Advanced · Hooks and Data · 10 min read · Updated

One hook, many components

  1. You wrote the same state and effect in two places, so you move it into a custom hook.
  2. A custom hook is a function whose name starts with use and calls other hooks.
  3. The search box uses the hook and gets its own separate state.
  4. The user filter uses the same hook and gets its own separate state. Hooks share logic, not data.
  5. If you improve the hook, every component gets the improvement.

You already use hooks that React gives you: useState, useEffect and others. React also lets you make your own hooks. A custom hook is a normal JavaScript function that uses other hooks and lets you reuse logic in many components. Components share their screens, and custom hooks share their behaviour.

What is a custom hook?

A custom hook is a function whose name starts with use, and which calls other hooks inside. That is all. The name matters: it tells React, and the lint rules, that the function follows the rules of hooks. Each component that calls a custom hook gets its own separate copy of the state inside it. Hooks share the logic, not the data.

Example 1: useToggle

Many components have a true or false switch: a menu, a modal, a password field. Instead of writing the same state and handler each time, write a hook once:

import { useState } from 'react';

export function useToggle(initial = false) {
  const [on, setOn] = useState(initial);
  const toggle = () => setOn((value) => !value);
  return [on, toggle];
}
function Menu() {
  const [open, toggleOpen] = useToggle(false);
  return (
    <div>
      <button onClick={toggleOpen}>{open ? 'Close' : 'Open'}</button>
      {open && <ul><li>Home</li><li>About</li></ul>}
    </div>
  );
}

The hook returns an array, just like useState. You can return an object instead when there are many values. Choose what is easier to read for the person using the hook.

Example 2: useLocalStorage

Saving a value in the browser, so that it is still there after a reload, needs a little code that you will want in many places. Put it in a hook:

import { useState, useEffect } from 'react';

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

  useEffect(() => {
    try {
      localStorage.setItem(key, JSON.stringify(value));
    } catch {
      /* storage may be full or blocked: ignore */
    }
  }, [key, value]);

  return [value, setValue];
}
function Settings() {
  const [name, setName] = useLocalStorage('username', '');
  return <input value={name} onChange={(e) => setName(e.target.value)} placeholder="Your name" />;
}

Two details to notice. First, useState(() => ...) takes a function, called lazy initial state. React runs it only on the first draw, so we read storage once, not on every draw. Second, try and catch protect us, because storage can be blocked, for example in private browsing. Never store passwords or private data in localStorage, because any script on the page can read it.

Example 3: useFetch

Loading, error and data appeared in the data lesson. Wrap them once:

import { useState, useEffect } from 'react';

export function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState('');

  useEffect(() => {
    const controller = new AbortController();
    setLoading(true);
    setError('');

    async function load() {
      try {
        const response = await fetch(url, { signal: controller.signal });
        if (!response.ok) throw new Error('Status ' + response.status);
        setData(await response.json());
      } catch (err) {
        if (err.name !== 'AbortError') setError('Could not load data.');
      } finally {
        if (!controller.signal.aborted) setLoading(false);
      }
    }
    load();

    return () => controller.abort();
  }, [url]);

  return { data, loading, error };
}
function Posts() {
  const { data, loading, error } = useFetch('https://jsonplaceholder.typicode.com/posts?_limit=3');

  if (loading) return <p>Loading...</p>;
  if (error) return <p role="alert">{error}</p>;
  return <ul>{data.map((p) => <li key={p.id}>{p.title}</li>)}</ul>;
}

The component is now only a few lines, and it still handles loading, errors and cancelling. If you later improve useFetch, every component gets the improvement. For big apps, a library like TanStack Query (lesson 25) gives you a more complete version of this idea.

Example 4: useDebounce

When someone types in a search box, you do not want to send a request on every key press. A debounce waits until the person pauses:

import { useState, useEffect } from 'react';

export function useDebounce(value, delay = 400) {
  const [debounced, setDebounced] = useState(value);

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

  return debounced;
}
function Search() {
  const [text, setText] = useState('');
  const search = useDebounce(text, 400);

  const { data } = useFetch('/api/search?q=' + encodeURIComponent(search));
  return <input value={text} onChange={(e) => setText(e.target.value)} />;
}

Each key press sets a new timer and cancels the old one with the cleanup. Only when the person stops typing for 400 milliseconds does debounced change, and only then does the search start. encodeURIComponent makes the text safe to use in a URL.

Example 5: useWindowSize and useOnline

Hooks are very good at wrapping browser features that need a listener and a cleanup:

export function useOnline() {
  const [online, setOnline] = useState(navigator.onLine);

  useEffect(() => {
    const on = () => setOnline(true);
    const off = () => setOnline(false);
    window.addEventListener('online', on);
    window.addEventListener('offline', off);
    return () => {
      window.removeEventListener('online', on);
      window.removeEventListener('offline', off);
    };
  }, []);

  return online;
}

Now any component can show a banner such as “You are offline” with a single line: const online = useOnline();.

The rules of hooks

  • Call hooks only at the top level of a component or a custom hook. Never inside if, loops, nested functions or after an early return.
  • Call hooks only from React components or custom hooks, never from normal functions.
  • Name custom hooks starting with use.

React remembers state by the order of the hook calls. If the order could change between draws, React would give the wrong state to the wrong hook. The ESLint plugin eslint-plugin-react-hooks checks these rules for you. Keep it on.

How to design a good hook

  • Give it one clear job and a clear name: useDebounce, not useStuff.
  • Accept simple inputs (a key, a URL, a delay) and return simple outputs.
  • Make it work for the first draw, for changes and for cleanup.
  • Do not make a hook just to move code around. Make one when logic repeats, or when it makes a component much easier to read.

Where to keep hooks

Put them in a hooks folder inside src, one file for each hook, named after it, for example src/hooks/useDebounce.js. A custom hook file needs no JSX, so .js is fine.

Common mistakes

  • Calling a hook inside a condition or loop.
  • Forgetting that each call of a hook has its own separate state, and expecting two components to share data (use context for that).
  • Missing cleanup for listeners and timers in the hook.
  • Forgetting dependencies in the effect inside the hook.
  • Naming the function without use, so the lint rules cannot help.

When to make a custom hook: a short guide

Beginners sometimes make hooks for everything, and sometimes never make one. A simple guide helps you find the right time.

  • You wrote the same state and effect in two places. This is the clearest sign. Move it into a hook.
  • A component is long because one piece of logic, such as a timer or a listener, takes many lines. A hook with a good name hides the details and makes the component easy to read.
  • The logic is something you can name in a few words, such as “keeps a value in storage” or “knows if the user is online”.

Do not make a hook if you only call one built-in hook and there is no real logic around it. function useCount() { return useState(0); } adds nothing. A hook should hide some work, not just rename a function.

A final tip: after you write a hook, use it from two different components to check that it works well with separate state in each. If the hook only works for one place, it is probably not general enough yet, and you may want to change its inputs.

Questions beginners often ask

Can a custom hook return JSX? It can, but usually it should not. A hook shares logic. If you want to share a piece of screen, make a component instead.

Can hooks call other hooks? Yes. This is how bigger hooks are built, for example useSearch can use both useDebounce and useFetch.

How do I test a custom hook? Use renderHook from React Testing Library, as lesson 26 shows. Or test it through a small component that uses it.

Should I share hooks between projects? Good hooks are often copied between your own projects, or published as small packages. Libraries such as usehooks-ts and react-use also give you many ready-made hooks to read and learn from.

Your task

  1. Write useToggle and use it for a menu and for a show/hide password button.
  2. Write useLocalStorage and keep a dark mode choice after reload.
  3. Write useDebounce and use it for a search box that filters a local list.
  4. Write useOnline and show a banner when the internet is lost (turn the network off in your browser tools to test).

How to check it worked

  • Each hook works in two components at the same time, with separate state.
  • The saved choice survives a reload.
  • Typing quickly in the search box does not filter on every key press.

Summary

  • A custom hook is a function starting with use that calls other hooks, so you can reuse logic.
  • Each call gets its own state. Hooks share logic, not data.
  • Good candidates: toggles, storage, fetching, debounce, browser listeners.
  • Follow the rules of hooks: top level only, and only in components or other hooks.

Frequently asked questions

What is a custom hook in React?

A function whose name starts with use and which calls other hooks. It lets you reuse state and effect logic in many components.

Do two components that use the same custom hook share state?

No. Each component gets its own copy of the state. To share data, use context or lift state up.

Why must hook names start with use?

It tells React and the lint tools that the function follows the rules of hooks, so mistakes can be found early.

Is it safe to store data in localStorage?

It is fine for simple choices like a theme. Never store passwords or private data there, because scripts on the page can read it.