Volume I index
State and Events From Scratch

The First useState Hook

Ishtmeet Singh @ishtms/July 14, 2026/10 min read
#react#state#usestate#hooks#interaction

A click handler can log an action, but a log does not change the interface. The component needs a value that survives its current function call and causes another render when it changes.

React calls that stored component memory state. The useState Hook adds one state position to a component instance.

jsx
import { useState } from "react";

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

  return (
    <button onClick={() => setCount(count + 1)}>
      {count}
    </button>
  );
}

The button begins at zero. Clicking requests one as the next count. React renders Counter again and the button receives the next text.

A Local Variable Fails Two Requirements

Start with a local variable.

jsx
function Counter() {
  let count = 0;

  function handleClick() {
    count += 1;
  }

  return <button onClick={handleClick}>{count}</button>;
}

The click changes the count binding captured by this render's handler. Two pieces are missing.

First, assigning the variable does not ask React to render the component again. The browser button keeps displaying zero.

Second, a later parent render calls Counter again and creates a new local count initialized to zero. The old function call has ended. Its local binding is not the current render's data.

State provides both requirements.

text
retain a value for this component instance
request another render after the value changes

Import the Hook

useState is a named export from the react package.

jsx
import { useState } from "react";

Hooks are functions whose names begin with use. React associates their calls with the component instance currently being rendered.

Call useState inside the component body.

jsx
function Counter() {
  const statePair = useState(0);
  // ...
}

The argument 0 is the initial state for this state position.

The function returns a two-position array.

text
position 0 -> current state value
position 1 -> state setter function

Array destructuring creates useful local names.

jsx
const [count, setCount] = useState(0);

The array destructuring is JavaScript. React defines the returned pair.

Name the Value and Setter Together

Use a noun or boolean condition for the value and the same name after set for the setter.

jsx
const [count, setCount] = useState(0);
const [query, setQuery] = useState("");
const [isOpen, setIsOpen] = useState(false);

The pattern is a convention, not special parsing. These names would run.

jsx
const [banana, updateBanana] = useState(0);

They fail to communicate the component state. count and setCount let a reader connect the stored value with its update function immediately.

Use an is, has, can, or show prefix for booleans when it makes the condition read directly.

jsx
const [showDetails, setShowDetails] = useState(false);
jsx
{showDetails && <LessonDetails />}

The Setter Requests the Next Value

Call the setter from the click handler.

jsx
function handleClick() {
  setCount(count + 1);
}

count + 1 is calculated from the count visible to this render. The setter queues that result as the next state.

The setter does not return the next state for immediate use.

jsx
const result = setCount(count + 1);

result is undefined. State is available to the next render, not as a setter return value.

Do not assign to the state binding.

jsx
count = count + 1;

The binding was declared with const, and an assignment would not update React's retained state. Use the setter.

Follow the First Render

Use the complete component.

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

  function handleClick() {
    setCount(count + 1);
  }

  return <button onClick={handleClick}>{count}</button>;
}

On the first render, React reaches the Hook call and initializes its state position with zero. The local count binding receives zero. React returns a stable setter for that position through setCount.

The returned button displays zero and stores handleClick in its event prop. The handler closes over the count binding from this render.

After the click, setCount(1) requests another render. During that render, React does not initialize the position again. It supplies the stored value one.

text
first render -> count 0
click        -> request count 1
next render  -> count 1

The component function starts again, but React retains the state position for that rendered instance.

State Is Stored per Component Instance

Render two counters.

jsx
function App() {
  return (
    <main>
      <Counter />
      <Counter />
    </main>
  );
}

Both occurrences use the same Counter function definition. React creates two component instances at two positions. Each instance receives its own state position.

Clicking the first counter updates the first instance. The second remains at zero.

text
Counter instance at position 0 -> count 1
Counter instance at position 1 -> count 0

State is not stored in a module variable on the function. React associates it with the instance in the rendered tree.

Moving a component to another position or replacing its type can therefore end the instance associated with its state.

One Component Can Have Several State Positions

A details component can store open state and a selected tab.

jsx
function LessonDetails() {
  const [isOpen, setIsOpen] = useState(false);
  const [tab, setTab] = useState("notes");
  // ...
}

React associates the first Hook call with one state position and the second Hook call with another.

The order must remain the same across renders.

text
Hook call 1 -> isOpen
Hook call 2 -> tab

This order is the basis for the Rules of Hooks.

Use separate state variables when values change independently. An object can group related fields later, but it adds replacement and copy behavior that a single boolean does not need.

Follow the Rules of Hooks

Call Hooks at the top level of a function component or another Hook. Do not call them inside conditions, loops, event handlers, or nested ordinary functions.

This condition changes the Hook order.

jsx
function LessonDetails({ available }) {
  if (available) {
    const [isOpen, setIsOpen] = useState(false);
  }

  // ...
}

One render can call useState while another skips it. React can no longer associate later Hook calls with consistent positions.

Call the Hook first and branch afterward.

jsx
function LessonDetails({ available }) {
  const [isOpen, setIsOpen] = useState(false);

  if (!available) {
    return null;
  }

  // ...
}

This loop is invalid for the same reason.

jsx
lessons.map(() => {
  const [open, setOpen] = useState(false);
});

Extract a LessonItem component and let each rendered instance call its own Hook at component top level.

The React Hooks ESLint plugin reports many order violations before runtime. Keep its rules enabled.

Do Not Call Hooks from Event Handlers

This handler runs after render, when React is not performing the component's Hook call sequence.

jsx
function handleClick() {
  const [count, setCount] = useState(0);
}

React reports an invalid Hook call. State must be declared while the component renders.

jsx
const [count, setCount] = useState(0);

function handleClick() {
  setCount(count + 1);
}

The handler closes over the setter and current render's value.

Initial State Is Used for Initialization

This Hook receives zero during every component function call in source.

jsx
const [count, setCount] = useState(0);

React uses the argument when initializing that state position. Later renders use the retained state, not the argument again.

This becomes visible with a prop.

jsx
function Counter({ initialCount }) {
  const [count, setCount] = useState(initialCount);
  // ...
}

Changing initialCount after the component is mounted does not automatically reset count. The prop names an initial value, not a synchronization rule.

If the component should always display the prop, it may not need local state. If another record should start a fresh instance, give that rendered component a different stable key.

Use a Lazy Initializer for Expensive Creation

Suppose initial state requires a non-trivial local calculation.

jsx
function createInitialLessons() {
  return readSavedLessons();
}

Pass the function itself.

jsx
const [lessons, setLessons] = useState(createInitialLessons);

React calls it when initialization is required. This version calls it during every render before React can ignore later initial arguments.

jsx
const [lessons, setLessons] = useState(createInitialLessons());

Use lazy initialization only when the calculation has enough cost to justify it. A primitive literal needs no initializer function.

The initializer must be pure. Development Strict Mode can call it twice and ignore one result to expose accidental mutation.

jsx
const [count] = useState(() => 0);

This works but adds syntax without saving work. Use useState(0).

State Can Hold JavaScript Values

These initial values are all supported.

jsx
useState(0);
useState("");
useState(false);
useState(null);
useState([]);
useState({ query: "", status: "all" });

React stores the value you supply. It does not validate a domain contract or make objects immutable.

Start with a primitive when the interaction needs one primitive. Object and array state must be replaced with new references when it changes.

Do not store a React component function in state without a specific design. Passing a function directly to a setter can be interpreted as an updater function. Storing callable values needs an extra wrapper and is uncommon in application code.

Store Only the Independent Value

This component needs one boolean.

jsx
const [isOpen, setIsOpen] = useState(false);

The label follows from it.

jsx
const label = isOpen ? "Hide details" : "Show details";

Do not store both.

jsx
const [isOpen, setIsOpen] = useState(false);
const [label, setLabel] = useState("Show details");

Every event would need to update two values that describe one fact. Calculate the second value from the first instead.

Build the First Stateful Component

Create a details control.

jsx
function ChapterDetails() {
  const [isOpen, setIsOpen] = useState(false);

  function handleClick() {
    setIsOpen((open) => !open);
  }

  return (
    <section>
      <button
        type="button"
        aria-expanded={isOpen}
        onClick={handleClick}
      >
        {isOpen ? "Hide details" : "Show details"}
      </button>
      {isOpen && <p>This chapter contains six lessons.</p>}
    </section>
  );
}

The updater function receives the queued current value and returns its opposite. Use this form whenever the next state depends on the previous state.

Click several times and inspect the component in React DevTools if installed. The state entry changes between false and true while the component type and position remain stable.

Lesson Check

The first state path has five distinct parts.

text
useState declares one state position
React supplies the current value and setter
the rendered handler closes over them
the interaction calls the setter
React renders the instance with the next stored value

Source Notes