Hooks and State

Managing component state with useState, the rules of hooks, and useRef for DOM access.

Why state exists

Props let data flow into a component, but props alone can't represent something changing over time as the user interacts with the page — a counter going up, a form field being typed into, a toggle flipping on and off. For that, a component needs its own local, persistent, mutable-feeling data: state.

Plain JavaScript variables don't work for this. Reassigning a normal variable inside a function component doesn't cause React to re-render — the function just runs again from scratch next time something else triggers it, with that variable reset to its initial value. State is React's mechanism for a value that survives across re-renders and, crucially, triggers a re-render whenever it changes.

useState

useState is a Hook — a special function that lets a function component "hook into" React features like state. It returns a pair: the current value, and a function to update it.

Javascript
import { useState } from "react";

function Counter() {
  const [count, setCount] = useState(0); // 0 is the initial value

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

Calling setCount does two things: it updates the value React associates with this piece of state, and it schedules a re-render of the component (and its descendants) so the new value shows up on screen. This is the fundamental React loop: state changes → component re-renders → JSX reflects the new state.

useState(0) — the argument is only used on the component's very first render, to set the initial value. On every render after that, count holds whatever it was most recently set to, ignoring the initial argument entirely.

Updating state based on the previous state

When a new state value depends on the previous one, pass an updater function instead of a plain value. This matters because state updates aren't applied instantly — several setCount(count + 1) calls made in quick succession can end up reading the same stale count:

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

  function incrementTwice() {
    setCount(c => c + 1); // c is guaranteed to be the latest value
    setCount(c => c + 1);
  }

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={incrementTwice}>+2</button>
    </div>
  );
}

With setCount(count + 1) called twice back-to-back, both calls would capture the same count from that render and the count would only go up by 1, not 2. The updater-function form (c => c + 1) always receives the truly latest pending value, so both updates apply correctly.

State with objects and arrays

State updates replace the value; they don't merge into it the way this.setState did in old class components. When state is an object or array, always create a new one rather than mutating the existing one:

Javascript
function ProfileForm() {
  const [user, setUser] = useState({ name: "", email: "" });

  function updateName(newName) {
    setUser({ ...user, name: newName }); // spread the old fields, override name
  }

  return (
    <input
      value={user.name}
      onChange={e => updateName(e.target.value)}
    />
  );
}

setUser({ ...user, name: newName }) builds a brand-new object. Writing user.name = newName; setUser(user) would mutate the existing object in place — React might not detect the change at all, since it compares references, not deep contents.

Rules of hooks

Hooks come with two non-negotiable rules, both enforced by the eslint-plugin-react-hooks linter that ships with most React setups:

  1. Only call hooks at the top level. Never inside a loop, a condition, or a nested function. This ensures hooks are called in the exact same order on every render, which is how React internally matches each useState call to the right stored value across re-renders.
  2. Only call hooks from React function components or custom hooks. Never from a regular JavaScript function, a class component, or an event handler passed elsewhere.
Javascript
// Wrong — a hook inside a condition
function Bad({ showExtra }) {
  if (showExtra) {
    const [extra, setExtra] = useState(0); // breaks the call order between renders
  }
  // ...
}
Javascript
// Right — always call the hook; use the condition for what you do with the result
function Good({ showExtra }) {
  const [extra, setExtra] = useState(0);

  return showExtra ? <p>Extra: {extra}</p> : null;
}

useRef: state's quieter cousin

useRef also gives you a value that persists across re-renders, but with an important difference from useState: updating a ref does not trigger a re-render.

Javascript
import { useRef, useState } from "react";

function Stopwatch() {
  const [, forceTick] = useState(0);
  const renderCount = useRef(0);

  renderCount.current += 1; // updates immediately, but doesn't re-render anything

  return (
    <div>
      <p>This component has rendered {renderCount.current} times.</p>
      <button onClick={() => forceTick(t => t + 1)}>Force re-render</button>
    </div>
  );
}

The two most common uses of useRef:

1. Accessing a real DOM node directly — for things React's declarative model doesn't cover, like focusing an input or measuring an element's size:

Javascript
import { useRef } from "react";

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

  function focusInput() {
    inputRef.current.focus(); // imperative DOM access
  }

  return (
    <div>
      <input ref={inputRef} type="text" />
      <button onClick={focusInput}>Focus the input</button>
    </div>
  );
}

2. Storing a mutable value that shouldn't cause a re-render when it changes — like a timer ID, a previous value for comparison, or a render counter as above.

useState vs useRef

useState useRef
Triggers a re-render on change Yes No
Value available immediately after setting Only on the next render Immediately (.current updates synchronously)
Typical use Data the UI displays or reacts to DOM node access, timer IDs, values that don't affect what's rendered

Use useState for anything that should visibly affect the rendered output. Use useRef for anything that needs to persist between renders but shouldn't, by itself, cause the component to re-render.

Common mistakes

  • Mutating state directly (user.name = "x"; setUser(user)) instead of creating a new object/array — React compares by reference and may not notice the change.
  • Reading count right after calling setCount(...) and expecting it to already reflect the new value — state updates are not synchronous within the same render.
  • Calling a hook conditionally or inside a loop, breaking React's assumption that hooks run in the same order every render.
  • Reaching for useRef when the value should actually drive the UI — if changing it should update what's on screen, it needs to be state, not a ref.