Mar 16, 2026

Advanced React Hooks Explained

Learn how advanced React hooks like useMemo, useCallback, useRef, and useReducer help build scalable and high-performance React applications.

React
Hooks
Performance
Architecture

9 min read

Advanced React Hooks Explained

1. Introduction

React Hooks changed frontend development by making stateful logic possible in functional components without class complexity. For many developers, the journey starts with useState and useEffect, and those two hooks solve a lot of day-to-day UI problems.

In a Next.js codebase, these hooks sit underneath route-level concerns like metadata and content rendering, so this guide pairs well with Next.js SEO Checklist for App Router Projects when you are tuning both component behavior and search visibility.

But in production applications, teams quickly hit challenges that basic hooks alone do not solve elegantly: expensive re-renders, callback identity issues, complex state transitions, shared state across deep trees, and repeated logic across multiple screens.

That is where Advanced React Hooks become essential. They help teams write cleaner abstractions, improve performance, and keep components maintainable as products scale.

In this guide, you will get practical, real-world React hooks explained with examples for:

  • useMemo
  • useCallback
  • useRef
  • useReducer
  • useContext
  • custom hooks

You will also see common mistakes and React hooks best practices used in real production codebases.

This guide is intentionally focused on production scenarios where teams use useMemo useCallback useRef useReducer together to control re-renders, model complex state transitions, and keep components maintainable in production React and Next.js applications.

2. What Are Advanced React Hooks?

Advanced hooks are not “different APIs” outside React. They are built-in or composable patterns that solve more complex concerns than local state toggles.

Think of them as tools for:

  • Performance control (useMemo, useCallback)
  • Stable mutable references (useRef)
  • Predictable state transitions (useReducer)
  • Cross-tree data sharing (useContext)
  • Logic reuse at scale (custom hooks)

If your component tree is growing, business rules are becoming richer, or your profiling shows unnecessary work, advanced hooks are often the right next step.

3. useMemo – Optimizing Expensive Computations

useMemo caches the result of a calculation and recomputes only when dependencies change.

const result = useMemo(() => expensiveCalculation(input), [input]);

When useMemo is useful

Use it when:

  • The calculation is expensive (sorting, filtering, heavy transforms)
  • The result is used during render
  • Dependencies change less often than renders

Example: sorting a large dataset

import { useMemo, useState } from 'react';

type User = { id: string; name: string; score: number };

function Leaderboard({ users }: { users: User[] }) {
  const [sortBy, setSortBy] = useState<'name' | 'score'>('score');

  const sortedUsers = useMemo(() => {
    const list = [...users];
    return list.sort((a, b) => {
      if (sortBy === 'name') return a.name.localeCompare(b.name);
      return b.score - a.score;
    });
  }, [users, sortBy]);

  return (
    <>
      <button onClick={() => setSortBy('name')}>Sort by name</button>
      <button onClick={() => setSortBy('score')}>Sort by score</button>
      <ul>
        {sortedUsers.map((u) => (
          <li key={u.id}>{u.name} - {u.score}</li>
        ))}
      </ul>
    </>
  );
}

Example: filtering lists

const filteredProducts = useMemo(() => {
  const q = query.trim().toLowerCase();
  return products.filter((p) => p.title.toLowerCase().includes(q));
}, [products, query]);

Example: memoizing expensive calculations

const analytics = useMemo(() => {
  return buildMonthlyMetrics(transactions); // CPU-heavy aggregation
}, [transactions]);

Do not memoize everything by default. useMemo adds cognitive overhead. Use it where measurements indicate real cost.

4. useCallback – Preventing Unnecessary Re-renders

Functions are recreated on every render in JavaScript. That is usually fine, but when you pass callbacks to memoized children, changing function references can trigger avoidable child re-renders.

useCallback memoizes function identity.

const handler = useCallback(() => {
  // logic
}, [deps]);

Example: stable callback for memoized child

import { memo, useCallback, useState } from 'react';

const SearchInput = memo(function SearchInput({ onSearch }: { onSearch: (v: string) => void }) {
  console.log('SearchInput rendered');
  return <input onChange={(e) => onSearch(e.target.value)} placeholder="Search" />;
});

export default function ProductPage() {
  const [query, setQuery] = useState('');
  const [theme, setTheme] = useState('dark');

  const handleSearch = useCallback((value: string) => {
    setQuery(value);
  }, []);

  return (
    <div data-theme={theme}>
      <button onClick={() => setTheme((t) => (t === 'dark' ? 'light' : 'dark'))}>Toggle Theme</button>
      <SearchInput onSearch={handleSearch} />
      <p>Current query: {query}</p>
    </div>
  );
}

Without useCallback, SearchInput may re-render on unrelated parent changes like theme toggling.

In performance-oriented components, useMemo useCallback useRef useReducer often work together to keep render cycles predictable.

5. useRef – Accessing DOM Elements and Persisting Values

useRef has two major use cases.

A. Accessing DOM elements

import { useEffect, useRef } from 'react';

function LoginForm() {
  const emailRef = useRef<HTMLInputElement>(null);

  useEffect(() => {
    emailRef.current?.focus();
  }, []);

  return <input ref={emailRef} placeholder="Email" />;
}

This is useful for focus management, scroll positioning, and imperative integrations.

B. Storing mutable values without re-renders

Unlike state, updating a ref.current value does not trigger re-render.

import { useEffect, useRef, useState } from 'react';

function RenderCounter() {
  const [count, setCount] = useState(0);
  const renderCount = useRef(0);

  useEffect(() => {
    renderCount.current += 1;
  });

  return (
    <div>
      <p>Count: {count}</p>
      <p>Rendered: {renderCount.current} times</p>
      <button onClick={() => setCount((c) => c + 1)}>Increment</button>
    </div>
  );
}

Great use cases: timers, previous value tracking, request IDs, or any mutable value that should persist across renders.

6. useReducer – Managing Complex State

When state transitions become complex, useState can get messy. useReducer centralizes transitions in a reducer function.

Use it when:

  • Multiple state values change together
  • You need predictable transition rules
  • State updates depend on action types

Example: form with validation state

import { useReducer } from 'react';

type State = {
  name: string;
  email: string;
  loading: boolean;
  error: string | null;
};

type Action =
  | { type: 'SET_FIELD'; field: 'name' | 'email'; value: string }
  | { type: 'SUBMIT' }
  | { type: 'SUCCESS' }
  | { type: 'FAIL'; message: string };

const initialState: State = {
  name: '',
  email: '',
  loading: false,
  error: null,
};

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case 'SET_FIELD':
      return { ...state, [action.field]: action.value };
    case 'SUBMIT':
      return { ...state, loading: true, error: null };
    case 'SUCCESS':
      return { ...state, loading: false };
    case 'FAIL':
      return { ...state, loading: false, error: action.message };
    default:
      return state;
  }
}

export function SignupForm() {
  const [state, dispatch] = useReducer(reducer, initialState);

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    dispatch({ type: 'SUBMIT' });
    try {
      await fakeApiCall(state);
      dispatch({ type: 'SUCCESS' });
    } catch {
      dispatch({ type: 'FAIL', message: 'Signup failed' });
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={state.name}
        onChange={(e) => dispatch({ type: 'SET_FIELD', field: 'name', value: e.target.value })}
      />
      <input
        value={state.email}
        onChange={(e) => dispatch({ type: 'SET_FIELD', field: 'email', value: e.target.value })}
      />
      {state.error ? <p>{state.error}</p> : null}
      <button disabled={state.loading}>{state.loading ? 'Saving...' : 'Submit'}</button>
    </form>
  );
}

useReducer makes complex state updates explicit and easier to test.

7. useContext – Managing Global State

useContext helps avoid prop drilling by sharing data across component trees.

Example: theme context

import { createContext, useContext, useMemo, useState } from 'react';

type Theme = 'light' | 'dark';

type ThemeContextType = {
  theme: Theme;
  toggleTheme: () => void;
};

const ThemeContext = createContext<ThemeContextType | null>(null);

export function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState<Theme>('dark');

  const value = useMemo(
    () => ({
      theme,
      toggleTheme: () => setTheme((t) => (t === 'dark' ? 'light' : 'dark')),
    }),
    [theme]
  );

  return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
}

export function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) throw new Error('useTheme must be used inside ThemeProvider');
  return context;
}

useContext is excellent for stable global concerns like theme, locale, or current user metadata. For high-frequency updates, evaluate dedicated state libraries.

8. Custom Hooks – Reusable Logic in React

Custom hooks let you package and reuse logic cleanly. This is one of the most powerful scaling patterns in modern React.

Example: useFetch

import { useEffect, useState } from 'react';

export function useFetch<T>(url: string) {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    let cancelled = false;

    async function load() {
      try {
        setLoading(true);
        const res = await fetch(url);
        if (!res.ok) throw new Error('Request failed');
        const json = await res.json();
        if (!cancelled) setData(json);
      } catch (e) {
        if (!cancelled) setError((e as Error).message);
      } finally {
        if (!cancelled) setLoading(false);
      }
    }

    load();
    return () => {
      cancelled = true;
    };
  }, [url]);

  return { data, loading, error };
}

Example: useAuth

export function useAuth() {
  const { user, token } = useSessionStore();
  const isAuthenticated = Boolean(user && token);
  return { user, token, isAuthenticated };
}

Example: useWindowSize

import { useEffect, useState } from 'react';

export function useWindowSize() {
  const [size, setSize] = useState({ width: 0, height: 0 });

  useEffect(() => {
    function onResize() {
      setSize({ width: window.innerWidth, height: window.innerHeight });
    }

    onResize();
    window.addEventListener('resize', onResize);
    return () => window.removeEventListener('resize', onResize);
  }, []);

  return size;
}

Well-designed React custom hooks reduce duplication, improve consistency, and keep component files focused.

9. Best Practices When Using React Hooks

These React hooks best practices are reliable in real projects:

  1. Follow Hook rules strictly
    Call hooks only at top level, never conditionally.

  2. Respect dependency arrays
    Use ESLint Hook rules. Missing dependencies create stale bugs.

  3. Keep hooks focused
    A hook should have one clear responsibility.

  4. Extract reusable logic
    If you copy logic twice, consider a custom hook.

  5. Avoid premature optimization
    Add useMemo/useCallback only where profiling shows value.

  6. Compose hooks for clarity
    Small hooks can be combined into higher-level hooks for complex workflows.

  7. Name hooks clearly
    Use intention-revealing names like useUserPermissions or usePaymentSummary.

10. Common Mistakes Developers Make with Hooks

Even experienced developers make these mistakes.

Incorrect dependency arrays

Missing dependencies in useEffect or useMemo can cause stale state and hard-to-debug behavior.

Excessive re-renders

Passing unstable functions/objects to children can trigger noisy renders.

Overusing hooks

Not every value needs useMemo. Not every function needs useCallback. Added complexity can outweigh gains.

Mixing too much logic in components

If your component is fetching data, handling business rules, and rendering complex UI all at once, split it.

Ignoring profiling tools

Hooks are not magic performance switches. Validate changes in React DevTools Profiler.

11. Conclusion

Advanced hooks are not just advanced syntax. They are architecture tools.

When used well, they help you:

  • Build components that are easier to reason about
  • Improve render performance where it matters
  • Reuse logic with clean abstractions
  • Keep applications scalable as teams and features grow

If you want to level up from “hooks user” to “production-ready React engineer,” focus on patterns, not just APIs. Understand where each hook provides leverage, profile before optimizing, and apply constraints deliberately.

With the right mental model, Advanced React Hooks become one of the most effective ways to build fast, maintainable React applications.

Related Reading