Episode 2 — React Frontend Architecture NextJS / 2.3 — State and Rerendering Logic
2.3.a — What Is State
In one sentence: State is data owned by a component that can change over time, and when it changes, React automatically re-renders that component to reflect the new reality.
Navigation: ← 2.3 Overview · Next → 2.3.b — useState Hook
Table of Contents
- State as Component Memory
- Why Regular Variables Fail
- Props vs State
- State as a Snapshot in Time
- State Is Isolated Per Component Instance
- State Ownership and Single Source of Truth
- Types of State
- State Design Principles
- Thinking in State: Identifying What Should Be State
- State vs the DOM
- Immutability and State
- State Management Complexity Spectrum
- When NOT to Use State
- Real-World State Examples
- Key Takeaways
- Explain-It Challenge
1. State as Component Memory
Every interactive application needs to remember things. A counter needs to remember its current count. A form needs to remember what the user has typed. A shopping cart needs to remember what items have been added. This "memory" is what React calls state.
State is data that:
- Belongs to a specific component
- Can change over time (usually in response to user actions)
- Causes the component to re-render when it changes
Think of a component as a function that returns UI. Without state, that function always returns the same output for the same props. State introduces variation across time -- the same component can produce different UI at different moments.
Without state: With state:
+-------------------+ +-------------------+
| Component(props) | | Component(props) |
| | | state: count=0 |
| Always same UI | | UI shows: 0 |
| for same props | +-------------------+
+-------------------+ |
[click +]
|
+-------------------+
| Component(props) |
| state: count=1 |
| UI shows: 1 |
+-------------------+
A helpful mental model: state is a component's personal notebook. The component can write notes in it, read from it, and every time it updates a note, it gets a fresh chance to re-draw itself based on the latest information.
What Makes State Special
Regular JavaScript variables disappear when a function finishes executing. Since React components are functions, local variables reset on every call:
function greet() {
let message = "Hello";
return message;
}
greet(); // "Hello"
greet(); // "Hello" -- no memory of previous call
State is different. React stores state values outside the component function, in its own internal data structures. When the component re-renders (the function runs again), React feeds the stored state back into the component. The component "remembers" because React remembers for it.
React's Internal Storage:
+----------------------------------+
| Component Instance #1 |
| Hook 0: count = 5 |
| Hook 1: isOpen = true |
| Hook 2: name = "Alice" |
+----------------------------------+
| Component Instance #2 |
| Hook 0: count = 0 |
| Hook 1: isOpen = false |
| Hook 2: name = "" |
+----------------------------------+
2. Why Regular Variables Fail
This is the most important concept to internalize early. Consider this broken component:
function Counter() {
let count = 0;
function handleClick() {
count = count + 1;
console.log(count); // This WILL log 1, 2, 3...
}
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>Increment</button>
</div>
);
}
When you click the button:
handleClickrunscountchanges from 0 to 1 in memoryconsole.logshows 1- But the screen still shows 0
Why? Two reasons:
Reason 1: Local variables don't persist between renders. When React calls Counter() again (which it does on re-render), it executes let count = 0 again. The old value is gone.
Reason 2: Changing a local variable doesn't trigger a re-render. React has no idea you changed count. It has no mechanism to watch arbitrary variables. You need to explicitly tell React "this data changed, please re-render."
Timeline with regular variable:
Render 1: let count = 0 --> UI shows "Count: 0"
|
[click]
|
count = 1 (in memory only, no re-render triggered)
|
Screen still shows "Count: 0"
If React DID re-render:
Render 2: let count = 0 --> UI shows "Count: 0" (reset!)
The fix is useState, which we cover in the next section. But the key insight here is:
To update the UI, you need two things:
- A way to persist data between renders
- A way to tell React "re-render this component"
useState provides both.
The Variable vs State Mental Model
Regular Variable:
- Lives inside the function call
- Dies when the function returns
- Changing it does nothing to the UI
- Reset every time the function runs
State:
- Lives in React's memory (outside the function)
- Persists across function calls (re-renders)
- Changing it triggers a re-render
- Preserved until the component unmounts
3. Props vs State
Props and state are both plain JavaScript data that influence render output, but they serve fundamentally different purposes.
Comparison Table
| Aspect | Props | State |
|---|---|---|
| Who owns it | Parent component | The component itself |
| Who can change it | Only the parent | Only the owning component |
| Mutable inside component? | No (read-only) | Yes (via setter function) |
| Direction of flow | Parent to child (top-down) | Local to the component |
| Triggers re-render? | Yes (when parent passes new props) | Yes (when state changes) |
| Purpose | Configure a component from outside | Track data that changes over time |
| Analogy | Function parameters | Function's local memory |
| Initialization | Set by parent in JSX | Set by component via useState |
When to Use Props vs State
Use props when:
- A parent component controls the data
- The data should be configurable from outside
- Multiple components need the same data (pass it down)
- The component is purely presentational
Use state when:
- The data originates inside this component
- The data changes in response to user interaction
- Only this component needs to know about changes
- The data represents something the user can modify
The Relationship Between Props and State
Props and state often work together. A parent's state becomes a child's props:
function Parent() {
const [username, setUsername] = useState("Alice");
return <Greeting name={username} />;
}
function Greeting({ name }) {
return <h1>Hello, {name}!</h1>;
}
Data flow:
Parent
+---------------------------+
| state: username = "Alice" |
| |
| <Greeting name={username}>|
+---------------------------+
|
| props.name = "Alice"
v
Greeting
+---------------------------+
| props: name = "Alice" |
| (read-only) |
| |
| <h1>Hello, Alice!</h1> |
+---------------------------+
A Common Mistake: Copying Props into State
// BAD: Don't copy props into state
function UserProfile({ initialName }) {
const [name, setName] = useState(initialName);
// Now 'name' and 'initialName' can diverge
// If the parent updates initialName, this component won't reflect it
}
// GOOD: Use the prop directly
function UserProfile({ name }) {
return <h1>{name}</h1>;
}
// ACCEPTABLE: When props are truly just an initial value
// and the component takes ownership of that data
function EditableField({ defaultValue }) {
const [value, setValue] = useState(defaultValue);
// This is fine IF defaultValue is truly just a starting point
// and this component manages the value from here on
}
4. State as a Snapshot in Time
This is one of React's most counter-intuitive concepts and one of the most important.
When React calls your component function, it gives you a snapshot of the state at that moment. For the entire duration of that render, the state value is fixed. It does not change mid-render, even if you call the setter.
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
console.log(count); // Still 0! Not 1.
setCount(count + 1);
console.log(count); // Still 0!
setCount(count + 1);
console.log(count); // Still 0!
}
// After handleClick, count will be 1, not 3.
// Because all three calls used the same snapshot: count = 0
// setCount(0 + 1) three times = setCount(1) three times = count becomes 1
return (
<div>
<p>{count}</p>
<button onClick={handleClick}>+3?</button>
</div>
);
}
Visualizing the snapshot:
Render with count = 0:
+-----------------------------------------------+
| The entire handleClick function sees count = 0 |
| |
| setCount(0 + 1) // queues: set count to 1 |
| console.log(0) // count is still 0 |
| setCount(0 + 1) // queues: set count to 1 |
| console.log(0) // count is still 0 |
| setCount(0 + 1) // queues: set count to 1 |
| console.log(0) // count is still 0 |
+-----------------------------------------------+
After event handler finishes:
React processes the queue: last value is 1
Next render: count = 1
Why Snapshots Matter
The snapshot model prevents a whole class of bugs. Consider a chat application:
function Chat() {
const [message, setMessage] = useState("");
const [recipient, setRecipient] = useState("Alice");
function handleSend() {
sendMessage(recipient, message);
}
// Even if the user switches recipient after clicking Send,
// the message goes to whoever was selected at click time.
// This is usually the correct behavior!
}
Snapshots with Timeouts
function DelayedAlert() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
setTimeout(() => {
alert(`Count was: ${count}`);
}, 3000);
}
return (
<div>
<p>{count}</p>
<button onClick={handleClick}>Increment + Alert</button>
</div>
);
}
If you click three times rapidly:
- Click 1: count is 0 at click time. Alert after 3s: "Count was: 0"
- Click 2: count is 1 at click time. Alert after 3s: "Count was: 1"
- Click 3: count is 2 at click time. Alert after 3s: "Count was: 2"
Each click captured a different snapshot. The alerts reflect the snapshot, not the "latest" value.
5. State Is Isolated Per Component Instance
When you use the same component multiple times, each instance gets its own independent state. Changing state in one instance does not affect others.
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
}
function App() {
return (
<div>
<Counter /> {/* Instance 1: has its own count */}
<Counter /> {/* Instance 2: has its own count */}
<Counter /> {/* Instance 3: has its own count */}
</div>
);
}
Component Tree with Isolated State:
App
/ | \
/ | \
v v v
Counter Counter Counter
count=3 count=0 count=7
Clicking Instance 1 does NOT affect Instance 2 or 3.
Each has its own independent state stored in React's memory.
Why Isolation Matters
This isolation is what makes components reusable. A <TextInput /> component used in a login form and a signup form on the same page won't interfere with each other. Each maintains its own value, its own validation state, its own focus state.
How React Keeps State Separate
React identifies component instances by their position in the component tree. The first <Counter /> in the JSX gets slot 0, the second gets slot 1, and so on. React maps state to these positions.
React's internal state map:
Tree position --> State
App/Counter[0] --> { hooks: [count: 3] }
App/Counter[1] --> { hooks: [count: 0] }
App/Counter[2] --> { hooks: [count: 7] }
This is why the order of hooks matters and why you cannot call hooks inside conditions or loops -- React relies on consistent ordering to map hooks to state slots.
6. State Ownership and Single Source of Truth
A core principle: for any piece of data in your application, there should be exactly one component that "owns" it. This component is the single source of truth for that data.
The Problem with Duplicated State
// BAD: Both components track the same "selected" state independently
function Sidebar({ items }) {
const [selected, setSelected] = useState(null);
return items.map(item => (
<SidebarItem
key={item.id}
selected={item.id === selected}
onClick={() => setSelected(item.id)}
/>
));
}
function MainContent({ items }) {
const [selected, setSelected] = useState(null);
return <Detail item={items.find(i => i.id === selected)} />;
}
These two components have their own selected state. If Sidebar updates its selection, MainContent doesn't know. They fall out of sync.
The Solution: Lift State Up
Move the shared state to the nearest common ancestor:
function App() {
const [selected, setSelected] = useState(null);
const items = [...];
return (
<div>
<Sidebar
items={items}
selected={selected}
onSelect={setSelected}
/>
<MainContent
items={items}
selected={selected}
/>
</div>
);
}
Now App is the single source of truth for selected. Both Sidebar and MainContent read from the same source and stay in sync.
Before (duplicated): After (single source):
Sidebar MainContent App (owns 'selected')
selected=2 selected=5 / \
(out of (out of Sidebar MainContent
sync!) sync!) reads from reads from
App App
Ownership Rules
- State should live in the lowest common ancestor of all components that need it
- If only one component uses the state, keep it local to that component
- If siblings need shared state, lift it to their parent
- If distant components need shared state, consider Context or a state management library
7. Types of State
Not all state is the same. Understanding categories helps you choose the right tool.
Local (Component) State
Data that belongs to a single component and is not shared.
function SearchBar() {
const [query, setQuery] = useState("");
return <input value={query} onChange={e => setQuery(e.target.value)} />;
}
Tool: useState, useReducer
Shared (Lifted) State
State that multiple components need, lifted to a common parent.
function Parent() {
const [filter, setFilter] = useState("all");
return (
<>
<FilterBar current={filter} onChange={setFilter} />
<ProductList filter={filter} />
</>
);
}
Tool: useState in parent, pass down via props
Global (App-wide) State
State that many components across the app need access to: current user, theme, locale, permissions.
const ThemeContext = createContext("light");
function App() {
const [theme, setTheme] = useState("light");
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Header />
<Main />
<Footer />
</ThemeContext.Provider>
);
}
Tool: React Context, Zustand, Redux, Jotai
Server State
Data that comes from an API and needs caching, revalidation, and synchronization.
Server state has unique challenges:
- It's asynchronous (loading, error, success states)
- It can become stale
- It might need background refresh
- It's shared across the network
- It requires caching strategies
Tool: TanStack Query (React Query), SWR, RTK Query
URL State
State stored in the URL: current page, search filters, sort order.
https://shop.example.com/products?category=shoes&sort=price&page=2
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
This is state! Stored in the URL.
Tool: React Router's useSearchParams, useParams, Next.js router
Form State
The current values, validation errors, touched fields, and submission status of a form.
Tool: React Hook Form, Formik, or manual useState
Summary Table
| Type | Scope | Persistence | Tool |
|---|---|---|---|
| Local | Single component | Component lifetime | useState |
| Shared | Sibling components | Common parent lifetime | Lifted useState |
| Global | Entire app | App lifetime | Context, Zustand, Redux |
| Server | API data | Cache-dependent | TanStack Query, SWR |
| URL | Current route | Browser history | Router params/search |
| Form | Form component | Form lifetime | React Hook Form |
8. State Design Principles
Designing your state structure well prevents bugs and simplifies your code.
Principle 1: Keep State Minimal
Only store the minimum data needed. If you can calculate something from existing state or props, calculate it -- don't store it.
// BAD: Redundant state
function ShoppingCart() {
const [cartItems, setCartItems] = useState([]);
const [total, setTotal] = useState(0);
const [itemCount, setItemCount] = useState(0);
}
// GOOD: Minimal state, derive the rest
function ShoppingCart() {
const [cartItems, setCartItems] = useState([]);
const total = cartItems.reduce((sum, item) => sum + item.price * item.qty, 0);
const itemCount = cartItems.reduce((sum, item) => sum + item.qty, 0);
}
Principle 2: Avoid Contradictory State
Don't have state combinations that shouldn't be possible.
// BAD: Can be in contradictory state
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
// GOOD: Use a single state with union of possible values
const [status, setStatus] = useState("idle");
// status can be: "idle" | "loading" | "error" | "success"
Principle 3: Avoid Duplicate State
Don't store the same data in multiple places.
// BAD: selectedItem duplicates data from items
const [items, setItems] = useState([...]);
const [selectedItem, setSelectedItem] = useState(null);
// GOOD: Store only the ID, derive the full object
const [items, setItems] = useState([...]);
const [selectedId, setSelectedId] = useState(null);
const selectedItem = items.find(item => item.id === selectedId);
Principle 4: Normalize Nested Data
Flat structures are easier to update than deeply nested ones.
// BAD: Deeply nested
const [data, setData] = useState({
posts: [
{
id: 1,
title: "Hello",
comments: [
{ id: 101, author: { id: 1, name: "Alice" }, text: "Great!" }
]
}
]
});
// GOOD: Normalized (flat)
const [posts, setPosts] = useState({ 1: { id: 1, title: "Hello", commentIds: [101] } });
const [comments, setComments] = useState({ 101: { id: 101, authorId: 1, text: "Great!" } });
const [users, setUsers] = useState({ 1: { id: 1, name: "Alice" } });
Principle 5: Avoid Deeply Nested State
If you must have nesting, keep it shallow (2-3 levels max).
// BAD: 4+ levels deep
const [form, setForm] = useState({
user: {
address: {
coordinates: {
lat: 0,
lng: 0
}
}
}
});
// Updating lat requires painful nested spreads
// BETTER: Flatten or split into separate state
const [lat, setLat] = useState(0);
const [lng, setLng] = useState(0);
9. Thinking in State: Identifying What Should Be State
When building a UI, follow this process to decide what should be state:
Step 1: List All Data Your UI Needs
For a todo app:
- List of todo items
- Current filter (all, active, completed)
- Text the user is typing in the input
- Whether each item is complete
Step 2: Apply the Three-Question Test
For each piece of data, ask:
- Does it change over time? If no, it's a constant, not state.
- Is it passed from a parent via props? If yes, it's not state in this component.
- Can you compute it from existing state or props? If yes, it's derived -- not state.
If you answered "no" to all three, it's probably state.
Step 3: Apply the Test to the Todo App
| Data | Changes? | From props? | Computed? | State? |
|---|---|---|---|---|
| Todo items | Yes | No | No | Yes |
| Filter value | Yes | No | No | Yes |
| Input text | Yes | No | No | Yes |
| Completed status | Yes | No | No | Part of todo items |
| Filtered items | Yes | No | Yes (from items + filter) | No (derived) |
| Item count | Yes | No | Yes (from items) | No (derived) |
Result: three pieces of state -- items, filter, inputText.
Step 4: Decide Where State Lives
Find the common ancestor of all components that need each piece of state.
TodoApp
|-- TodoInput (needs: inputText)
|-- FilterBar (needs: filter)
|-- TodoList (needs: items, filter)
|-- TodoItem (needs: individual item)
inputText: only TodoInput uses it --> local to TodoInput
filter: FilterBar sets it, TodoList reads it --> lift to TodoApp
items: TodoInput adds to it, TodoList reads it --> lift to TodoApp
10. State vs the DOM
A critical rule: do not store DOM information in React state. Let React manage the DOM. If you find yourself storing DOM node references, element dimensions, or scroll positions in state, reconsider.
Why Not?
React's model is: state -> UI. Your state describes what should appear, not how it appears on screen. DOM measurements are implementation details.
// BAD: Storing DOM info in state
function Tooltip({ text }) {
const [tooltipHeight, setTooltipHeight] = useState(0);
useEffect(() => {
const el = document.getElementById("tooltip");
setTooltipHeight(el.getBoundingClientRect().height);
});
return <div id="tooltip">{text}</div>;
}
// BETTER: Use a ref for DOM measurements
function Tooltip({ text }) {
const tooltipRef = useRef(null);
return <div ref={tooltipRef}>{text}</div>;
}
State Describes the "What," Not the "How"
// BAD: Tracking DOM details in state
const [inputWidth, setInputWidth] = useState("200px");
// GOOD: Track meaningful UI concepts in state
const [isExpanded, setIsExpanded] = useState(false);
// Let CSS handle the actual widths
When You Need DOM Info
Use useRef for:
- Focusing an input
- Measuring element dimensions
- Scrolling to an element
- Managing animations
- Integrating with non-React libraries
Refs do not trigger re-renders. That's the point.
11. Immutability and State
React state must be treated as immutable. You never modify state directly -- you always create a new value and tell React about it via the setter function.
Why Immutability?
React determines whether to re-render by comparing old and new state values. For objects and arrays, it compares by reference, not by deep equality.
const [user, setUser] = useState({ name: "Alice", age: 25 });
// BAD: Mutation -- React doesn't detect the change
user.name = "Bob";
setUser(user); // Same reference! React thinks nothing changed.
// GOOD: Create a new object
setUser({ ...user, name: "Bob" }); // New reference! React re-renders.
Visualizing Reference Equality
Mutation (BAD):
user (ref: 0x001) --> { name: "Alice", age: 25 }
user.name = "Bob"
user (ref: 0x001) --> { name: "Bob", age: 25 }
setUser(user)
React compares: 0x001 === 0x001? YES --> no re-render
New Object (GOOD):
user (ref: 0x001) --> { name: "Alice", age: 25 }
newUser = { ...user, name: "Bob" }
newUser (ref: 0x002) --> { name: "Bob", age: 25 }
setUser(newUser)
React compares: 0x001 === 0x002? NO --> re-render!
Common Mutation Mistakes
const [items, setItems] = useState(["a", "b", "c"]);
// BAD: These mutate the original array
items.push("d");
items.splice(1, 1);
items[0] = "z";
items.sort();
items.reverse();
// GOOD: These create new arrays
setItems([...items, "d"]); // push
setItems(items.filter((_, i) => i !== 1)); // splice (remove)
setItems(items.map((item, i) => i === 0 ? "z" : item)); // replace
setItems([...items].sort()); // sort (copy first!)
setItems([...items].reverse()); // reverse (copy first!)
Tools That Help with Immutability
For deeply nested state, consider:
- Immer (via
useImmerhook): Write "mutating" code that produces immutable updates under the hood - structuredClone: Deep clones objects (but slower than targeted spreads)
// With Immer (conceptual)
import { useImmer } from "use-immer";
const [person, updatePerson] = useImmer({ name: "Alice", address: { city: "Portland" } });
updatePerson(draft => {
draft.address.city = "Seattle"; // Immer handles immutability
});
12. State Management Complexity Spectrum
As your app grows, your state management needs evolve.
Complexity Spectrum:
Simple Complex
|-----|-----------|-----------|-----------|-----------|-----------|
useState useReducer Context+ Zustand Redux Redux
useState Toolkit Saga/Thunk
When to Use What
| Scenario | Tool | Why |
|---|---|---|
| Toggle a modal | useState | One boolean, one component |
| Complex form with many fields | useReducer | Many related updates, complex transitions |
| Theme/locale across app | Context + useState | Many components need it, changes rarely |
| Shopping cart | Zustand or Redux | Complex operations, persistence needed |
| Server data (API responses) | TanStack Query | Caching, revalidation, loading states |
| URL parameters | Router | Shareable, bookmarkable |
The Decision Flowchart
Is it used by one component only?
YES --> Does it have complex update logic?
YES --> useReducer
NO --> useState
NO --> Is it used by a parent and direct children?
YES --> Lift state to parent, pass via props
NO --> Is it used by many distant components?
YES --> Does it change frequently?
YES --> External store (Zustand, Redux)
NO --> React Context
13. When NOT to Use State
Not everything belongs in state. Misusing state is one of the most common React mistakes.
Use Constants, Not State, for Fixed Values
// BAD: This never changes, why is it state?
const [taxRate, setTaxRate] = useState(0.08);
// GOOD: It's a constant
const TAX_RATE = 0.08;
Use Refs, Not State, for Values That Don't Affect UI
// BAD: Interval ID doesn't affect rendering
const [intervalId, setIntervalId] = useState(null);
// GOOD: Use a ref
const intervalRef = useRef(null);
Use Derived Values, Not State, for Computed Data
// BAD: Storing computed data in state
const [items, setItems] = useState([...]);
const [filteredItems, setFilteredItems] = useState([]);
// GOOD: Compute during render
const [items, setItems] = useState([...]);
const [filterText, setFilterText] = useState("");
const filteredItems = items.filter(item =>
item.name.toLowerCase().includes(filterText.toLowerCase())
);
Decision Matrix: State, Ref, or Derived?
| Question | If Yes |
|---|---|
| Does the UI need to re-render when this changes? | State |
| Does it need to persist between renders but NOT trigger re-render? | Ref |
| Can it be calculated from existing state/props? | Derived value |
| Is it always the same? | Constant |
| Does it come from the URL? | Router param |
| Does it come from an API? | Server state (TanStack Query) |
14. Real-World State Examples
Shopping Cart
function useShoppingCart() {
const [items, setItems] = useState([]);
// Derived values
const itemCount = items.reduce((sum, item) => sum + item.quantity, 0);
const subtotal = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
const tax = subtotal * 0.08;
const total = subtotal + tax;
function addItem(product) {
setItems(prev => {
const existing = prev.find(item => item.id === product.id);
if (existing) {
return prev.map(item =>
item.id === product.id
? { ...item, quantity: item.quantity + 1 }
: item
);
}
return [...prev, { ...product, quantity: 1 }];
});
}
function removeItem(productId) {
setItems(prev => prev.filter(item => item.id !== productId));
}
return { items, itemCount, subtotal, tax, total, addItem, removeItem };
}
Authentication State
function useAuth() {
const [user, setUser] = useState(null);
// Derived
const isAuthenticated = user !== null;
const isAdmin = user?.role === "admin";
async function login(email, password) {
const response = await fetch("/api/login", {
method: "POST",
body: JSON.stringify({ email, password }),
});
const userData = await response.json();
setUser(userData);
}
function logout() {
setUser(null);
}
return { user, isAuthenticated, isAdmin, login, logout };
}
Form State
function ContactForm() {
const [formData, setFormData] = useState({
name: "",
email: "",
message: "",
});
const [status, setStatus] = useState("idle");
// Derived
const isValid = formData.name.trim() !== ""
&& formData.email.includes("@")
&& formData.message.trim() !== "";
const canSubmit = isValid && status !== "submitting";
function handleChange(field, value) {
setFormData(prev => ({ ...prev, [field]: value }));
}
async function handleSubmit(e) {
e.preventDefault();
if (!canSubmit) return;
setStatus("submitting");
try {
await fetch("/api/contact", {
method: "POST",
body: JSON.stringify(formData),
});
setStatus("success");
} catch {
setStatus("error");
}
}
return (
<form onSubmit={handleSubmit}>
<input
value={formData.name}
onChange={e => handleChange("name", e.target.value)}
placeholder="Name"
/>
<input
value={formData.email}
onChange={e => handleChange("email", e.target.value)}
placeholder="Email"
/>
<textarea
value={formData.message}
onChange={e => handleChange("message", e.target.value)}
placeholder="Message"
/>
<button disabled={!canSubmit}>
{status === "submitting" ? "Sending..." : "Send"}
</button>
{status === "success" && <p>Message sent.</p>}
{status === "error" && <p>Something went wrong. Try again.</p>}
</form>
);
}
Theme State
function ThemeProvider({ children }) {
const [theme, setTheme] = useState(() => {
const saved = localStorage.getItem("theme");
return saved || "light";
});
const isDark = theme === "dark";
function toggleTheme() {
setTheme(prev => {
const next = prev === "light" ? "dark" : "light";
localStorage.setItem("theme", next);
return next;
});
}
return (
<ThemeContext.Provider value={{ theme, isDark, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
Key Takeaways
- State is component memory -- data that persists between renders and triggers re-renders when changed.
- Regular variables don't work because they reset on every render and don't trigger re-renders.
- Props are read-only inputs from parents; state is mutable data owned by the component.
- State is a snapshot -- within a single render, state values are fixed.
- Each component instance has its own isolated state.
- Single source of truth -- every piece of state should have exactly one owner.
- Types of state: local, shared, global, server, URL, form -- each with appropriate tools.
- Minimal state -- only store what you can't derive or compute.
- Don't store DOM info in state -- use refs for that.
- State must be treated as immutable -- always create new values, never mutate.
- Not everything is state -- constants, refs, derived values, and URL params are alternatives.
Explain-It Challenge
Pick one concept from this section and explain it to someone with zero React knowledge. Use an everyday analogy (no code). Some ideas:
- "State as a snapshot in time" -- explain using a camera or a scoreboard
- "Props vs State" -- explain using a restaurant analogy (menu vs order)
- "Single source of truth" -- explain using a shared Google Doc vs separate Word files
- "Immutability" -- explain using a whiteboard that you must photograph before erasing
If you can explain it clearly without jargon, you understand it.
Navigation: ← 2.3 Overview · Next → 2.3.b — useState Hook