intermediate frameworks React 19 · Updated April 2026

React Hooks Cheatsheet

Master React Hooks in React 19, including useState, useEffect, and new features like use(), useActionState, and useOptimistic for efficient component logic.

· 8 min read · AI-reviewed

Quick Overview

React Hooks are functions that let you “hook into” React state and lifecycle features from function components. Introduced in React 16.8, they enabled developers to write entire applications with functional components, avoiding the complexities of class components. React 19 refines these patterns and introduces powerful new hooks like use(), useActionState(), and useOptimistic() to simplify asynchronous operations, form handling, and optimistic UI updates, especially with React Server Components.

  • Current Stable Version: React 19
  • Install: Hooks are built into React. Upgrade your project to React 19.
    # For a new project (using Vite as an example)
    npm create vite@latest my-react-app -- --template react-ts
    cd my-react-app
    npm install react@latest react-dom@latest
    # Or for an existing project
    npm install react@latest react-dom@latest

Getting Started

To use React Hooks, you need a functional component. The simplest hook is useState, which allows you to add state to function components.

// src/App.jsx
import React, { useState } from 'react';

function Counter() {
  // Declare a new state variable, which we'll call "count"
  // setCount is the function to update count
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      {/* Update state using setCount */}
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

export default Counter;

Core Concepts

Hooks fundamentally change how you build React components by enabling state and side effects within functions.

ConceptDescription
Functional ComponentsHooks allow you to use state and other React features without writing a class. Your components become plain JavaScript functions that receive props and return JSX.
Rules of Hooks1. Only call Hooks at the Top Level: Don’t call Hooks inside loops, conditions, or nested functions. 2. Only call Hooks from React Function Components: Don’t call Hooks from regular JavaScript functions. (The only other valid place is your own custom Hooks).
Mental ModelReact runs your function components from top to bottom on every render. Hooks provide a way to “hook into” this rendering process to preserve state and perform effects across renders.
Custom HooksA JavaScript function whose name starts with “use” and that calls other Hooks. They allow you to reuse stateful logic between different components, promoting modularity and separation of concerns.
Dependencies ArrayUsed in hooks like useEffect, useCallback, and useMemo to specify values that the hook depends on. The hook will only re-run or re-compute if these dependencies change, preventing unnecessary re-renders or effects.

Essential Hooks

Organized by common tasks.

State Management

Manage component-specific data that triggers re-renders when updated.

useState(initialState)

Adds state to functional components. Returns a stateful value and a function to update it.

import React, { useState } from 'react';

function ToggleButton() {
  // Declare a boolean state, initial value is false
  const [isOn, setIsOn] = useState(false);

  // Toggle the state on click
  const handleClick = () => setIsOn(!isOn);

  return (
    <button onClick={handleClick}>
      {isOn ? 'ON' : 'OFF'}
    </button>
  );
}

useReducer(reducer, initialState)

An alternative to useState for more complex state logic or when the next state depends on the previous one.

import React, { useReducer } from 'react';

// Reducer function: defines how state changes
function counterReducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    case 'reset':
      return { count: 0 };
    default:
      return state;
  }
}

function ComplexCounter() {
  // useReducer returns current state and a dispatch function
  const [state, dispatch] = useReducer(counterReducer, { count: 0 });

  return (
    <div>
      <p>Count: {state.count}</p>
      {/* Dispatch actions to update state */}
      <button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>Decrement</button>
      <button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
    </div>
  );
}

Simplifies form state management, especially with asynchronous form submissions. It handles loading states and errors automatically.

import { useActionState } from 'react';

async function updateName(previousState, formData) {
  // Simulate an API call
  await new Promise(resolve => setTimeout(resolve, 1000));
  const newName = formData.get('name');
  if (newName.length < 3) {
    return { error: 'Name must be at least 3 characters' };
  }
  return { name: newName, error: null };
}

function NameForm() {
  // useActionState manages state, dispatch function, and pending status
  const [state, dispatch, isPending] = useActionState(updateName, { name: 'Guest', error: null });

  return (
    <form action={dispatch}>
      <p>Current Name: {state.name}</p>
      <input type="text" name="name" defaultValue={state.name} disabled={isPending} />
      <button type="submit" disabled={isPending}>
        {isPending ? 'Updating...' : 'Update Name'}
      </button>
      {state.error && <p style={{ color: 'red' }}>{state.error}</p>}
    </form>
  );
}

Side Effects & Lifecycle

Perform actions that interact with the outside world (DOM, API calls, subscriptions).

useEffect(setup, dependencies?)

Runs side effects after every render where specified dependencies have changed. Returns an optional cleanup function.

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

function DataFetcher() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  // Effect to fetch data when the component mounts
  useEffect(() => {
    async function fetchData() {
      try {
        const response = await fetch('https://api.example.com/items');
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        const result = await response.json();
        setData(result);
      } catch (err) {
        setError(err);
      } finally {
        setLoading(false);
      }
    }

    fetchData();

    // Optional cleanup function (e.g., to cancel ongoing requests)
    return () => {
      // abortController.abort(); // if using AbortController
    };
  }, []); // Empty dependency array means this effect runs once after the initial render

  if (loading) return <p>Loading data...</p>;
  if (error) return <p>Error: {error.message}</p>;

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

Context & Data Consumption

Pass data deep down the component tree without prop drilling.

useContext(MyContext)

Reads the current context value. Component will re-render if the context value changes.

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

// 1. Create a Context
const ThemeContext = createContext(null);

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  const toggleTheme = () => setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light'));

  return (
    // 2. Provide the context value
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

function ThemeToggler() {
  // 3. Consume the context value
  const { theme, toggleTheme } = useContext(ThemeContext);

  return (
    <button onClick={toggleTheme} style={{ background: theme === 'dark' ? 'black' : 'white', color: theme === 'dark' ? 'white' : 'black' }}>
      Switch to {theme === 'light' ? 'Dark' : 'Light'} Mode
    </button>
  );
}

// Usage:
// <ThemeProvider>
//   <ThemeToggler />
// </ThemeProvider>

use(promise | context) (New in React 19)

A powerful new hook to read values from resources like Promises or Context directly in components, even conditionally. It integrates seamlessly with Suspense and Error Boundaries for asynchronous data.

import React, { use, Suspense, createContext } from 'react';

// Example with a Promise
async function fetchUser(id) {
  const response = await fetch(`https://api.example.com/users/${id}`);
  return response.json();
}

function UserProfile({ userId }) {
  // Directly consume the promise, Suspense handles the loading state
  const user = use(fetchUser(userId)); //
  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  );
}

// Example with Context
const MyContext = createContext('Default Value');

function MyComponent() {
  // Directly consume context, can be used conditionally unlike useContext
  const value = use(MyContext);
  return <p>Context value: {value}</p>;
}

function App() {
  return (
    <>
      <Suspense fallback={<div>Loading user...</div>}>
        <UserProfile userId="123" />
      </Suspense>
      <MyContext.Provider value="Provided Value">
        <MyComponent />
      </MyContext.Provider>
    </>
  );
}

Performance Optimization

Memoize expensive computations or functions to prevent unnecessary re-renders.

useCallback(fn, dependencies)

Returns a memoized callback function. React will only re-create the function if one of the dependencies has changed. Useful for passing callbacks to optimized child components.

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

function Button({ onClick, children }) {
  console.log('Button rendered', children);
  return <button onClick={onClick}>{children}</button>;
}

function ParentComponent() {
  const [count, setCount] = useState(0);
  const [otherState, setOtherState] = useState(0);

  // Memoized callback: only re-creates if 'count' changes
  const handleClick = useCallback(() => {
    setCount(prevCount => prevCount + 1);
  }, []); // Empty array means it's created once

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setOtherState(otherState + 1)}>
        Update Other State ({otherState})
      </button>
      {/* Button component won't re-render if only otherState changes */}
      <Button onClick={handleClick}>Increment Count</Button>
    </div>
  );
}

useMemo(factory, dependencies)

Returns a memoized value. It will only re-compute the memoized value when one of the dependencies has changed.

import React, { useState, useMemo } from 'react';

function ExpensiveCalculation({ num }) {
  // Simulate an expensive calculation
  const calculatedValue = useMemo(() => {
    console.log('Calculating expensive value...');
    let result = 0;
    for (let i = 0; i < num * 1000000; i++) {
      result += i;
    }
    return result;
  }, [num]); // Re-calculate only when 'num' changes

  return (
    <div>
      <p>Number: {num}</p>
      <p>Expensive Result: {calculatedValue}</p>
    </div>
  );
}

function AppWithMemo() {
  const [count, setCount] = useState(1);
  const [multiplier, setMultiplier] = useState(1);

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Increment Count</button>
      <button onClick={() => setMultiplier(multiplier + 1)}>Increment Multiplier</button>
      <ExpensiveCalculation num={count * multiplier} />
    </div>
  );
}

Refs

Access DOM elements or persist mutable values across renders without causing re-renders.

useRef(initialValue)

Returns a mutable ref object whose .current property is initialized to the passed argument. The returned object will persist for the full lifetime of the component.

import React, { useRef } from 'react';

function TextInputWithFocusButton() {
  // Create a ref to store a DOM element reference
  const inputEl = useRef(null);

  const onButtonClick = () => {
    // `current` points to the mounted text input element
    inputEl.current.focus();
  };

  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}

UI Feedback & Optimistic Updates

Enhance user experience by providing immediate feedback.

useOptimistic(state, updater) (New in React 19)

Allows you to show an immediate, “optimistic” UI update while an asynchronous background operation (e.g., a network request) completes.

import { useOptimistic } from 'react';

async function deliverMessage(message) {
  // Simulate network delay
  await new Promise(res => setTimeout(res, 1000));
  return `Delivered: ${message}`;
}

function Chat({ messages, sendMessage }) {
  // optimisticMessages initially mirrors 'messages', but updates instantly
  const [optimisticMessages, addOptimisticMessage] = useOptimistic(
    messages,
    (currentMessages, newMessage) => [...currentMessages, { text: newMessage, sending: true }]
  );

  const formAction = async (formData) => {
    const message = formData.get('message');
    // Immediately show the message as sending
    addOptimisticMessage(message);
    // Send the actual message (async operation)
    await deliverMessage(message);
    // After delivery, the original 'messages' prop will re-render,
    // and optimisticMessages will re-sync.
    sendMessage(message); // Propagates to parent to update actual messages
  };

  return (
    <form action={formAction}>
      {optimisticMessages.map((msg, index) => (
        <div key={index} style={{ opacity: msg.sending ? 0.5 : 1 }}>
          {msg.text} {msg.sending && '(Sending...)'}
        </div>
      ))}
      <input type="text" name="message" />
      <button type="submit">Send</button>
    </form>
  );
}

useFormStatus() (New in React 19)

Provides status information of the nearest <form> element to its nested components, without prop drilling.

import { useFormStatus } from 'react-dom'; // Note: useFormStatus is from react-dom

function SubmitButton() {
  // Access form status from the nearest parent <form>
  const { pending } = useFormStatus();

  return (
    <button type="submit" disabled={pending}>
      {pending ? 'Submitting...' : 'Submit'}
    </button>
  );
}

function UserSettingsForm() {
  const handleSubmit = async (formData) => {
    // Simulate API call
    await new Promise(resolve => setTimeout(resolve, 2000));
    alert(`Form submitted with name: ${formData.get('username')}`);
  };

  return (
    <form action={handleSubmit}>
      <label>
        Username:
        <input type="text" name="username" defaultValue="John Doe" />
      </label>
      <SubmitButton /> {/* This button automatically knows its form's pending state */}
    </form>
  );
}

Common Patterns

Custom Hooks for Reusability

Extracting stateful logic into reusable functions.

import { useState, useEffect } from 'react';

// Custom Hook to manage local storage
function useLocalStorage(key, initialValue) {
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.log(error);
      return initialValue;
    }
  });

  // Update local storage when the state changes
  useEffect(() => {
    window.localStorage.setItem(key, JSON.stringify(storedValue));
  }, [key, storedValue]);

  return [storedValue, setStoredValue];
}

function Settings() {
  // Use the custom hook like a built-in one
  const [username, setUsername] = useLocalStorage('username', 'Guest');
  const [theme, setTheme] = useLocalStorage('theme', 'light');

  return (
    <div>
      <label>
        Username:
        <input
          type="text"
          value={username}
          onChange={(e) => setUsername(e.target.value)}
        />
      </label>
      <p>Current Theme: {theme}</p>
      <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
        Toggle Theme
      </button>
    </div>
  );
}

Data Fetching with use() and Suspense (React 19)

Modern approach for async data fetching, especially in Server Components.

import React, { use, Suspense } from 'react';

async function fetchProducts() {
  // In a real app, this might be a server-side fetch
  const response = await fetch('https://api.example.com/products');
  return response.json();
}

function ProductList() {
  // `use` hook makes data fetching synchronous-looking
  const products = use(fetchProducts());

  return (
    <ul>
      {products.map(product => (
        <li key={product.id}>{product.name} - ${product.price}</li>
      ))}
    </ul>
  );
}

function AppWithProducts() {
  return (
    <Suspense fallback={<div>Loading products...</div>}>
      <ProductList />
    </Suspense>
  );
}

Gotchas & Tips

  • Don’t Break the Rules of Hooks: Always call hooks at the top level of your component and only from React function components or custom hooks. Violating this leads to subtle bugs and unpredictable behavior.
  • Stale Closures: Be mindful of closures, especially in useEffect. If your effect uses variables from the component’s scope, they should be in the dependency array. Forgetting them can lead to the effect “seeing” outdated values.
    // BAD: counter might be stale
    useEffect(() => {
      const id = setInterval(() => {
        // 'count' here might be the initial 0 if not in dependencies
        console.log(count);
      }, 1000);
      return () => clearInterval(id);
    }, []); // Missing 'count'
    
    // GOOD: 'count' is up-to-date, or use functional update
    useEffect(() => {
      const id = setInterval(() => {
        setCount(prevCount => prevCount + 1); // Functional update avoids 'count' dependency
      }, 1000);
      return () => clearInterval(id);
    }, []); // Correct as setCount from useState is stable
  • Dependency Array Completeness: Ensure your useEffect, useCallback, useMemo dependency arrays include all values from the component scope that the hook uses. If the linter suggests it, include it.
  • Infinite useEffect Loops: Setting state inside useEffect without proper dependencies can cause an infinite re-render loop. For example, if useEffect sets state, that state change causes a re-render, which re-runs useEffect, and so on.
    // BAD
    useEffect(() => {
      setCount(count + 1); // This will cause an infinite loop
    }, [count]); // 'count' changes, effect runs, changes 'count' again
  • State Updates are Asynchronous: useState’s setter function (setCount) does not immediately update the state value (count). If you need to perform an action after state has definitely updated, use useEffect or the functional update form of the setter.
  • Avoid Over-Memoizing: Don’t wrap every function or value in useCallback or useMemo. Memoization has a cost. Use it when you observe performance issues or when passing props to React.memo wrapped child components.
  • Cleanup Functions in useEffect: If your useEffect subscribes to something (like an event listener, a timer, or an external data source), return a cleanup function to unsubscribe or clear it. This prevents memory leaks.
  • use() in Client Components: While use() is powerful for promises and context, especially with Server Components, remember its current limitations and best practices. Always wrap components using use(Promise) in a <Suspense> boundary.

Next Steps

  • Official React Docs: The definitive guide for all React concepts, including in-depth explanations of Hooks. https://react.dev/
  • React use Hook RFC: For the latest on the use hook and its capabilities. https://github.com/reactjs/rfcs/blob/main/text/0213-use-hook.md
  • z2h.fyi/react-router-guide: Learn how to handle routing in your React applications.
  • z2h.fyi/react-query-guide: Explore advanced data fetching and caching with React Query.

Source: z2h.fyi/cheatsheets/react-hooks-cheatsheet — Zero to Hero cheatsheets for developers.