Once you catch yourself copy-pasting the same useState and useEffect logic into a second or third component, it's time for a custom hook. This lesson covers how to extract stateful logic into reusable functions, why each hook call gets fully independent state, and how to build real patterns like useFetch, useLocalStorage, and useAuth.
1The Duplicate Logic Problem
As React apps grow, it's common to end up writing the exact same useState and useEffect logic in multiple components ā tracking window width, fetching data, or handling form inputs are classic examples. Copy-pasting this logic across components quickly becomes a maintenance burden.
A fix or improvement has to be repeated everywhere the logic was duplicated, and the copies inevitably drift out of sync over time ā exactly the problem custom hooks exist to solve.
// Custom Hooks: Extracting and Reusing LogicRedundant Code
Copy-pasting state logic
2What is a Custom Hook?
A custom hook is simply a standard JavaScript function that encapsulates stateful logic so it can be reused across components. By convention its name must start with use, as in useWindowWidth.
Inside that function, you're free to call built-in hooks like useState and useEffect, or even other custom hooks ā the function just packages up whatever combination of hooks it needs and exposes a simpler interface to whoever calls it.
function useWindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
// ... logic
return width;
}Logic Extraction
Wrapping hooks inside functions
3The 'use' Prefix Rule
A custom hook's name must start with use ā useTheme, useFetch, useAuth ā and this isn't just a style preference, it's enforced by React's linter. That prefix is what tells the linter a function contains stateful logic and must follow the Rules of Hooks.
Without the use prefix, the linter has no way of knowing the function calls hooks internally, so it can't warn you about violations like calling it conditionally or inside a loop.
// Component A: useWidth() -> State A
// Component B: useWidth() -> State BReact Component Preview
4Independent State
Custom hooks share reusable logic, not state itself. If two different components each call useCounter(), they get two completely independent count variables with their own separate memory.
Clicking a button that updates state through one component's hook instance has no effect whatsoever on the other's, even though both components are running the exact same underlying function.
const { data, loading, error } = useFetch(url);State Isolation
Logic is shared, State is unique
5Encapsulating Data Fetching
Data fetching is one of the most common uses for custom hooks, since a robust fetch typically needs to track the returned data, a loading flag, and an error state all at once. Rather than repeating three useState calls and a useEffect in every component that needs data, you consolidate all of it into a single hook.
A useFetch(url) hook can return { data, loading, error }, letting a component just destructure exactly what it needs and render conditionally based on those flags.
// Same rules apply: No loops, No conditionsClean Components
Abstracting the dirty work.
6Encapsulating Browser APIs
Custom hooks are also a great way to bridge React state with native browser APIs that weren't designed with React in mind. A useMediaQuery hook can wrap window.matchMedia to report whether the viewport matches a breakpoint like (max-width: 768px).
Similarly, a useGeolocation hook can wrap the browser's Geolocation API to expose the user's latitude and longitude as ordinary React state, hiding the messy native listener setup behind a simple hook call.
return { count, increment, decrement };Browser Integrations
Wrapping native APIs
7Returning Values
Because a custom hook is just a function, it can return whatever shape of value best fits how it will be used. It might return a single primitive like a boolean, an array the way useState does (return [value, toggleValue]), or an object when there are several named values to expose.
An object return like return { data, isLoading, refetch } is often clearer for hooks with more than two values, since callers can destructure only the names they actually need.
/* Hook Lab: useTimer & useForm Independent Instances Rendered */Flexible Returns
Return whatever the consumer needs.
8Rule of Hooks Applicability
Custom hooks are bound by the exact same Rules of Hooks as built-in ones: they cannot be called conditionally inside an if statement, and they cannot be called inside a loop, such as calling useImageLoader(i) on every iteration of a for loop.
They must always run at the top level of a component or inside another custom hook, in the same order on every render ā that consistent order is how React matches each hook call to its correct piece of internal state.
const [theme, setTheme] = useLocalStorage('theme', 'light');Rules Apply Here Too
No loops. No conditions.
9Creating useLocalStorage
A classic custom hook example is useLocalStorage, which behaves just like useState but automatically persists its value to the browser's localStorage on every change. It initializes state by reading any existing value from localStorage first.
A useEffect inside the hook then writes the current value back to storage whenever it changes, so the data survives a page refresh ā all while the component using it just sees const [theme, setTheme] = useLocalStorage('theme', 'dark'), exactly like ordinary useState.
import { useSearch } from './hooks/useSearch';Persistence
Syncing state to local storage
10Building useAuth
Another powerful pattern is wrapping a context in a custom hook, such as useAuth() returning useContext(AuthContext), instead of forcing every component to import both useContext and the raw context object directly.
This hides the underlying implementation and gives every consuming component a single, clean call to reach values like the current user and a logout function, e.g. const { user, logout } = useAuth().
/* Next: SPA Navigation (Router) */Context Wrappers
Simplifying global state access.
11Composing Hooks Together
Custom hooks aren't limited to wrapping built-in hooks ā they can call other custom hooks as well. A usePaginatedFetch hook might internally combine a usePagination hook and a useFetch hook, producing a higher-level hook out of two smaller, independently reusable ones.
This composability is what lets a project's hook library grow into progressively more powerful building blocks without any single hook becoming a monolith.
12Generic, Typed Custom Hooks
In a TypeScript codebase, a hook like useFetch shouldn't be hardcoded to one specific data shape. Declaring it generic, as useFetch<T>(url: string), lets each call site supply its own type ā useFetch<User>(url) at one call site and useFetch<Product[]>(url) at another ā while the hook's internal implementation stays identical.
This keeps a single hook fully reusable across an entire codebase without sacrificing type safety at any individual call site.
13Cleanup and Cancellation
A production-ready useFetch hook needs to clean up after itself. If a component unmounts, or its URL argument changes, before an in-flight request resolves, updating state from that now-stale request is a common source of bugs and React warnings.
Wrapping the request in an AbortController and calling controller.abort() inside the effect's cleanup function cancels the stale request, so state can only ever be updated from the request that's actually still relevant.
14Testing Custom Hooks
A custom hook can't be rendered like a component, but it also can't be called like an ordinary function outside of React's rendering context, since it calls other hooks internally. Testing Library's renderHook utility resolves this by mounting the hook inside a minimal, invisible test component and exposing its live return value.
Wrapping any state-updating call in act(...) ensures React processes the resulting re-render before the test asserts against the hook's updated return value.
15Step-by-Step Breakdown
The Duplicate Logic Problem. Welcome to Custom Hook Design. As you build React applications, you'll often find yourself writing the exact same useState and useEffect logic in multiple components. For example, tracking the window width, fetching data, or handling form inputs. Copy-pasting this logic creates a maintenance nightmare.
What is a Custom Hook?. Custom Hooks are the solution. A Custom Hook is simply a standard JavaScript function that encapsulates stateful logic. By convention, its name must start with 'use'. Inside this function, you can call built-in hooks like useState or useEffect, or even other custom hooks.
The 'use' Prefix Rule. The name of your custom hook MUST start with 'use' (e.g., useTheme, useFetch, useAuth). This isn't just a naming convention; it's a strict rule enforced by the React linter. It tells React that this function contains stateful logic and must abide by the Rules of Hooks.
What prefix MUST you add to a function name to tell React that it is a Custom Hook containing stateful logic?
- āget
- āuse
Independent State. A critical concept to understand is that Custom Hooks DO NOT share state between components. They only share stateful *logic*. If two different components call useCounter(), they each get their own completely independent count variable. Clicking the button in Component A will not affect Component B.
If Navbar and Footer both call useThemeSwitch(), and Navbar triggers the switch to dark mode internally, what happens to Footer?
- āFooter also switches to dark mode because they share state
- āFooter is unaffected because custom hooks create independent state instances
Encapsulating Data Fetching. One of the most common uses for Custom Hooks is data fetching. A robust fetch operation requires tracking data, loading status, and error states. Instead of writing three useStates and a complex useEffect in every component, you can build a clean useFetch hook.
Encapsulating Browser APIs. Custom hooks are fantastic for bridging React state with standard browser APIs. For instance, you can create a useMediaQuery hook to check if the user is on a mobile device, or useGeolocation to easily track the user's GPS coordinates.
Returning Values. Because a custom hook is just a JavaScript function, you can return anything you want. You can return a single primitive (like a boolean), an array (like useState does), an object (great for multiple values), or even functions that the component can call.
Rule of Hooks Applicability. Never forget: Custom hooks are subject to the same strict Rules of Hooks as built-in hooks. You cannot call them conditionally (inside if statements), and you cannot call them inside loops. They must always be called at the top level of your component or inside another custom hook.
Can a custom hook (like useWindowSize) call a built-in React hook (like useEffect) internally?
- āNo, only React components can call built-in hooks
- āYes, custom hooks are designed specifically to compose other hooks
Creating useLocalStorage. Let's build a classic: useLocalStorage. This hook acts exactly like useState, but it automatically intercepts changes and saves the value to the browser's localStorage, ensuring the data persists even if the user refreshes the page.
Building useAuth. Another powerful pattern is useAuth. Instead of exposing the raw Context object to every component, you build a custom hook that wraps the context. This hides implementation details and makes consuming auth data incredibly clean.
The DRY Principle. Custom hooks are the ultimate manifestation of the DRY (Don't Repeat Yourself) principle in React. By building a robust library of custom hooks, your actual UI components become highly declarative, containing very little imperative logic.
Composing Hooks Together. Custom hooks aren't limited to wrapping built-in hooks ā they can call OTHER custom hooks too. A usePaginatedFetch hook might internally call both useFetch and a usePagination hook, combining their behavior into one higher-level hook. This composability is what lets a hook library grow into increasingly powerful building blocks.
Generic, Typed Custom Hooks. In TypeScript, a hook like useFetch shouldn't be locked to one specific shape of data. Making it generic with useFetch<T>(url: string) lets every call site specify its own type ā useFetch<User>(url) versus useFetch<Product[]>(url) ā while the hook's internal implementation stays exactly the same.
Why make a hook like useFetch generic (useFetch<T>) instead of hardcoding a specific data type?
- āIt lets every call site specify its own data type while reusing the same implementation
- āGenerics make the hook execute faster at runtime
Cleanup and Cancellation. A well-built useFetch hook must clean up after itself. If the component unmounts (or the URL changes) before a request finishes, updating state on a stale request is a common source of bugs and warnings. Use an AbortController inside the effect and cancel the request in the cleanup function.
Testing Custom Hooks. Because a custom hook isn't a component, you can't render it directly in a test the way you'd render JSX ā but it still calls hooks internally, so it can't be called like a plain function either. Testing Library's renderHook utility solves this: it mounts the hook in a minimal test component behind the scenes and gives you back its live return value.
Why can't a custom hook like useCounter() simply be called directly as a plain function inside a test?
- āIt calls React hooks internally, which only work inside a rendering component
- āIt would be a JavaScript syntax error
Mastery Achieved. Hook design mastery achieved! You've learned how to extract logic into reusable functions, how independent state works, and how to build powerful primitives like useFetch and useLocalStorage ā plus how to compose hooks together, type them generically, clean up after async work, and test them with renderHook. Ready to navigate between pages with React Router?
Level Up š
Advanced cheat sheets, SEO tricks, and interview prep for this topic.
Browser Support
Fully supported.
Fully supported.
Fully supported.
Fully supported.
Accessibility (A11y)
1Encapsulate Accessible Behavior, Not Just Styling, in Shared Hooks
A hook like `useDisclosure` or `useModal` that's reused across the app is a good place to centralize keyboard handling (Escape to close, focus trapping) so every component using it inherits correct behavior automatically instead of reimplementing it inconsistently.
2Hooks Wrapping Async State Should Expose Enough for Accessible Loading and Error UI
A `useFetch`-style hook that returns `{ data, loading, error }` lets consuming components render an `aria-busy` region while loading and an accessible error message on failure, rather than leaving users staring at a blank screen with no announcement.
SEO Implications
- 1
Data-Fetching Hooks Determine What a Crawler Sees Before Hydration
If a `useFetch`-style custom hook only fetches data on the client after mount, the content it renders won't be present in the initial server-rendered HTML ā for content that matters for SEO, fetch or pass it in server-side instead of relying solely on a client-only hook.
- 2
Reusable Hooks Encourage Consistent, Crawlable Markup Across Pages
Centralizing logic like `useAuth` or `usePagination` in shared hooks makes it easier to guarantee every page that uses them renders the same predictable structure, which helps crawlers parse repeated patterns like paginated listings consistently.
Best Practices
Keep Custom Hooks Focused on One Concern
A hook like `useFetch` or `useLocalStorage` should do one job well and return a small, predictable interface ā bundling unrelated logic into a single hook makes it harder to test, reuse, and reason about independently.
Name Every Custom Hook Starting With `use`
This isn't optional ā React's linter relies on the `use` prefix to know a function calls other hooks internally and enforce the Rules of Hooks against it; skipping the prefix silently disables that safety net.
Cancel Async Work in the Cleanup Function
Any hook that starts a fetch, subscription, or timer should return a cleanup function that cancels it ā for fetch, an AbortController is the standard tool ā so a component that unmounts or re-runs the effect never sets state from stale, superseded work.
Frequent Bugs
A developer assumes two components sharing a custom hook are sharing the same state, and is confused when updating one doesn't affect the other.
Each call to a custom hook creates its own independent state ā custom hooks share logic, not state instances. If shared state across components is actually needed, lift it into a parent's state or a Context Provider instead.
React throws a 'Rendered fewer hooks than expected' error after adding a custom hook inside a conditional or early return.
Custom hooks are bound by the same Rules of Hooks as built-ins ā they must run unconditionally at the top level on every render. Move the condition inside the hook itself, e.g. by passing `null` as an argument, rather than wrapping the hook call in an `if`.
A 'Can't perform a React state update on an unmounted component' warning appears from inside a custom data-fetching hook.
The hook's fetch effect has no cleanup, so a request that resolves after the component unmounts still tries to call its setState. Add an AbortController, call abort() in the effect's cleanup function, and skip the state update if the request was aborted.
Real-World Examples
useLocalStorage for Persisted UI Preferences
A settings panel stores the user's chosen theme with `const [theme, setTheme] = useLocalStorage('theme', 'light')`, so the preference automatically survives page refreshes without any component needing to know about localStorage directly.
function useLocalStorage(key, initialVal) {
const [val, setVal] = useState(() => localStorage.getItem(key) || initialVal);
useEffect(() => localStorage.setItem(key, val), [key, val]);
return [val, setVal];
}