Skip to content

Hooks

Hooks are special functions that React gives you, which let function components do things like store data (state), run side effects, work with references, and reuse logic. Before hooks existed, you had to do most of this inside class components. Hooks made React code cleaner and easier to break into small, reusable pieces.

Before hooks came along:

  • Most of the state and lifecycle logic had to live inside class components.
  • Reusing logic through HOCs and render props often made the code hard to read.

Hooks made things better by making it easier to:

  • Use state inside function components.
  • Reuse logic by writing your own custom hooks.
  • Handle side effects without needing class lifecycle methods.
  • Keep related pieces of logic together in one place.

A hook is just a normal JavaScript function, but it has special behavior inside React. You can only call hooks inside React function components, or inside your own custom hooks.

const [count, setCount] = useState(0);
  1. Always call hooks at the top level of your component - not inside loops, conditions (like if statements), or nested functions.
  2. Only call hooks inside React function components or inside custom hooks.

React keeps track of hooks based on the order they are called in, on every single render. If that order changes between renders, React can end up connecting the wrong state to the wrong hook.

graph TD R1["Render 1"] --> H1["useState"] H1 --> H2["useEffect"] H2 --> H3["useState"] R2["Render 2"] --> S1["useState"] S1 --> S2["useEffect"] S2 --> S3["useState"] R1 -. "same order required" .-> R2

On every render, React runs your component function from top to bottom, step by step.

  1. Your component function runs.
  2. Hooks run one after another, in the same order every time.
  3. React looks at the values it stored from the last render.
  4. React gives back the updated UI.
graph TD A["Render Starts"] --> B["Run Component Function"] B --> C["Run Hooks in Same Order"] C --> D["Build New UI"] D --> E["Commit to DOM"]

useState is used to store data (state) that belongs to a single component.

const [state, setState] = useState(initialValue);
  • Form fields.
  • Toggle states, like open, loading, or error.
  • Counters and other small, local pieces of UI state.
  • Very complex state changes with many different action types - in that case, useReducer is a better fit.
  • State that needs to be shared across many components that are far apart in your app - in that case, use context or a state management library instead.
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount((c) => c + 1)}>Count: {count}</button>;
}
graph TD A["setState()"] --> B["Update queued"] B --> C["Component re-renders"] C --> D["New value shown in UI"]

useEffect lets you run side-effect code after React has updated the screen.

  • Fetching API data.
  • Setting up subscriptions.
  • Timers and intervals.
  • Syncing with browser APIs.
  • Simple calculations based on props or state - just calculate them directly while rendering, or use useMemo if the calculation is expensive.
  • Small events that an event handler could already take care of - you don’t need useEffect for those.
Dependency arrayBehavior
[]Runs once after mount
[x]Runs when x changes
no arrayRuns after every render
import { useEffect, useState } from "react";
function Clock() {
const [time, setTime] = useState(new Date());
useEffect(() => {
const id = setInterval(() => setTime(new Date()), 1000);
return () => {
clearInterval(id);
};
}, []);
return <p>{time.toLocaleTimeString()}</p>;
}
graph LR A["Render phase"] --> B["React notes effects"] B --> C["Commit phase"] C --> D["React runs effects"]

useRef stores a value that can change, and that value stays the same across re-renders - but changing it does not cause the component to re-render.

  • Focus an input.
  • Store previous values.
  • Keep IDs or timers between renders.
  • Access DOM nodes directly when needed.
  • Values that should update the UI whenever they change - for that, use state instead.
Changing ref.current does NOT re-render
import { useRef } from "react";
function FocusField() {
const inputRef = useRef(null);
return (
<>
<input ref={inputRef} />
<button onClick={() => inputRef.current?.focus()}>Focus</button>
</>
);
}

This example combines controlled state with useRef to build a real-world form flow - it auto-focuses fields, resets the form, and focuses the field that has a validation error.

import { FormEvent, useRef, useState } from "react";
export default function SignupForm() {
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [error, setError] = useState("");
const nameRef = useRef<HTMLInputElement | null>(null);
const emailRef = useRef<HTMLInputElement | null>(null);
function handleSubmit(e: FormEvent) {
e.preventDefault();
setError("");
if (!name.trim()) {
setError("Name is required");
nameRef.current?.focus();
return;
}
if (!email.includes("@")) {
setError("Enter a valid email");
emailRef.current?.focus();
return;
}
console.log("Submitted:", { name, email });
setName("");
setEmail("");
nameRef.current?.focus();
}
return (
<form onSubmit={handleSubmit}>
<input ref={nameRef} value={name} onChange={(e) => setName(e.target.value)} placeholder="Name" />
<input ref={emailRef} value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Email" />
<button type="submit">Create Account</button>
{error && <p>{error}</p>}
</form>
);
}

useMemo saves (caches) the result of a calculation that takes a lot of effort to run, so React doesn’t have to redo it every time.

  • Heavy filtering, sorting, or other calculations that run often.
  • Values that are calculated from other data, when that calculation is expensive and depends on specific values.
  • Small cheap calculations.
  • Don’t use it just out of habit for every single value - only use it when it’s actually needed.
import { useMemo } from "react";
const visibleUsers = useMemo(() => {
return users.filter((u) => u.active).sort((a, b) => a.name.localeCompare(b.name));
}, [users]);
graph TD A["Check dependencies"] --> B{"Changed?"} B -- "No" --> C["Return cached value"] B -- "Yes" --> D["Recalculate value"]

useCallback saves (caches) a function so it doesn’t get recreated on every render.

  • When you’re passing a function as a prop to a memoized child component.
  • When you need the function to stay the same between renders, for example inside a dependency array.
  • If the child component isn’t memoized, and there’s no real performance issue to begin with.
  • Don’t wrap every single function in useCallback just by default.
import { useCallback } from "react";
const handleClick = useCallback(() => {
console.log("Click");
}, []);
graph LR A["useMemo"] --> B["Memoizes value"] C["useCallback"] --> D["Memoizes function"]

useReducer is useful when your state logic is complex, or when you have many related pieces of state that update together.

const [state, dispatch] = useReducer(reducer, initialState);
  • Complex forms.
  • Multiple related state values.
  • Updates that are easier to understand when described as clear, named actions.
import { useReducer } from "react";
type State = { count: number };
type Action = { type: "increment" } | { type: "decrement" } | { type: "reset" };
function reducer(state: State, action: Action): State {
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;
}
}
export default function CounterReducer() {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<div>
<p>{state.count}</p>
<button onClick={() => dispatch({ type: "increment" })}>+</button>
<button onClick={() => dispatch({ type: "decrement" })}>-</button>
<button onClick={() => dispatch({ type: "reset" })}>Reset</button>
</div>
);
}
graph TD A["dispatch(action)"] --> B["Reducer runs"] B --> C["New state returned"] C --> D["Component re-renders"]

useLayoutEffect runs right after React updates the DOM, but before the browser actually paints anything on the screen.

  • Measuring the size or position of elements before the browser paints them.
  • Avoiding a visual flicker in certain UI calculations.
  • Regular API calls and most other side effects - use useEffect for those instead.
useEffect -> after paint
useLayoutEffect -> before paint

A custom hook is just a function that uses other hooks inside it, so you can reuse the same logic across multiple components.

The function name must start with use.

import { useState } from "react";
function useCounter(initial) {
const [count, setCount] = useState(initial);
function increment() {
setCount((c) => c + 1);
}
return { count, increment };
}
function App() {
const { count, increment } = useCounter(0);
return <button onClick={increment}>{count}</button>;
}
  • Reuse logic.
  • Avoid duplication.
  • Keep components smaller and cleaner.

Every component has some internal data that React keeps track of, called a Fiber. Your hook values are stored there, in the same order you called them.

graph LR F["fiber.memoizedState"] --> H1["Hook 1"] H1 --> H2["Hook 2"] H2 --> H3["Hook 3"] H3 --> H4["..."]

Each hook node stores data like:

{
memoizedState,
queue,
next
}
graph LR A["Fiber Node"] --> B["Hook 1: useState"] B --> C["Hook 2: useEffect"] C --> D["Hook 3: useRef"]

Hook Inside Condition

Calling a hook inside an if block messes up the order hooks run in.

Missing Effect Dependency

If you forget a dependency, you can end up with old (stale) values and confusing bugs.

Missing Cleanup

Timers and listeners need to be cleaned up, or they can cause memory leaks.

Overusing Memo Hooks

Using useMemo and useCallback everywhere adds extra complexity, even when there’s no real benefit.

if (isOpen) {
useState(0); // Wrong
}

Quick Guide: Which Hook for Which Problem?

Section titled “Quick Guide: Which Hook for Which Problem?”
ProblemHook
Local value that changes UIuseState
Complex state transitionsuseReducer
Side effects / fetch / subscriptionsuseEffect
DOM access / mutable boxuseRef
Expensive computed valueuseMemo
Stable callback functionuseCallback
Measure layout before paintuseLayoutEffect
graph LR A["Render"] --> B["Commit"] B --> C["Effect"]

Keep these simple ideas in mind:

Hooks = ordered state slots for a component
React stores hook values outside your function, then gives them back on next render