šŸš€ LEVEL UP TO SENIOR:Unlock 500+ Advanced Practical Challenges & Exercises.
šŸŽ“ COURSERA PARTNER:Earn professional Google, Meta, and IBM certificates to supercharge your resume.
HTML MASTER CLASS /// LEARN TAGS /// BUILD STRUCTURE /// SEMANTIC WEB /// HTML MASTER CLASS /// LEARN TAGS ///

Custom Hooks in React: Web Development

Learn about Custom Hooks in this comprehensive React tutorial for frontend web development. Master logic encapsulation. Learn the naming protocols, implement complex data-fetching hooks, and discover how to build your own library of reusable patterns like useLocalStorage and useAuth.

⚔ Total XP: 0|šŸ’» react XP: 0

Skill Matrix

UNLOCK NODES BY LEARNING NEW TAGS.

System Hub

Core logic.

Quick Quiz //

What is the primary danger of ignoring this concept?


šŸš€ LEVEL UP TO SENIOR:Unlock 500+ Advanced Practical Challenges & Exercises.
šŸŽ“ COURSERA PARTNER:Earn professional Google, Meta, and IBM certificates to supercharge your resume.

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 Logic
localhost:3000

Redundant 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;
}
localhost:3000

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 B
localhost:3000
localhost:3000/concept-3
UI Rendered Successfully
React 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);
localhost:3000

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 conditions
localhost:3000

Clean 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 };
localhost:3000

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 */
localhost:3000

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');
localhost:3000

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';
localhost:3000

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) */
localhost:3000

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

ChromeSupported

Fully supported.

FirefoxSupported

Fully supported.

SafariSupported

Fully supported.

EdgeSupported

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

THE BUG

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.

THE FIX

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.

THE BUG

React throws a 'Rendered fewer hooks than expected' error after adding a custom hook inside a conditional or early return.

THE FIX

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`.

THE BUG

A 'Can't perform a React state update on an unmounted component' warning appears from inside a custom data-fetching hook.

THE FIX

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];
}

Interview Prep

?Frequently Asked Questions

Pascual Vila

Pascual Vila

Frontend Instructor // Code Syllabus

Common Pitfalls & Errors

The Error //

Mutating State Directly

// Wrong const [user, setUser] = useState({ name: 'Alice' }); user.name = 'Bob'; // React won't re-render // Correct setUser({ ...user, name: 'Bob' });

The Solution //

Never mutate a state variable directly (e.g., state.count = 1). Always use the setter function provided by useState to ensure the component re-renders.

The Error //

Missing 'key' prop in lists

// Wrong {items.map(item => <li>{item.name}</li>)} // Correct {items.map(item => <li key={item.id}>{item.name}</li>)}

The Solution //

When rendering a list of elements using .map(), always provide a unique 'key' prop to the outermost element to help React identify which items have changed.

Lesson Glossary

[01]Custom Hook

A JavaScript function that starts with 'use' and can call other hooks.

Code Preview
function useLogic() {}

[02]Logic Encapsulation

The practice of hiding complex logic inside a function, providing a simple interface to the user.

Code Preview
Abstraction

[03]use

The mandatory prefix for all custom hooks to ensure React's rules are followed.

Code Preview
Rule of Naming

[04]Independent State

The fact that each call to a custom hook creates its own unique state instances.

Code Preview
Isolation

[05]DRY

Don't Repeat Yourself. A software engineering principle aimed at reducing repetition.

Code Preview
Efficiency

[06]useFetch

A common custom hook pattern for handling data loading, error, and response states.

Code Preview
Pattern

[07]Hook Composition

Building a higher-level custom hook by calling other custom hooks inside it.

Code Preview
usePaginatedFetch() { useFetch(); usePagination(); }

[08]Generic Hook

A custom hook parameterized with a TypeScript type so callers can specify their own data shape.

Code Preview
useFetch<T>(url: string)

[09]AbortController

A browser API for cancelling an in-flight fetch request, commonly used in a hook's effect cleanup.

Code Preview
controller.abort()

[10]renderHook

A Testing Library utility that mounts a custom hook in a hidden test component to test it directly.

Code Preview
renderHook(() => useCounter())

Continue Learning