React Hooks Explained: useState and useEffect for Beginners

A clear, beginner-friendly guide to the two most important React Hooks. Learn what useState and useEffect actually do, when to reach for each, and how they work together to build interactive interfaces in 2026.

Reactbeginner
12 min read

Every modern React app you read is built from two ideas: a component returns what should appear on screen, and hooks let that component remember things and react to changes. The two hooks you will reach for first — useState and useEffect — cover an enormous percentage of real production code. Get them right and the rest of React clicks into place quickly.

This guide explains exactly what each hook does, when to use which, and how they work together. If React itself is brand new, start there first; this article assumes you can read a basic component.

Why Hooks Exist

Before 2019, React components that needed state had to be classes with this.state, this.setState, and componentDidMount lifecycle methods. The ergonomics were rough and patterns like "share this stateful logic across components" required gymnastics. Hooks let function components do everything classes could do, with much less ceremony. In 2026 you almost never see a class component in new code.

The rules are simple and the linter (eslint-plugin-react-hooks, included by default in modern setups) enforces them:

  1. Call hooks only at the top level of a component or a custom hook — never inside loops, conditions, or nested functions.
  2. Call them in the same order on every render.

Both rules exist because React tracks hooks by call order, not by name.

useState: Memory for a Component

A function component normally forgets everything between renders. useState gives it a value that survives, and a setter that triggers a re-render when called.

App.jsxApp.jsx
import { useState } from "react";
 
function Counter() {
  const [count, setCount] = useState(0);
  return (
    <button onClick={() => setCount(count + 1)}>
      Clicked {count} times
    </button>
  );
}

useState(0) returns [currentValue, setterFunction]. You destructure into whatever names make sense. When setCount runs, React schedules a re-render, calls the function again, and count is now the new value.

The state can be any type — number, string, boolean, object, array. For arrays and objects, always create a new reference when updating. React compares by reference, so mutating in place does not trigger a re-render.

App.jsxApp.jsx
// wrong: mutating, React will not see the change
items.push(newItem); setItems(items);
 
// right: new array reference
setItems([...items, newItem]);

When the new state depends on the old state, pass a function to the setter. This avoids stale-closure bugs that come up in event handlers and timers.

App.jsxApp.jsx
setCount(prev => prev + 1);

useEffect: Reacting to Renders

Some work has to happen because the UI rendered: fetching data, subscribing to a websocket, setting a document title, registering an interval. That work is called a side effect, and useEffect is where it goes.

App.jsxApp.jsx
import { useEffect, useState } from "react";
 
function PageTitle({ user }) {
  useEffect(() => {
    document.title = `Hello, ${user.name}`;
  }, [user.name]);
  return <h1>Welcome</h1>;
}

useEffect takes a function (the effect) and a dependency array. React runs the effect after the render commits, and re-runs it whenever any value in the array changes. Three dependency-array patterns to recognise:

  • [] — run once after the first render only.
  • [a, b] — run after the first render and any time a or b changes.
  • omitted entirely — run after every render. Almost never what you want.

If your effect sets up something that needs cleaning up (an interval, an event listener, a websocket), return a cleanup function. React calls it before re-running the effect and when the component unmounts.

App.jsxApp.jsx
useEffect(() => {
  const id = setInterval(() => console.log("tick"), 1000);
  return () => clearInterval(id);
}, []);

A Real Example: Fetch on Mount

The classic "load some data when the component appears" pattern combines both hooks. State holds the data, an effect fetches it.

App.jsxApp.jsx
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [error, setError] = useState(null);
 
  useEffect(() => {
    const ctrl = new AbortController();
    fetch(`/api/users/${userId}`, { signal: ctrl.signal })
      .then(r => r.ok ? r.json() : Promise.reject(r.status))
      .then(setUser)
      .catch(e => e.name !== "AbortError" && setError(e));
    return () => ctrl.abort();
  }, [userId]);
 
  if (error) return <p>Failed to load.</p>;
  if (!user) return <p>Loading…</p>;
  return <h1>{user.name}</h1>;
}

Notice the AbortController. If userId changes before the previous fetch completes, the old request is cancelled and the cleanup function runs. Without it, an out-of-order response could overwrite a newer one. This pattern is the canonical way to do data fetching with raw useEffect.

In real apps you usually reach for TanStack Query or your framework's built-in data layer instead, because they handle caching, retries, and deduplication for you. But understanding the manual version makes everything else clearer.

Common Mistakes Beginners Make

  • Mutating state in place. Always pass a new object or array to the setter.
  • Forgetting the dependency array. Without it, an effect runs after every render — usually causing infinite loops.
  • Lying to the dependency array. If the effect uses userId, list userId. The eslint rule will warn you.
  • Putting fetches outside useEffect directly in the component body. They run during render, fire on every re-render, and break SSR.
  • Reaching for useEffect for derived data. If you can compute a value from props or state, just compute it — no hook needed.

Quick Reference

  • const [v, setV] = useState(initial);
  • Setter triggers a re-render; updates are batched
  • For new value based on old: setV(prev => prev + 1)
  • useEffect(() => { ... }, [deps]) — runs after render when deps change
  • Return a cleanup function to undo subscriptions, intervals, listeners
  • [] runs once, no array runs every render (rarely correct)
  • For data fetching in production, prefer TanStack Query, SWR, or your framework's data layer
  • Always destructure props that the effect uses, and list them as deps
Rune AI

Rune AI

Key Insights

  • useState gives a component memory; the setter triggers a re-render.
  • Always create new references for arrays and objects when updating state.
  • useEffect runs side effects after render; the dependency array controls when it re-runs.
  • Return a cleanup function to undo subscriptions, intervals, or in-flight fetches.
  • Trust the React Hooks ESLint rule — it catches almost every common bug before runtime.
RunePowered by Rune AI

Frequently Asked Questions

Why does my effect run twice in development?

React 18+ Strict Mode intentionally double-invokes effects in development to surface bugs in cleanup logic. It does not happen in production. Make your effect safe to run twice.

Can I call hooks conditionally?

No. Always call them at the top level of the component, in the same order every render. If you need conditional behaviour, put the condition inside the hook body.

Should I use `useState` or `useReducer`?

Use `useState` for simple values. Reach for `useReducer` when state updates involve multiple related fields and complex transitions (a shopping cart, a multi-step form).

When should I avoid `useEffect`?

For values that can be computed from props or state — just compute them. For event handlers — put the work in the handler directly. The Effect is for genuine *side effects* (DOM, network, subscriptions).

Are class components dead?

Effectively yes for new code. Hooks cover everything classes did, with less code and fewer footguns. You will still see classes in older codebases.

Conclusion

useState is memory; useEffect is reaction. Almost every interactive React component you will ever write is some combination of those two patterns. Build a counter, then a fetch-on-mount user profile, then a small TODO list with localStorage persistence — by the third project the hooks rules are second nature.