❤ Like
🔖 Save
🔗 Share
Eric Hu
Eric Hu

React Hooks

@thedevspaceio

⚛️ 18 Built-in Hooks Every React Developer Should Know

There are 18 hooks built into React. Some you'll use every day. Others you'll reach for when you need them.

This cheatsheet gives an overview of each of them.

✅ useState ✅ useReducer ✅ useActionState ✅ useOptimistic ✅ useEffect ✅ useLayoutEffect ✅ useInsertionEffect ✅ useEffectEvent ✅ useMemo ✅ useCallback ✅ useDeferredValue ✅ useTransition ✅ useContext ✅ useRef ✅ useImperativeHandle ✅ useId ✅ useSyncExternalStore ✅ useDebugValue

#react #hooks #usestate #useeffect #usecontext #useref #usememo #usecallback #webdev #frontend #coding


Hooks are functions that let you tap into the power of React to manage state, side effects, references, and more.

This cheatsheet provides a quick reference to every built-in React hook.


HookDescription
useStateAdd local state to a component.
useReducerManage complex state logic with a reducer function.
useActionStateTrack the pending state and result of a form action.
useOptimisticShow a temporary result while an async action completes.
useEffectSynchronize with external systems after rendering.
useLayoutEffectRun effects before the browser paints.
useInsertionEffectInsert CSS-in-JS styles before layout effects run.
useEffectEventRead the latest values inside an effect without re-running it.
useMemoCache the result of an expensive computation.

HookDescription
useCallbackCache a function definition between renders.
useDeferredValueDefer updating a value to keep the UI responsive.
useTransitionMark state updates as non-blocking transitions.
useContextRead a context value without a Consumer.
useRefReference a DOM node or persist a mutable value.
useImperativeHandleCustomize the ref exposed to parent components.
useIdGenerate a unique ID for accessibility attributes.
useSyncExternalStoreSubscribe to an external data store.
useDebugValueAdd a label to custom hooks in React DevTools.

useState

Add local state to a component.

jsx
const [count, setCount] = useState(0);
 
setCount(count + 1);
setCount((prev) => prev + 1); // functional update

Functional updates are useful when the new state depends on the previous state.


useReducer

Manage complex state logic with a reducer function.

jsx
import { useReducer } from "react";
 
// Reducer function
const counterReducer = (state, action) => {
  switch (action.type) {
    case "INCREMENT":
      return { ...state, count: state.count + 1 };
    case "DECREMENT":
      return { ...state, count: state.count - 1 };
    case "RESET":
      return { ...state, count: 0 };
    default:
      return state;
  }
};

js
// continued...
 
// Initial state
const initialState = { count: 0, lastAction: null };
 
function Counter() {
  const [state, dispatch] = useReducer(counterReducer, initialState);
 
  return (
    <div>
      <h2>Count: {state.count}</h2>
      <button onClick={() => dispatch({ type: "INCREMENT" })}>+1</button>
      <button onClick={() => dispatch({ type: "DECREMENT" })}>-1</button>
      <button onClick={() => dispatch({ type: "RESET" })}>Reset</button>
    </div>
  );
}

useActionState

Track a form action's result and pending state.

jsx
const [state, formAction, isPending] = useActionState(submitForm, null);
 
<form action={formAction}>...</form>;

state will be null while the action is pending, and will update to the result when it completes.

formAction is a function that can be passed to a form's action prop to handle the submission.

isPending is a boolean that indicates whether the action is currently in progress.


useOptimistic

Enables optimistic UI updates, showing the expected result immediately while the actual async operation completes in the background.

jsx
import { useOptimistic, useState } from "react";
 
function TodoList() {
  const [todos, setTodos] = useState([]);
 
  // Optimistic state - updates immediately, then syncs with real state
  const [optimisticTodos, addOptimisticTodo] = useOptimistic(
    todos,
    (current, newTodo) => [...current, newTodo],
  );
 
  const [input, setInput] = useState("");

js
// continued...
 
const handleSubmit = async (e) => {
  e.preventDefault();
  if (!input.trim()) return;
 
  const newTodo = { id: Date.now(), text: input, completed: false };
 
  // UI updates instantly
  addOptimisticTodo(newTodo);
  setInput("");
 
  // Simulate API call
  await new Promise((resolve) => setTimeout(resolve, 1000));
 
  // Real state updates after API completes
  setTodos((prev) => [...prev, newTodo]);
};

js
// continued...
 
  return (
    <div>
      <h2>Todo List</h2>
 
      <form onSubmit={handleSubmit}>
        <input
          value={input}
          onChange={(e) => setInput(e.target.value)}
          placeholder="Add todo..."
        />
        <button type="submit">Add</button>
      </form>
 
      <ul>
        {optimisticTodos.map((todo) => (
          <li key={todo.id}>{todo.text}</li>
        ))}
      </ul>
    </div>
  );
}

When the submit button is clicked, the new todo appears immediately in the list (optimistic update), and after the API call, the real state is updated to reflect the change.


useEffect

Synchronize with external systems after rendering, such as subscriptions, timers, or manually changing the DOM.

jsx
useEffect(() => {
  const subscription = subscribe();
  return () => subscription.unsubscribe(); // cleanup
}, []);

useLayoutEffect

Runs synchronously after React updates the DOM but before the browser paints to the screen. It's perfect for DOM measurements and mutations that need to happen before the user sees the update.

jsx
useLayoutEffect(() => {
  const { height } = ref.current.getBoundingClientRect();
}, []);

useInsertionEffect

Insert CSS-in-JS styles before layout effects run. For styling libraries only.

jsx
useInsertionEffect(() => {
  // insert <style> tag
}, []);

useEffectEvent

Read the latest props and states inside an effect without causing the effect to re-run when those values change.

jsx
const onMessage = useEffectEvent((msg) => {
  showNotification(msg, theme); // always the latest theme
});
 
useEffect(() => {
  chat.on("message", onMessage);
}, []); // does not re-run when theme changes

useMemo

Cache the result of an expensive computation. No longer necessary if you're using the React Compiler.

jsx
const total = useMemo(
  () => items.reduce((sum, item) => sum + item.price, 0),
  [items],
);

useCallback

Cache a function definition between renders. No longer necessary if you're using the React Compiler.

jsx
const increment = useCallback(() => setCount((c) => c + 1), []);

useDeferredValue

Defer updating a value to keep the UI responsive.

jsx
import { useState, useDeferredValue, useMemo } from "react";
 
function SearchApp() {
  const [query, setQuery] = useState("");
  const deferredQuery = useDeferredValue(query);
 
  // Expensive filtering - only runs when deferredQuery changes
  const filteredItems = () => {
    const items = Array.from({ length: 10000 }, (_, i) => ({
      id: i,
      name: `Item ${i}`,
    }));
 
    if (!deferredQuery) return items;
 
    return items.filter((item) =>
      item.name.toLowerCase().includes(deferredQuery.toLowerCase()),
    );
  };

js
// continued...
 
  return (
    <div>
      <h2>Search</h2>
 
      <input
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search items..."
        style={{ width: "100%", padding: "10px" }}
      />
 
      <p>Found: {filteredItems.length} items</p>
 
      <div style={{ maxHeight: "300px", overflowY: "auto" }}>
        {filteredItems.slice(0, 20).map((item) => (
          <div key={item.id}>{item.name}</div>
        ))}
      </div>
    </div>
  );
}

During the initial render, deferredQuery will be the same as query.

As the user types in the search box, deferredQuery will lag behind query, allowing the UI to remain responsive while the expensive filtering operation runs.


useTransition

Mark state updates as non-blocking transitions.

jsx
const [isPending, startTransition] = useTransition();
const [tab, setTab] = useState("about");
 
function selectTab(nextTab) {
  startTransition(() => {
    setTab(nextTab);
  });
}
 
// ...

useContext

Read a context value, eliminating prop drilling and providing a clean way to share data across components.

jsx
import { useState, useContext, createContext } from "react";
 
// 1. Create context
const ThemeContext = createContext();
 
// 2. Provider component
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState("light");
 
  const toggleTheme = () => {
    setTheme((prev) => (prev === "light" ? "dark" : "light"));
  };
 
  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

js
// continued...
 
// 3. Consumer component using useContext
function ThemedButton() {
  const { theme, toggleTheme } = useContext(ThemeContext);
 
  return (
    <button
      onClick={toggleTheme}
      style={{
        background: theme === "light" ? "#fff" : "#333",
        color: theme === "light" ? "#333" : "#fff",
        padding: "10px 20px",
        border: `2px solid ${theme === "light" ? "#333" : "#fff"}`,
        borderRadius: "4px",
        cursor: "pointer",
      }}>
      Theme: {theme}
    </button>
  );
}

js
// continued...
 
// 4. App component with Provider
function App() {
  return (
    <ThemeProvider>
      <div style={{ padding: "20px" }}>
        <h2>Theme Context</h2>
        <ThemedButton />
      </div>
    </ThemeProvider>
  );
}
 
export default App;

useRef

Reference a DOM node or persist a mutable value without re-rendering.

jsx
import { useRef } from "react";
 
function App() {
  const inputRef = useRef(null);
 
  const focusInput = () => {
    inputRef.current.focus();
  };
 
  return (
    <div style={{ padding: "20px" }}>
      <input ref={inputRef} type="text" />
 
      <button onClick={focusInput} style={{ padding: "8px 16px" }}>
        Focus Input
      </button>
    </div>
  );
}
 
export default App;

useImperativeHandle

Customizes the instance value that is exposed when a parent component uses ref.

It's used with forwardRef to control what methods and properties are accessible to parent components.

jsx
import { useRef, useImperativeHandle, forwardRef } from "react";
 
const CustomInput = forwardRef((props, ref) => {
  const inputRef = useRef(null);
 
  // Expose custom methods to parent
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    },
    clear: () => {
      inputRef.current.value = "";
    },
  }));
 
  return <input ref={inputRef} type="text" />;
});

js
function App() {
  const customInputRef = useRef(null);
 
  const focusInput = () => {
    customInputRef.current.focus();
  };
 
  const clearInput = () => {
    customInputRef.current.clear();
  };
 
  return (
    <div style={{ padding: "20px" }}>
      <CustomInput ref={customInputRef} />
 
      <button onClick={focusInput} style={{ padding: "8px 16px" }}>
        Focus Input
      </button>
      <button onClick={clearInput} style={{ padding: "8px 16px" }}>
        Clear Input
      </button>
    </div>
  );
}
 
export default App;

useId

Generate a unique ID for accessibility attributes.

jsx
const id = useId();
 
<label htmlFor={id}>Name</label>
<input id={id} />

useSyncExternalStore

Subscribe to external data stores (like browser APIs, Redux, or custom stores) and get updates when they change.

jsx
const isOnline = useSyncExternalStore(subscribe, () => navigator.onLine);

useDebugValue

Add a label to custom hooks in React DevTools.

jsx
function useOnlineStatus() {
  const isOnline = useSyncExternalStore(subscribe, getSnapshot);
  useDebugValue(isOnline ? "Online" : "Offline");
  return isOnline;
}

Rules of Hooks

✅ Only call hooks at the top level of a component or custom hook.

✅ Only call hooks from React functions, not regular JavaScript functions.

✅ Call hooks in the same order on every render.

Full-Stack AI Developer Roadmap

From HTML & CSS to working with AI models, all in one structured roadmap.

@thedevspaceio
www.thedevspace.io