Usestate And Useeffect

useState gives a component memory and useEffect lets it talk to the outside world -- together they cover the majority of what real React features need.

May 15, 20264 min read2 / 7

The Order form in the previous post had hardcoded pizza options. That works for a demo, but a real pizzeria's menu changes. The options need to come from the API.

That is the exact problem useEffect was built for.

useState Recap

useState gives a component a piece of memory that persists across re-renders. When the setter is called, React re-renders the component with the new value.

JSX
const [pizzaType, setPizzaType] = useState("pepperoni");

pizzaType is the current value. setPizzaType replaces it. The string "pepperoni" is the initial value -- used only on the very first render.

useEffect: Running Code After Render

useEffect runs a function after the component renders. You use it any time your component needs to reach outside React -- fetch from an API, set up a subscription, write to localStorage.

JSX
useEffect(() => { // this runs after the component renders }, []);

The second argument is the dependency array. It controls when the effect re-runs:

  • [] -- run once, after the first render only
  • [value] -- run after every render where value changed
  • no second argument -- run after every render (almost never what you want)

For fetching initial data, [] is correct. You want to fetch once when the component mounts, not on every keystroke.

Fetching Pizza Types

Add a pizzaTypes state to hold the API data, a loading state to track whether the fetch is complete, and a useEffect to trigger the fetch:

JSX
import { useState, useEffect } from "react"; const Order = () => { const [pizzaTypes, setPizzaTypes] = useState([]); const [pizzaType, setPizzaType] = useState("pepperoni"); const [pizzaSize, setPizzaSize] = useState("M"); const [loading, setLoading] = useState(true); useEffect(() => { fetch("/api/pizzas") .then((res) => res.json()) .then((data) => { setPizzaTypes(data); setLoading(false); }); }, []); // render ... };

useState stores the component's data; useEffect fetches it once after mount ExpanduseState stores the component's data; useEffect fetches it once after mount

Handling the Loading State

While the fetch is in flight, pizzaTypes is an empty array and loading is true. Trying to render the options from an empty array would produce a blank select. Worse, if anything tried to access pizzaTypes[0].name, it would throw.

A ternary on loading keeps things clean:

JSX
{loading ? ( <p>Loading pizzas...</p> ) : ( <select id="pizza-type" value={pizzaType} onChange={(e) => setPizzaType(e.target.value)} > {pizzaTypes.map((pizza) => ( <option key={pizza.id} value={pizza.id}> {pizza.name} </option> ))} </select> )}

The select only renders once the data is available.

Showing the Price

Each pizza from the API includes a sizes object: { S: 9.99, M: 12.99, L: 15.99 }. The selected pizza and size together give you the price.

Intl.NumberFormat formats it correctly for the user's locale:

JSX
const intl = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", }); // inside the component render: {!loading && ( <p> {intl.format( pizzaTypes.find((p) => p.id === pizzaType)?.sizes[pizzaSize] ?? 0 )} </p> )}

Intl.NumberFormat is built into every modern browser and Node. It handles symbol placement, decimal separators, and currency codes -- no library needed.

With the list rendering correctly, the next concept to nail is how React tracks which item is which when the list changes. The next post covers React keys and why using array index as the key causes silent bugs.

The lifecycle above also applies when you add custom hooks later -- extracting useState + useEffect into a named function is exactly what makes hooks composable.

The Essentials

  1. useState gives a component memory. The setter triggers a re-render; the new value is not available until that render.
  2. useEffect(() => {}, []) runs once after the first render. It is the correct place for API calls, subscriptions, and any side effect that should happen on mount.
  3. The dependency array controls when the effect re-runs: [] for once, [val] for every change to val, nothing for every render (rare).
  4. loading state prevents rendering before data arrives. A ternary that swaps out the whole section is cleaner than sprinkling optional chaining everywhere.
  5. Intl.NumberFormat formats currency without a third-party library. Construct it outside the render function so it is not rebuilt on every render.

Further Reading and Watching

Practice what you just read.

Controlled Pizza Selector
1 exercise