🧠 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.
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 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.
| State | Props |
|---|---|
| Internal to a component | Passed in from the parent |
| Can be updated by its owner | Read-only (immutable) to the child |
| Changing it triggers a re-render | New props from the parent trigger a re-render |
| Persists across the component's renders | Recomputed by the parent on each of its renders |
| Keep it minimal — only what must change | Can 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.
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:
| Approach | Best for | Watch out for |
|---|---|---|
useState | Local, component-specific state | Prop drilling when shared widely |
useReducer | Complex state with many related transitions | Overkill for a simple toggle |
| Context API | Low-frequency global values (theme, auth) | Re-renders all consumers on change |
| Zustand / Redux Toolkit | Large apps, frequently updated shared state | Extra concepts and setup |
| TanStack Query / SWR | Server 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:
- Delete the three redundant
useStatecalls and theuseEffect. - Compute
inStockProducts,totalProducts, andtotalValueas plain constants during render. - Simplify
toggleStockso it only callssetProducts.
💡 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
- React Docs — State: A Component's Memory
- React Docs — Choosing the State Structure
- React Docs — Sharing State Between Components
🚀 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.