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

  1. State as Component Memory
  2. Why Regular Variables Fail
  3. Props vs State
  4. State as a Snapshot in Time
  5. State Is Isolated Per Component Instance
  6. State Ownership and Single Source of Truth
  7. Types of State
  8. State Design Principles
  9. Thinking in State: Identifying What Should Be State
  10. State vs the DOM
  11. Immutability and State
  12. State Management Complexity Spectrum
  13. When NOT to Use State
  14. Real-World State Examples
  15. Key Takeaways
  16. 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:

  1. handleClick runs
  2. count changes from 0 to 1 in memory
  3. console.log shows 1
  4. 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:

  1. A way to persist data between renders
  2. 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

AspectPropsState
Who owns itParent componentThe component itself
Who can change itOnly the parentOnly the owning component
Mutable inside component?No (read-only)Yes (via setter function)
Direction of flowParent to child (top-down)Local to the component
Triggers re-render?Yes (when parent passes new props)Yes (when state changes)
PurposeConfigure a component from outsideTrack data that changes over time
AnalogyFunction parametersFunction's local memory
InitializationSet by parent in JSXSet 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

  1. State should live in the lowest common ancestor of all components that need it
  2. If only one component uses the state, keep it local to that component
  3. If siblings need shared state, lift it to their parent
  4. 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

TypeScopePersistenceTool
LocalSingle componentComponent lifetimeuseState
SharedSibling componentsCommon parent lifetimeLifted useState
GlobalEntire appApp lifetimeContext, Zustand, Redux
ServerAPI dataCache-dependentTanStack Query, SWR
URLCurrent routeBrowser historyRouter params/search
FormForm componentForm lifetimeReact 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:

  1. Does it change over time? If no, it's a constant, not state.
  2. Is it passed from a parent via props? If yes, it's not state in this component.
  3. 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

DataChanges?From props?Computed?State?
Todo itemsYesNoNoYes
Filter valueYesNoNoYes
Input textYesNoNoYes
Completed statusYesNoNoPart of todo items
Filtered itemsYesNoYes (from items + filter)No (derived)
Item countYesNoYes (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 useImmer hook): 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

ScenarioToolWhy
Toggle a modaluseStateOne boolean, one component
Complex form with many fieldsuseReducerMany related updates, complex transitions
Theme/locale across appContext + useStateMany components need it, changes rarely
Shopping cartZustand or ReduxComplex operations, persistence needed
Server data (API responses)TanStack QueryCaching, revalidation, loading states
URL parametersRouterShareable, 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?

QuestionIf 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

  1. State is component memory -- data that persists between renders and triggers re-renders when changed.
  2. Regular variables don't work because they reset on every render and don't trigger re-renders.
  3. Props are read-only inputs from parents; state is mutable data owned by the component.
  4. State is a snapshot -- within a single render, state values are fixed.
  5. Each component instance has its own isolated state.
  6. Single source of truth -- every piece of state should have exactly one owner.
  7. Types of state: local, shared, global, server, URL, form -- each with appropriate tools.
  8. Minimal state -- only store what you can't derive or compute.
  9. Don't store DOM info in state -- use refs for that.
  10. State must be treated as immutable -- always create new values, never mutate.
  11. 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