Skip to content

Hooks

Hooks are functions provided by React that let functional components use state, run side effects, work with references, and reuse logic. Before hooks, these features were mostly done in class components. Hooks made React code cleaner and easier to split into small reusable parts.

Before hooks:

  • State and lifecycle logic mostly lived in class components.
  • Logic reuse through HOCs and render props often became hard to read.

Hooks improved this by making it easier to:

  • Use state in function components.
  • Reuse logic with custom hooks.
  • Handle side effects without class lifecycle methods.
  • Keep related logic close together.

A hook is a normal JavaScript function with special behavior in React. You call hooks inside React function components or inside custom hooks.

const [count, setCount] = useState(0);
  1. Call hooks at the top level (not inside loops, conditions, or nested functions).
  2. Call hooks only in React function components or custom hooks.

React tracks hooks by call order on every render. If the order changes, React can connect 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 each render, React runs your component function from top to bottom.

  1. Component function executes.
  2. Hooks run in order.
  3. React reads previous stored values.
  4. React returns 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 stores local component state.

const [state, setState] = useState(initialValue);
  • Form fields.
  • Toggle states (open, loading, error).
  • Counters and small local UI state.
  • Very complex state transitions with many action types: prefer useReducer.
  • Global app-wide state shared across many distant components: prefer context or a state library.
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 runs side-effect code after React updates the screen.

  • Fetching API data.
  • Setting up subscriptions.
  • Timers and intervals.
  • Syncing with browser APIs.
  • Pure calculations from props/state: compute directly in render or use useMemo if expensive.
  • Every small event when an event handler is enough.
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 mutable value that survives re-renders without causing re-renders.

  • Focus an input.
  • Store previous values.
  • Keep IDs or timers between renders.
  • Access DOM nodes directly when needed.
  • Values that should update the UI when changed. Use state for that.
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 mixes controlled state and useRef to create a practical form flow: auto-focus, reset, and validation focus.

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 caches the result of an expensive calculation.

  • Heavy filtering, sorting, or computation that runs often.
  • Derived values that are expensive and based on dependencies.
  • Small cheap calculations.
  • As a default habit for every value.
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 caches a function reference.

  • Passing callbacks to memoized children.
  • Stable callback identity needed in dependencies.
  • If child is not memoized and no measurable performance issue.
  • For every function 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 state logic is complex or has many related updates.

const [state, dispatch] = useReducer(reducer, initialState);
  • Complex forms.
  • Multiple related state values.
  • Clear action-based updates.
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 after DOM updates but before the browser paints.

  • Measuring layout size before paint.
  • Avoiding visual flicker in specific UI calculations.
  • Regular API calls and most side effects. Use useEffect there.
useEffect -> after paint
useLayoutEffect -> before paint

A custom hook is a function that uses hooks and reuses logic across components.

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.

Each component has internal React data (Fiber). Hook values are stored there in order.

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 hooks inside if blocks breaks hook order.

Missing Effect Dependency

Missing dependencies can cause stale values and confusing bugs.

Missing Cleanup

Timers and listeners must be cleaned up to avoid leaks.

Overusing Memo Hooks

useMemo and useCallback add complexity when no real gain exists.

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