Module 1: The Four Kinds Of State
Local state: the UI of one component
Overview
We start with the simplest and safest box of all: local state. It's the state that a single component uses to manage its own UI, and that dies when that component disappears. A dropdown menu that's open or closed (menuOpen), the text someone half-types in a field before submitting it, whether a section is expanded or collapsed, whether the mouse is over a card. All of that is private to the component: nobody else needs it, and it makes no sense for it to survive the component's life. It's the box where most of a well-classified app's state lives —and, precisely because of that, the box people empty out too much when they discover the global tools and want to put everything there—.
Connection with the module. In lesson 1 you drew the four drawers and the question that opens them: "whose truth is it?". Local state is the most humble answer to that question: the truth is a single component's, and it dies with it. It's the box a piece lands in when the other three questions —is it the backend's?, is it shareable/reloadable?, do several distant components use it?— all answered "no". That's why it's the last branch of the decision tree: the local is what's left over after ruling out server, url and global. You already know the tool from react-fundamentals —it's useState—, so we don't re-teach it here; what we work on is recognizing when a piece truly belongs to this box, and what property defines it: isolation per instance.
An analogy: yours, in your room
Go back to the house from lesson 1. Local state is your toothbrush in your room: a thing only you use and that doesn't leave there. Two details of that image matter.
First, the isolation: if your sister also has her toothbrush in her room, they're two different toothbrushes. Your using yours doesn't move hers. There's no "one shared house toothbrush"; there's one per room, each with its own life. In React it's the same: if ten product cards each have a menuOpen, they're ten independent menuOpen. Opening one's menu doesn't open the other nine's.
Second, the mortality: when you move out and dismantle your room, your toothbrush goes with you or in the trash; it doesn't stay floating in the house. In React, when a component unmounts (disappears from the screen), its local state disappears with it. If you mount the component again afterwards, it starts from scratch, with its initial value. That mortality is a feature, not a defect: local state doesn't clutter the rest of the app with old data, because it cleans itself up.
Keep the two ideas —isolated per instance and dies with the component—, because they're exactly what we're going to measure.
The case in Mercado: menuOpen in a ProductCard
In the storefront, each ProductCard has a small actions menu ("Add to favorites", "Share", "View details") that opens on tapping a "···" button. Whether that menu is open or not is the local state piece par excellence: only that card uses it, and nobody else in the app cares about it. Here's how the component looks in real React —this is what you'll write, and it's pure useState from react-fundamentals—:
import { useState } from 'react';
function ProductCard({ product }) {
const [menuOpen, setMenuOpen] = useState(false); // LOCAL state of THIS card
return (
<div className="product-card">
<h3>{product.name}</h3>
<p>{formatPrice(product.priceCents)}</p>
<button onClick={() => setMenuOpen((open) => !open)}>···</button>
{menuOpen && (
<ul className="card-menu">
<li>Add to favorites</li>
<li>Share</li>
<li>View details</li>
</ul>
)}
</div>
);
}
Apply the decision rule to it, question by question: does the truth of menuOpen live in the backend? No —the server couldn't care less whether you opened a menu—. Would you want to share by link "card 3's menu is open"? No —nobody sends that—. Do several distant components need it? No —only this card reads it, to show or hide its <ul>—. The first three questions answer "no", so it falls into the last box: local. And since each ProductCard has its own call to useState, each card has its own menuOpen. That —isolation per instance— is what we're going to see executed.
Worked example: two menuOpen that don't notice each other
React doesn't run in an agent, so we model the mechanic of local state in Node: a function that makes a "card" with its own menuOpen cell, enclosed in a closure so nobody outside sees it —it's, in miniature, what useState does per instance—. We mount two cards, tap one's menu and the other, and at the end we unmount one to see how its local state disappears:
// LOCAL state: each instance of a component has ITS own state cell.
// We model two ProductCard, each with its isolated menuOpen.
// (It's a mini-version of React's per-instance useState.)
function makeProductCard(name) {
// Each card closes over its own cell: nobody else sees it.
let menuOpen = false; // LOCAL state of THIS card
return {
name,
toggleMenu() { menuOpen = !menuOpen; },
render() {
if (menuOpen === undefined) return `${name}: (unmounted, its menuOpen no longer exists)`;
return `${name}: menu ${menuOpen ? 'OPEN' : 'closed'}`;
},
unmount() { menuOpen = undefined; }, // on unmount, the local state disappears
};
}
console.log('=== Local state: isolated per instance ===\n');
const mouseCard = makeProductCard('Wireless Mouse');
const keyboardCard = makeProductCard('Mechanical Keyboard');
console.log('1) Both cards just mounted:');
console.log(' ' + mouseCard.render());
console.log(' ' + keyboardCard.render());
console.log('\n2) I open the menu ONLY of the mouse card:');
mouseCard.toggleMenu();
console.log(' ' + mouseCard.render());
console.log(' ' + keyboardCard.render() + ' <- did not notice: its menuOpen is another');
console.log('\n3) I open the keyboard one too, and close the mouse one:');
keyboardCard.toggleMenu();
mouseCard.toggleMenu();
console.log(' ' + mouseCard.render());
console.log(' ' + keyboardCard.render());
console.log('\n4) I unmount the mouse card (the user leaves that view):');
mouseCard.unmount();
console.log(' ' + mouseCard.render());
console.log(' ' + keyboardCard.render() + ' <- intact');
What to expect. When you run the file with Node, the output is exactly this:
=== Local state: isolated per instance ===
1) Both cards just mounted:
Wireless Mouse: menu closed
Mechanical Keyboard: menu closed
2) I open the menu ONLY of the mouse card:
Wireless Mouse: menu OPEN
Mechanical Keyboard: menu closed <- did not notice: its menuOpen is another
3) I open the keyboard one too, and close the mouse one:
Wireless Mouse: menu closed
Mechanical Keyboard: menu OPEN
4) I unmount the mouse card (the user leaves that view):
Wireless Mouse: (unmounted, its menuOpen no longer exists)
Mechanical Keyboard: menu OPEN <- intact
Read the output step by step, because each one demonstrates a property of local state:
Step 1 — two cells, two initial values. The two cards start with menu closed. Nothing strange yet, but notice that they're two distinct cells, each with its own initial false. In React, each useState(false) of each ProductCard creates its own cell; they share nothing.
Step 2 — the isolation, in action. I open only the mouse's menu. The result: the mouse ends up OPEN, and the keyboard stays closed. The keyboard's card didn't notice —its menuOpen is a distinct cell, in another room—. This is local state's star property: touching instance A doesn't touch instance B. If menuOpen were a shared global state, opening one would open all, and you'd have a classic bug ("I open one product's menu and everyone's opens").
Step 3 — each with its own life. I open the keyboard's and close the mouse's: now the mouse is closed and the keyboard OPEN. The two cells evolve separately, without coordinating. Each card carries its own history.
Step 4 — the mortality. I unmount the mouse card (the user navigated away from that view). Its menuOpen stops existing —"it went with the instance"—. The keyboard's card stays intact, with its menu open. This is the automatic cleanup: local state doesn't stay floating when the component goes away. If the user came back to that view, a new ProductCard would mount, with its menuOpen starting again at false.
That's the entire mental model of local state: a private cell per instance, isolated from the others, that's born with the component and dies with it. Simple, predictable, no action at a distance. That's why it's the default box for everything truly private to a component.
How to recognize that a piece is local
The most reliable sign is a two-question mental test:
- Does anyone else, outside this component, need to read or write this piece of data? If the answer is "no —only I use it for my own UI—", it's a candidate for local. (If "yes", it's probably global; lesson 3.)
- Does it make sense for this data to disappear when the component disappears? If "yes —when the view closes, this question no longer matters—", it confirms local. (If the data should survive a reload or navigation, it's probably url or server.)
Examples that pass both tests, and are therefore local:
piece why it's local
───────────────────────────── ────────────────────────────────────────────
menuOpen (action dropdown) only the card uses it; dies on close
input draft (text not sent) only the form uses it while typing
isExpanded (accordion) only that section uses it; private UI
isHovered (mouse over) ephemeral, visual, of a single element
activeTab (tabs of an only that tabs widget uses it; nobody
isolated widget) would want to share the tab by link
And a subtlety that lesson 7 will measure thoroughly, but which is worth sowing already: the draft of an input —the text being typed, key by key, before confirming— is local, even though the confirmed value ends up in another box. In Mercado, while the user types in the SearchBar, that half-typed text is local; but the moment they apply the search (press Enter), the resulting query belongs to the URL (it's shareable/reloadable). The same "search bar" mixes two boxes depending on which moment of its cycle the data is in. Recognizing it is part of classifying well.
Common mistakes
Promoting to global something that was local. What happens: menuOpen, isHovered or activeTab are put in a Context or a global store "to keep everything together". Why it happens: "many components of the same type use it" is confused with "it's shared". How to detect it: the global store has a menuOpen and suddenly all the cards open their menu at once, or switching tabs in one widget affects another. How to fix it: many instances of ProductCard each having a menuOpen does not make it shared —it makes it repeated local, one cell per instance, which is exactly what you saw in the example—. Local doesn't mean "unique in the app"; it means "private to each instance".
Lifting state higher than needed. What happens: menuOpen is lifted to App (or to ProductList) to "control it from above", when only the card uses it. Why it happens: in react-fundamentals you learned to lift state when two components share it, and it's applied reflexively even though nobody shares it here. How to detect it: the parent has state it only passes to one child and that no other child touches. How to fix it: state lives as low as possible —in the component that truly uses it—. Lifting only makes sense when two or more components need the same piece; if it's just one, it stays local there. (Lowering state to where it's used is as important as knowing how to lift it.)
Storing in local what should have survived. What happens: the search filter or the page is stored in a component's useState, and on reload or navigation it's lost. Why it happens: "it's a value that changes, it goes in useState". How to detect it: the user reloads and their search disappears; they share the link and it arrives blank. How to fix it: apply the second question's test —"does it make sense for it to disappear when the component disappears?"—. If the answer is "no, it should survive", it's not local: it's url (lesson 5) or server (lesson 4). The local is what can die without anyone hurting.
Exercises
Exercise 1 — Local or not? For each piece, decide whether it's local and justify with the two questions ("does anyone else need it?" and "does it make sense for it to die with the component?"): (a) whether a help tooltip is visible on mouseover; (b) which product is in the cart; (c) the text typed in the "discount code" field before applying it; (d) whether the "shipping details" accordion is expanded; (e) the already-applied search term that filters the product list.
See solution
- (a) tooltip visible → local. Only that element uses it, it's ephemeral and visual, and it dies without a problem when the mouse leaves or the component disappears. The two questions answer "only me" and "yes, let it die".
- (b) product in the cart → NOT local (it's global). The cart is read and written by several distant components (the card that adds, the badge, the checkout), and it shouldn't disappear because a component unmounts. First question: "yes, others need it" → not local. (It's global, lesson 3.)
- (c) unapplied discount code → local. Just like the search draft: the half-typed text is private to the form and can die with it. When it's applied, the effect (the already-validated discount) may belong to another box, but the draft is local.
- (d) expanded accordion → local. Private UI of that section; nobody else needs it and it dies painlessly on closing the view.
- (e) applied search term → NOT local (it's url). Here's the trap: while it's being typed it's local, but the term already applied that filters the list is something the user would want to share/reload → url (lesson 5). The second question gives it away: "no, it shouldn't die with the component; it should survive a reload".
Exercise 2 — The isolation, explained. With the executed example, explain why opening the mouse card's menu did not open the keyboard's. What would happen if menuOpen were a single global variable shared by all the cards? Describe the concrete bug you'd see on screen.
See solution
In the example, each makeProductCard creates its own menuOpen cell in a closure; the mouse's and the keyboard's are two distinct cells. That's why mouseCard.toggleMenu() only touched the mouse's cell, and the keyboard —which reads another cell— stayed closed. It's isolation per instance: in React, each useState(false) of each ProductCard is a separate cell.
If menuOpen were a single global variable shared by all the cards, tapping any product's "···" button would set that single menuOpen to true, and all the cards —which would read the same variable— would show their menu at once. The bug on screen: you open one product's actions menu and the menus of all twenty products in the list unfold at the same time. It's the classic "all the dropdowns open together", and its cause is exactly that: a state that should have been local per instance was made shared global.
Exercise 3 — Lower the state to its place. A colleague put the cards' menuOpen in App (the root), passing it through props down to each ProductCard. Explain why that's misclassified, what problems it brings, and how you'd fix it. Does your answer change if the requirement were "only one menu can be open at a time in the whole list"?
See solution
Putting a single menuOpen in App is wrong for two reasons. First, classification: menuOpen is local to each card (nobody outside the card needs it), so its place is the useState of the ProductCard itself, as low as possible. Second, behavior: a single menuOpen in the root would make all the cards share the same value → all the menus open together (the bug from exercise 2). The fix: lower the state to the ProductCard (const [menuOpen, setMenuOpen] = useState(false) inside each card), so each instance has its own.
If the requirement were "only one open at a time", the answer changes: now there is coordination between cards —which one is open is a question about the list, not about a single card—. That state (openCardId) is lifted to the common parent (ProductList), which knows which is open and passes it to each card. It doesn't become app-global; it's lifted only up to the common ancestor that needs to coordinate (exactly what you learned in react-fundamentals: lift to the precise point where it's shared, no higher and no lower). The lesson: a piece's box depends on who needs it, and that "who" can be a component (local), a subtree (lift to the parent) or the whole app (global).
Summary and next step
In this lesson you opened the first drawer: local state, a component's private UI. We saw its two defining properties, executed: the isolation per instance —two ProductCard with two menuOpen that don't notice each other— and the mortality —local state is born with the component and dies when it unmounts—. You learned to recognize it with two questions ("does anyone else need it?", "does it make sense for it to die with the component?") and you saw the subtlety of an input's draft, which is local even though its confirmed value belongs to another box. You took the tool —useState, from react-fundamentals— as known; what was new was classifying with confidence what belongs here.
Before moving on you should be able to: define local state and its two properties; distinguish "repeated local per instance" from "shared"; apply the two recognition questions; and explain why lifting or globalizing something that was local brings bugs (all the menus opening together, state that clutters the app).
Lesson 3 opens the second drawer: global client state, what several distant components share. There you'll see, executed, the case local state can't solve: the cart that the ProductCard writes and the header badge reads, two components that aren't parent-child and that need the same truth. It's the moment when the isolation —local state's virtue— becomes an obstacle, and the need for a single shared truth appears.
Resources
- React, "State: A Component's Memory" — react.dev/learn/state-a-components-memory. The official page of
useState: what a component's state is and how each instance has its own. The review of the tool this box uses. In English. - React, "Choosing the State Structure" — react.dev/learn/choosing-the-state-structure. Principles for deciding where and how to store state; in particular, keeping it as local as possible and not duplicating it. In English.
- React, "Sharing State Between Components" — react.dev/learn/sharing-state-between-components. The contrast with this lesson: when state stops being local and has to be lifted. The bridge to lesson 3. In English.
- React, "Preserving and Resetting State" — react.dev/learn/preserving-and-resetting-state. What happens to local state when a component mounts, unmounts or changes position: the "mortality" we measured in the example. In English.