React Hooks Complete Guide: useState, useEffect & Beyond (2026)

A practical guide to every React Hook you will actually use. Covers the core hooks, performance hooks, React 19 additions like use() and useOptimistic, custom hook patterns, common mistakes, and the rules that govern them all.

In This Guide
  1. What Are React Hooks?
  2. useState: Managing Component State
  3. useEffect: Side Effects and Lifecycle
  4. useContext: Sharing State Without Props
  5. useReducer: Complex State Logic
  6. useRef: Refs and Mutable Values
  7. Performance Hooks: useMemo and useCallback
  8. React 19 Hooks: use, useOptimistic, useFormStatus
  9. Building Custom Hooks
  10. Common Mistakes and How to Fix Them
  11. Related Developer Tools
  12. Frequently Asked Questions

React Hooks changed how developers build user interfaces. Since their introduction in React 16.8, hooks have become the standard way to manage state, handle side effects, and share logic between components. Class components still work, but the React team has been clear: function components with hooks are the path forward.

This guide covers every hook you will encounter in a modern React codebase, from the foundational useState and useEffect to React 19 additions like use() and useOptimistic. Each section includes code you can copy into a project and start using immediately.

Format your React code. The JavaScript Formatter auto-formats JSX and JavaScript with consistent indentation and spacing. Paste, format, copy.

What Are React Hooks?

Hooks are regular JavaScript functions that let you "hook into" React features from function components. They follow two rules:

  1. Only call hooks at the top level. Do not call hooks inside loops, conditions, or nested functions. React relies on the call order being the same on every render to correctly associate state with each hook call.
  2. Only call hooks from React functions. Call hooks from function components or from other custom hooks. Do not call them from regular JavaScript functions.

These rules exist because React tracks hooks by their position in the call sequence. If you put a useState inside an if block, the call sequence changes between renders, and React cannot match state to the correct hook.

The eslint-plugin-react-hooks package enforces both rules automatically. If you are using Create React App, Next.js, or Vite with the React plugin, it is already configured.

useState: Managing Component State

useState is the most fundamental hook. It declares a state variable and gives you a function to update it.

import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <button onClick={() => setCount(0)}>Reset</button>
    </div>
  );
}

Functional Updates

When the new state depends on the previous state, use the functional form. This avoids stale closures when multiple updates happen in the same render cycle.

// Correct: functional update
setCount(prev => prev + 1);

// Risky: might use stale value in async handlers
setCount(count + 1);

Objects and Arrays as State

React uses Object.is for equality checks. If you mutate an object and pass the same reference, React skips the re-render. Always create a new object or array.

const [user, setUser] = useState({ name: 'Jane', role: 'dev' });

// Correct: spread to create new object
setUser(prev => ({ ...prev, role: 'admin' }));

// Wrong: mutation (React won't re-render)
user.role = 'admin';
setUser(user);

Lazy Initialization

If computing the initial value is expensive, pass a function instead of a value. React calls it only on the first render.

// This reads localStorage on every render (wasteful)
const [theme, setTheme] = useState(localStorage.getItem('theme'));

// This reads localStorage only once
const [theme, setTheme] = useState(() => localStorage.getItem('theme'));

useEffect: Side Effects and Lifecycle

useEffect runs code after the component renders. It handles data fetching, subscriptions, DOM manipulation, and anything else that interacts with the world outside of rendering.

import { useState, useEffect } from 'react';

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

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

    async function fetchUser() {
      const res = await fetch(`/api/users/${userId}`);
      const data = await res.json();
      if (!cancelled) setUser(data);
    }

    fetchUser();

    // Cleanup: runs before re-running or unmounting
    return () => { cancelled = true; };
  }, [userId]); // Only re-run when userId changes

  if (!user) return <p>Loading...</p>;
  return <h1>{user.name}</h1>;
}

The Dependency Array

Cleanup Functions

Return a function from useEffect to clean up subscriptions, timers, or abort controllers. React calls the cleanup before running the effect again and when the component unmounts.

useEffect(() => {
  const controller = new AbortController();

  fetch('/api/data', { signal: controller.signal })
    .then(res => res.json())
    .then(setData)
    .catch(err => {
      if (err.name !== 'AbortError') console.error(err);
    });

  return () => controller.abort();
}, []);
React 19 and Data Fetching

In React 19+, the recommended pattern for data fetching is using the use() hook with Suspense boundaries rather than useEffect. useEffect remains the right tool for subscriptions, event listeners, and DOM side effects.

useContext: Sharing State Without Props

useContext reads a value from a React context, eliminating the need to pass props through intermediate components (prop drilling).

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

const ThemeContext = createContext('light');

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

function ThemeToggle() {
  const { theme, setTheme } = useContext(ThemeContext);
  return (
    <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
      Current: {theme}
    </button>
  );
}

When the context value changes, every component that calls useContext for that context re-renders. For frequently-changing values, consider splitting into multiple contexts or using external state libraries like Zustand or Jotai that allow subscribing to specific slices.

useReducer: Complex State Logic

useReducer is an alternative to useState for state that involves multiple sub-values or when the next state depends on the previous state in complex ways.

import { useReducer } from 'react';

const initialState = { count: 0, step: 1 };

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { ...state, count: state.count + state.step };
    case 'decrement':
      return { ...state, count: state.count - state.step };
    case 'setStep':
      return { ...state, step: action.payload };
    case 'reset':
      return initialState;
    default:
      throw new Error(`Unknown action: ${action.type}`);
  }
}

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

  return (
    <div>
      <p>Count: {state.count} (step: {state.step})</p>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
      <input
        type="number"
        value={state.step}
        onChange={e => dispatch({ type: 'setStep', payload: +e.target.value })}
      />
    </div>
  );
}

A practical rule: if your useState updater functions are getting complex or you have multiple related state variables that change together, switch to useReducer. The reducer centralizes state transitions in one testable function.

useRef: Refs and Mutable Values

useRef creates a mutable object whose .current property persists across renders without triggering re-renders when changed.

DOM References

import { useRef } from 'react';

function SearchInput() {
  const inputRef = useRef(null);

  function handleClick() {
    inputRef.current.focus();
  }

  return (
    <div>
      <input ref={inputRef} type="text" placeholder="Search..." />
      <button onClick={handleClick}>Focus Input</button>
    </div>
  );
}

Storing Previous Values

function usePrevious(value) {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
}

function PriceDisplay({ price }) {
  const prevPrice = usePrevious(price);
  const direction = price > prevPrice ? 'up' : price < prevPrice ? 'down' : '';
  return <span className={direction}>${price}</span>;
}

Storing Interval IDs

function Timer() {
  const [seconds, setSeconds] = useState(0);
  const intervalRef = useRef(null);

  function start() {
    if (intervalRef.current) return;
    intervalRef.current = setInterval(() => {
      setSeconds(prev => prev + 1);
    }, 1000);
  }

  function stop() {
    clearInterval(intervalRef.current);
    intervalRef.current = null;
  }

  useEffect(() => {
    return () => clearInterval(intervalRef.current);
  }, []);

  return (
    <div>
      <p>{seconds}s</p>
      <button onClick={start}>Start</button>
      <button onClick={stop}>Stop</button>
    </div>
  );
}

Performance Hooks: useMemo and useCallback

useMemo

useMemo caches the result of a computation. It recalculates only when its dependencies change.

import { useMemo } from 'react';

function ProductList({ products, filterTerm }) {
  const filtered = useMemo(() => {
    return products.filter(p =>
      p.name.toLowerCase().includes(filterTerm.toLowerCase())
    );
  }, [products, filterTerm]);

  return (
    <ul>
      {filtered.map(p => <li key={p.id}>{p.name}</li>)}
    </ul>
  );
}

useCallback

useCallback caches a function definition. It returns the same function reference as long as the dependencies have not changed. This is useful when passing callbacks to memoized child components.

import { useCallback, memo } from 'react';

const ExpensiveChild = memo(function ExpensiveChild({ onClick, label }) {
  console.log(`Rendering: ${label}`);
  return <button onClick={onClick}>{label}</button>;
});

function Parent() {
  const [count, setCount] = useState(0);

  // Without useCallback, this creates a new function on every render,
  // causing ExpensiveChild to re-render every time
  const handleClick = useCallback(() => {
    console.log('Button clicked');
  }, []);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(c => c + 1)}>Increment</button>
      <ExpensiveChild onClick={handleClick} label="Action" />
    </div>
  );
}

Do not optimize prematurely. Both useMemo and useCallback add overhead. Use them when you measure a performance problem, not on every value and function. The React Compiler in React 19+ can auto-memoize in many cases, making manual memoization less necessary.

React 19 Hooks: use, useOptimistic, useFormStatus

use()

The use hook reads a resource (a Promise or a context) during render. Unlike useEffect, it integrates with Suspense for a declarative loading pattern.

import { use, Suspense } from 'react';

async function fetchUser(id) {
  const res = await fetch(`/api/users/${id}`);
  return res.json();
}

function UserProfile({ userPromise }) {
  const user = use(userPromise);
  return <h1>{user.name}</h1>;
}

function App() {
  const [userId, setUserId] = useState(1);
  const userPromise = fetchUser(userId);

  return (
    <Suspense fallback={<p>Loading...</p>}>
      <UserProfile userPromise={userPromise} />
    </Suspense>
  );
}

use() is unique among hooks because it can be called inside conditionals and loops. It suspends the component until the Promise resolves, and React shows the nearest Suspense fallback while waiting.

useOptimistic

Shows an optimistic UI update while an async action is in progress. If the action fails, React automatically reverts to the previous state.

import { useOptimistic } from 'react';

function TodoList({ todos, addTodoAction }) {
  const [optimisticTodos, addOptimistic] = useOptimistic(
    todos,
    (currentTodos, newTodo) => [...currentTodos, { ...newTodo, pending: true }]
  );

  async function handleSubmit(formData) {
    const title = formData.get('title');
    addOptimistic({ id: Date.now(), title });
    await addTodoAction(title);
  }

  return (
    <div>
      {optimisticTodos.map(todo => (
        <p key={todo.id} style={{ opacity: todo.pending ? 0.5 : 1 }}>
          {todo.title}
        </p>
      ))}
      <form action={handleSubmit}>
        <input name="title" />
        <button type="submit">Add</button>
      </form>
    </div>
  );
}

useFormStatus

Provides the status of a parent form to child components. This avoids prop drilling for form submission state.

import { useFormStatus } from 'react-dom';

function SubmitButton() {
  const { pending } = useFormStatus();
  return (
    <button type="submit" disabled={pending}>
      {pending ? 'Saving...' : 'Save'}
    </button>
  );
}

function ContactForm({ submitAction }) {
  return (
    <form action={submitAction}>
      <input name="email" type="email" required />
      <textarea name="message" required />
      <SubmitButton />
    </form>
  );
}

For validating your JSX output or converting between HTML and JSX during development, the HTML to JSX Converter handles the transformation instantly.

Building Custom Hooks

Custom hooks extract reusable logic into a function. The only rule: the function name must start with use.

useLocalStorage

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

  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);

  return [value, setValue];
}

// Usage
function Settings() {
  const [theme, setTheme] = useLocalStorage('theme', 'dark');
  return <button onClick={() => setTheme(t => t === 'dark' ? 'light' : 'dark')}>{theme}</button>;
}

useDebounce

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

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

  return debounced;
}

// Usage: debounce a search query
function Search() {
  const [query, setQuery] = useState('');
  const debouncedQuery = useDebounce(query, 300);

  useEffect(() => {
    if (debouncedQuery) {
      fetch(`/api/search?q=${debouncedQuery}`).then(/* ... */);
    }
  }, [debouncedQuery]);

  return <input value={query} onChange={e => setQuery(e.target.value)} />;
}

useMediaQuery

function useMediaQuery(query) {
  const [matches, setMatches] = useState(
    () => window.matchMedia(query).matches
  );

  useEffect(() => {
    const mql = window.matchMedia(query);
    const handler = (e) => setMatches(e.matches);

    mql.addEventListener('change', handler);
    return () => mql.removeEventListener('change', handler);
  }, [query]);

  return matches;
}

// Usage
function Layout() {
  const isMobile = useMediaQuery('(max-width: 768px)');
  return isMobile ? <MobileNav /> : <DesktopNav />;
}

Common Mistakes and How to Fix Them

1. Missing Dependencies in useEffect

// Bug: stale closure, count is always 0
useEffect(() => {
  const id = setInterval(() => setCount(count + 1), 1000);
  return () => clearInterval(id);
}, []);

// Fix: functional updater
useEffect(() => {
  const id = setInterval(() => setCount(c => c + 1), 1000);
  return () => clearInterval(id);
}, []);

2. Creating Objects in the Dependency Array

// Bug: options is a new object every render, effect runs infinitely
function App() {
  const options = { method: 'GET' };

  useEffect(() => {
    fetch('/api/data', options);
  }, [options]); // new reference every render!

  // Fix: useMemo or move inside effect
  const options = useMemo(() => ({ method: 'GET' }), []);
}

3. Mutating State Directly

// Bug: mutation, React doesn't detect the change
const [items, setItems] = useState([1, 2, 3]);
items.push(4);
setItems(items); // same reference, no re-render

// Fix: create a new array
setItems(prev => [...prev, 4]);

4. Forgetting Cleanup in useEffect

// Bug: memory leak if component unmounts during fetch
useEffect(() => {
  fetch('/api/data').then(r => r.json()).then(setData);
}, []);

// Fix: abort controller
useEffect(() => {
  const controller = new AbortController();
  fetch('/api/data', { signal: controller.signal })
    .then(r => r.json())
    .then(setData)
    .catch(() => {});
  return () => controller.abort();
}, []);

5. Using useEffect for Derived State

// Anti-pattern: useEffect to sync derived state
const [firstName, setFirstName] = useState('Jane');
const [lastName, setLastName] = useState('Doe');
const [fullName, setFullName] = useState('');

useEffect(() => {
  setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);

// Fix: just compute it during render
const fullName = `${firstName} ${lastName}`;
// Or useMemo if the computation is expensive
const fullName = useMemo(() => `${firstName} ${lastName}`, [firstName, lastName]);

The ESLint Config Generator can scaffold an ESLint configuration with the React hooks plugin enabled, catching many of these mistakes at development time.

Related Developer Tools

Free browser-based tools to complement your React development workflow.


Frequently Asked Questions

React Hooks are functions that let you use state, lifecycle behavior, context, and other React features inside function components without writing classes. Introduced in React 16.8, hooks are now the standard way to build React applications. They simplify code by eliminating class boilerplate, make logic reusable through custom hooks, and compose better than higher-order components or render props. As of 2026, the React team recommends function components with hooks for all new code, and class components are considered legacy.

useMemo caches the result of a computation: useMemo(() => expensiveCalc(a, b), [a, b]) returns the cached value and only recalculates when a or b changes. useCallback caches the function itself: useCallback((x) => x + a, [a]) returns the same function reference as long as a stays the same. In practice, useCallback(fn, deps) is equivalent to useMemo(() => fn, deps). Use useMemo for expensive calculations you want to skip on re-renders. Use useCallback for stable function references, typically when passing callbacks to child components wrapped in React.memo.

Infinite loops happen when your useEffect updates a value that is in its own dependency array. For example, if the effect calls setState(count + 1) and count is a dependency, the state change triggers a re-render, which triggers the effect again. To fix this: use the functional updater form setState(prev => prev + 1) so you do not need count in the dependency array; wrap objects and arrays with useMemo so they have stable references; use a ref instead of state if you do not need to trigger a re-render; and always specify the correct dependency array. The eslint-plugin-react-hooks catches most of these at development time.

Use useRef when you need to store a value that persists across renders but should not trigger a re-render when it changes. Common use cases include referencing DOM elements for focus or measurement, storing previous values, holding interval or timeout IDs, and tracking mounted state. Use useState when changing the value should update the UI. The key difference: updating a ref (ref.current = newValue) is silent, while calling a state setter (setValue(newValue)) triggers a re-render. If the screen needs to reflect the change, use state. If you just need to remember something, use a ref.

No. useCallback adds overhead: the hook call itself, the dependency comparison, and the cached reference. It only helps when the stable function reference prevents unnecessary work, specifically when the function is passed as a prop to a child wrapped in React.memo, or when it appears in the dependency array of another hook. If a function is only used inside the same component, useCallback adds complexity with no benefit. Write code without useCallback first, then add it where you measure a real performance problem. The React Compiler in React 19+ can automatically memoize in many cases, reducing the need for manual useCallback.

React 19 introduced several new hooks. use() reads resources like Promises and context during render, replacing useEffect for data fetching when used with Suspense. useFormStatus() gives child components access to the status of a parent form (pending, data, method, action) without prop drilling. useFormState() connects a form action to state that updates when the form is submitted. useOptimistic() lets you show an optimistic UI state while an async action is in progress, automatically reverting if it fails. React 19 also includes the React Compiler which can auto-memoize, reducing the need for manual useMemo and useCallback.

NT

Christian Bucher

We build free developer tools including JavaScript formatters, HTML-to-JSX converters, TypeScript playgrounds, and many more. All browser-based, no signup required.

269 Developer Tools, One Place

Browse 269 indexed tool pages with no QTool account required, and inspect the source on GitHub.

Open Source — Free Forever Try Free Tools

Related Tools

CSS Box Shadow Generator · Emoji Picker & Search · Free JSON to YAML Converter

Related Tools

Free JSON to YAML Converter · Free JSON Validator · Free Regex Playground

Related Articles

Built by Miguel

Need a custom tool or website?

From . Delivered in 24-48h. You own the code.

View Services →