Skip to main content

🧠 Component State Fundamentals

State is a component's memory — the data that changes while the app is running and that React watches so it can keep the screen in sync. Before you reach for any specific hook, it pays to understand what state is, how it differs from props, and why changing it is what makes a React UI come alive.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Define state and explain the state → render → UI cycle that powers React
  • Distinguish state from props and choose the right one for a given piece of data
  • Categorize state (local vs. global, UI vs. server) so you know where each piece belongs
  • Identify derived values that should be calculated rather than stored in state
  • Recognize when to lift state up and when a state library earns its keep

Estimated Time: 30–40 minutes  •  Difficulty: Intermediate

Hands-on: Refactor a component riddled with redundant state into clean primary state plus derived values.

In This Lesson

What Is State in React?

State is the data a component owns that can change over its lifetime. It is a component's private memory: the current value of a text input, whether a menu is open, the list of items in a cart, the response that just came back from an API. When state changes, React re-renders the component so the screen reflects the new reality.

💡 State as memory: Think of a component as a person and state as that person's short-term memory. A person remembers a phone number they just heard; a component remembers what the user typed. When the memory changes, behavior changes — and when the component is removed from the page (unmounts), that memory is gone unless it was saved somewhere more permanent.

Three properties make state special, and they are worth committing to memory:

  • It is preserved between renders. Regular local variables reset every time a function runs; state survives because React stores it outside your component function.
  • Changing it triggers a re-render. This is the whole point — updating state is how you ask React to redraw the UI.
  • It is private and local by default. Two instances of the same component each have their own independent state.

📖 Key Terms

Render: React calling your component function to work out what the UI should look like right now.

Re-render: React calling it again because something (state or props) changed.

Source of truth: the single authoritative place a piece of data lives; the UI is derived from it, never the other way around.

The State → Render Cycle

React is declarative: you describe what the UI should look like for a given state, and React figures out the DOM changes needed to get there. That turns interactivity into a loop. An event updates state, the new state produces a new render, and the render updates the screen — which invites the next event.

flowchart LR E[User event] --> S[setState called] S --> R[Component re-renders] R --> U[UI reflects new state] U --> E

The critical mental shift for newcomers: you never touch the DOM directly to update the screen. You change state, and React updates the DOM for you. Your job is to keep state accurate; React's job is to keep the screen matching it.

State as a series of snapshots Each render is a snapshot: state value 0 produces one UI, an event bumps state to 1 which produces the next render and UI. Render #1 count = 0 UI shows "0" Event setCount(1) schedules re-render Render #2 count = 1 UI shows "1"
Figure 1 — Each render is a snapshot frozen in time. The state value used inside one render never changes mid-render; a new value only appears in the next render.

State vs. Props

State and props are both plain JavaScript data that influence what a component renders, and both trigger re-renders when they change. The difference is ownership: a component owns and can change its state; props are given to it by its parent and are read-only from the child's point of view.

StateProps
Internal to a componentPassed in from the parent
Can be updated by its ownerRead-only (immutable) to the child
Changing it triggers a re-renderNew props from the parent trigger a re-render
Persists across the component's rendersRecomputed by the parent on each of its renders
Keep it minimal — only what must changeCan carry any data, including callbacks

The two work together constantly. A parent holds state and passes it down as props, along with a callback the child can call to request a change. This is React's one-way data flow: data flows down, events flow up.

function Parent() {
  // State lives in the parent
  const [count, setCount] = useState(0);
  const increment = () => setCount(prev => prev + 1);

  return (
    <div>
      <h2>Parent count: {count}</h2>
      {/* State flows down as props; the callback lets the child ask for a change */}
      <Child count={count} onIncrement={increment} />
    </div>
  );
}

function Child({ count, onIncrement }) {
  // `count` is a prop here — the child reads it but never reassigns it
  return (
    <div>
      <p>Received from parent: {count}</p>
      <button onClick={onIncrement}>Increment parent</button>
    </div>
  );
}

💡 A quick decision rule

Ask: "Does this component need to change this value itself over time?" If yes, it is state. If the value simply arrives from elsewhere and the component only displays or forwards it, it is a prop.

Categories of State

Not all state is the same. Recognizing which kind of state you are dealing with tells you where it should live and which tool is best suited to manage it.

graph TD A[Application state] --> B[Local UI state] A --> C[Form state] A --> D[Navigation state] A --> E[Global app state] A --> F[Server cache state] B --> B1[Modal open/closed] C --> C1[Input values & validation] D --> D1[Active tab / route] E --> E1[Auth & theme] F --> F1[API responses]

Two useful axes

  • Local vs. global. Local state belongs to a single component (a toggle, an input). Global state is shared across many components (the signed-in user, the color theme).
  • UI state vs. server state. UI state controls how the interface behaves (which tab is active). Server state is a copy of data that truly lives on a server (a product list) — it can go stale, needs caching, and is best handled by tools like TanStack Query or SWR rather than raw useState.

⚠️ A common beginner trap

Reaching for a global store like Redux to hold data that only one component uses. Start local. Keep state as close as possible to where it is used, and only lift or globalize it when a real need appears. Premature globalization makes apps harder to reason about, not easier.

State vs. Derived Values

One of the highest-leverage skills in React is knowing what does not belong in state. If a value can be calculated from existing state or props during render, calculate it — do not store a second copy. Duplicated state is the source of an entire family of "the two numbers disagree" bugs.

// ❌ Redundant state: itemCount and totalPrice can drift out of sync
function CartBad() {
  const [items, setItems] = useState(initialItems);
  const [itemCount, setItemCount] = useState(3);   // duplicate!
  const [totalPrice, setTotalPrice] = useState(35); // duplicate!
  // Every mutation now has to remember to update all three...
}

// ✅ Derive on render: one source of truth, impossible to desync
function CartGood() {
  const [items, setItems] = useState(initialItems);

  const itemCount = items.reduce((sum, i) => sum + i.quantity, 0);
  const totalPrice = items.reduce((sum, i) => sum + i.price * i.quantity, 0);

  const addItem = (item) => setItems(prev => [...prev, item]);
  // No bookkeeping — the totals recompute automatically on the next render
}

Deriving on every render is essentially free for most calculations. Only if a derivation is genuinely expensive and runs often should you wrap it in useMemo so it recomputes only when its inputs change:

import { useMemo, useState } from 'react';

function Report({ rows }) {
  // Recomputes only when `rows` changes, not on every unrelated render
  const summary = useMemo(() => {
    return rows.reduce((acc, r) => acc + r.amount, 0);
  }, [rows]);

  return <p>Total: {summary}</p>;
}

✅ The litmus test

Before adding a piece of state, ask: "Can I compute this from state or props I already have?" If yes, derive it. Reserve state for values that cannot be calculated and that must persist between renders.

State Management Patterns

Lifting state up

When two sibling components need to share the same data, move that state to their nearest common parent and pass it down. The parent becomes the single source of truth, and the siblings stay in sync automatically.

function Thermostat() {
  // Lifted so both the display and the slider read the same value
  const [temp, setTemp] = useState(21);

  return (
    <div>
      <TemperatureDisplay value={temp} />
      <TemperatureSlider value={temp} onChange={setTemp} />
    </div>
  );
}

When to reach for more

As an app grows, passing props through many intermediate layers ("prop drilling") gets tedious. That is the signal to consider Context or a dedicated store. Here is a rough map:

ApproachBest forWatch out for
useStateLocal, component-specific stateProp drilling when shared widely
useReducerComplex state with many related transitionsOverkill for a simple toggle
Context APILow-frequency global values (theme, auth)Re-renders all consumers on change
Zustand / Redux ToolkitLarge apps, frequently updated shared stateExtra concepts and setup
TanStack Query / SWRServer state (fetching, caching)Don't hand-roll this with useState

💡 Modern default

For most apps in 2026: use useState/useReducer for local state, Context for a handful of truly global values, a query library for server data, and only add a store like Zustand or Redux Toolkit when shared client state gets genuinely complex.

Hands-on Exercise

🏋️ Refactor: Eliminate Redundant State

Objective: Turn a component that stores derivable data in state into one with a single source of truth.

The component below tracks products, but it also stores the in-stock list, the count, and the total value in state — and keeps them in sync by hand inside an effect and inside every handler. Refactor it so those three become derived values.

function ProductList() {
  const [products, setProducts] = useState([
    { id: 1, name: 'Laptop', price: 999.99, inStock: true },
    { id: 2, name: 'Phone',  price: 699.99, inStock: true },
    { id: 3, name: 'Tablet', price: 399.99, inStock: false }
  ]);

  // Redundant state — should be derived:
  const [inStockProducts, setInStockProducts] = useState([]);
  const [totalProducts, setTotalProducts] = useState(0);
  const [totalValue, setTotalValue] = useState(0);

  useEffect(() => {
    setInStockProducts(products.filter(p => p.inStock));
    setTotalProducts(products.length);
    setTotalValue(products.reduce((s, p) => s + p.price, 0));
  }, [products]);

  const toggleStock = (id) => {
    const next = products.map(p =>
      p.id === id ? { ...p, inStock: !p.inStock } : p
    );
    setProducts(next);
    setInStockProducts(next.filter(p => p.inStock)); // duplicated logic
    setTotalProducts(next.length);
    setTotalValue(next.reduce((s, p) => s + p.price, 0));
  };
  // ...render...
}

Steps:

  1. Delete the three redundant useState calls and the useEffect.
  2. Compute inStockProducts, totalProducts, and totalValue as plain constants during render.
  3. Simplify toggleStock so it only calls setProducts.
💡 Hint

products is the single source of truth. Anything you were storing separately can be produced from it with filter, .length, and reduce right before the return. Once you derive them, the handler no longer needs any bookkeeping.

✅ Solution
function ProductList() {
  const [products, setProducts] = useState([
    { id: 1, name: 'Laptop', price: 999.99, inStock: true },
    { id: 2, name: 'Phone',  price: 699.99, inStock: true },
    { id: 3, name: 'Tablet', price: 399.99, inStock: false }
  ]);

  // Derived on every render — always consistent, no effect needed
  const inStockProducts = products.filter(p => p.inStock);
  const totalProducts = products.length;
  const totalValue = products.reduce((sum, p) => sum + p.price, 0);

  const toggleStock = (id) => {
    setProducts(prev =>
      prev.map(p => (p.id === id ? { ...p, inStock: !p.inStock } : p))
    );
  };

  return (
    <div>
      <h2>
        {totalProducts} products — ${totalValue.toFixed(2)} total
      </h2>
      <ul>
        {products.map(p => (
          <li key={p.id}>
            {p.name} — ${p.price.toFixed(2)}
            <button onClick={() => toggleStock(p.id)}>
              {p.inStock ? 'Mark out of stock' : 'Mark in stock'}
            </button>
          </li>
        ))}
      </ul>
      <h3>In stock ({inStockProducts.length})</h3>
    </div>
  );
}

Notice how much shorter the handler became and how it is now impossible for the totals to disagree with the list.

🎯 Quick Quiz

Question 1: What is the defining difference between state and props?

Question 2: A cart component stores items in state and also stores totalPrice in state, updating it by hand whenever items change. What is the problem?

Question 3: Two sibling components need to display and edit the same value. What is the idiomatic React fix?

Best Practices

✅ Do

  • Keep state minimal — store only what can't be derived.
  • Keep state local — as close as possible to where it is used.
  • Treat state as immutable — create new objects/arrays instead of mutating existing ones.
  • Split unrelated values into separate state variables for clarity.
  • Match the tool to the kind of state — a query library for server data, Context for a few global values.

⚠️ Avoid

  • Duplicating derivable data into state (the classic desync bug).
  • Mutating state directly, e.g. items.push(x) — React won't notice the change.
  • Mirroring props into state without a reason; read the prop instead.
  • Reaching for a global store before you have a real sharing problem.

Summary & Quiz

🎉 Key Takeaways

  • State is a component's private, persistent memory; changing it re-renders the component.
  • The heartbeat of React is the state → render → UI cycle — you change state, React updates the DOM.
  • Props flow down and are read-only; state is owned and mutable. Data down, events up.
  • Reserve state for values you can't compute — derive everything else on render.
  • Start local, lift state up to share, and match bigger tools to the kind of state you have.

📚 Further Reading

🚀 What's Next?

Now that you know what state is and where it belongs, the next lesson gets hands-on with the tool you'll use most: the useState hook. We'll dig into its syntax, functional updates, lazy initialization, and the pitfalls that trip up almost every new React developer.

🎉 Well done!

You've built the mental model. Time to wire it up with a hook.