React Custom Hooks
Table of Contents + −
In the last lesson you met the React useContext Hook, the final built-in hook. Now you’ll learn to build custom hooks, your own functions that bundle hook logic so you can reuse it anywhere instead of copy-pasting it.
🤔 Why do we need custom hooks?
Here’s the pain. You write some useState and useEffect logic, then you need the exact same logic in another component, so you copy and paste it. That copy-paste habit causes problems.
- A toggle (
useStateplus a flip function) appears in many components. - Data fetching (loading state, data state, error state, an effect) repeats on every page that loads data.
- When you fix a bug in one copy, you have to remember to fix it in all the others.
- Each component gets cluttered with logic that isn’t really about what it shows.
So a custom hook is about extracting that logic into one reusable function. Write it once, call it anywhere, fix it in one place.
🐍 What is a custom hook?
A custom hook is simpler than it sounds. There’s no special React feature for it, it’s just a function that follows two small rules.
- Its name starts with
use, likeuseToggleoruseFetch. That prefix is how React and its tools know it’s a hook. - Inside, it calls other hooks, like
useStateoruseEffect. That’s what makes it a hook and not a plain function.
So a custom hook is just a normal JavaScript function that uses hooks inside and has a use name. That’s the whole definition.
The use prefix is required
The use name isn’t just a style choice. React’s rules-of-hooks tooling relies on it to check that hooks are called correctly. A function that calls hooks but isn’t named useSomething will trip those checks.
💡 A simple custom hook: useToggle
Let’s build the smallest useful one. A toggle is a boolean plus a function to flip it, so we pull that pair into useToggle.
import { useState } from "react";
// our custom hook - just a function that uses useState insidefunction useToggle(initialValue = false) { const [on, setOn] = useState(initialValue);
function toggle() { setOn((prev) => !prev); }
// return what the component needs to use return [on, toggle];}Read it like any function that happens to use a hook.
- It takes an
initialValue, defaulting tofalse. - Inside, it keeps the boolean with
useState, exactly like you’d do in a component. toggleflips the value using the functional updater, so it’s always based on the latest value.- It returns
[on, toggle], the pair the component will use, much likeuseStateitself returns a pair.
Now any component can use it in one clean line.
function Settings() { const [darkMode, toggleDarkMode] = useToggle(false);
return ( <button onClick={toggleDarkMode}> Dark mode is {darkMode ? "on" : "off"} </button> );}So the component reads almost like English now. The toggle logic lives in the hook, and Settings just uses it.
🖥️ A more real example: useFetch
Data fetching is the classic case. The loading, data, and error logic repeats on every page, so let’s bundle it into useFetch.
import { useState, useEffect } from "react";
function useFetch(url) { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null);
useEffect(() => { setLoading(true); fetch(url) .then((res) => res.json()) .then((json) => setData(json)) .catch((err) => setError(err)) .finally(() => setLoading(false)); }, [url]); // re-run when the url changes
return { data, loading, error };}Walk through what it bundles up.
- It keeps three pieces of state:
data,loading, anderror. - The
useEffectruns the fetch, fillsdataon success, fillserroron failure, and turnsloadingoff at the end. - The dependency array
[url]re-runs the fetch whenever the url changes. - It returns all three values in an object, so the component can read whichever it needs.
Now a component that loads users is tiny and readable.
function UserList() { const { data, loading, error } = useFetch("https://api.example.com/users");
if (loading) return <p>Loading...</p>; if (error) return <p>Something went wrong.</p>; return <ul>{data.map((u) => <li key={u.id}>{u.name}</li>)}</ul>;}So all the fetching machinery is hidden in useFetch, and the component just shows the result. Any other page can reuse the exact same hook.
🔁 Each call gets its own state
This is the part people miss, so it’s worth saying clearly. A custom hook does not share state between components, see, each call is fully separate.
- Two components both calling
useToggleget two independent toggles. - Calling
useFetchtwice gives two separate loading, data, and error sets. - A custom hook shares logic, not state. Think of it as a recipe, not a single shared pot.
So reusing a hook never accidentally links two components together. Each one gets its own private state, just as if you’d written the hooks directly inside it.
Logic is shared, state is not
If you actually want two components to share the same value, that’s a job for context or lifting state up, not a custom hook. Custom hooks reuse the behavior, but each caller keeps its own separate state.
⚠️ Common Mistakes
The most common mistake is calling hooks inside a plain function that doesn’t start with use.
// ❌ calls useState but isn't named with "use" - breaks the rules of hooksfunction getToggle() { const [on, setOn] = useState(false); return [on, () => setOn(!on)];}
// ✅ name it with the use prefixfunction useToggle() { const [on, setOn] = useState(false); return [on, () => setOn(!on)];}Keep these in mind.
- Don’t skip the
useprefix. Any function that calls hooks must be nameduseSomething. - Don’t call your hook conditionally. Like all hooks, call custom hooks at the top level, not inside an
ifor a loop. - Don’t expect shared state. Each call gets its own state, so don’t use a custom hook hoping two components will stay in sync.
✅ Best Practices
A few habits make custom hooks a pleasure to use.
- Extract logic into a custom hook when you see the same hook code in two or more places.
- Name it for what it does, like
useToggle,useFetch, oruseLocalStorage. - Return exactly what callers need, an array for a simple pair or an object for several named values.
- Keep one hook focused on one job, so it stays easy to understand and reuse.
Start by copying, then extract
You don’t have to design a hook up front. Write the logic right inside a component first. The moment you need the same thing somewhere else, lift it into a use-named function. That’s the natural time to make a custom hook.
🧩 What You’ve Learned
- ✅ A custom hook is just a function that starts with
useand calls other hooks inside - ✅ It lets you extract and reuse stateful logic across many components, written once
- ✅
useTogglebundles a boolean and a flip function;useFetchbundles loading, data, and error - ✅ It returns whatever the caller needs, an array for a pair or an object for several values
- ✅ Each call gets its own separate state, so it shares logic, not state
- ✅ Follow the rules of hooks: use the
useprefix and call hooks at the top level
Check Your Knowledge
Test what you learned. Pick an answer for each question, then click Check.
- 1
What makes a function a custom hook?
Why: A custom hook is just a function named useSomething that calls hooks like useState or useEffect inside. The use prefix lets React's tooling check it.
- 2
What does a custom hook let you reuse?
Why: Custom hooks reuse logic, not state. You extract the useState and useEffect code once and call it from many components.
- 3
If two components both call the same custom hook, what happens to their state?
Why: A custom hook shares logic, not state. Each call creates its own independent state, just as if the hooks were written directly inside the component.
- 4
Why must a custom hook be named with the use prefix?
Why: The use prefix is how React and its linting tools recognize a hook, so they can enforce that hooks are called at the top level and not conditionally.
🚀 What’s Next?
Now you can package your own hook logic and reuse it across your whole app. To finish the module, let’s pull together the habits that keep all your hooks, built-in and custom, clean and bug-free.