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.
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.
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.
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 wherevaluechanged- 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:
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 ...
}; 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:
{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:
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
useStategives a component memory. The setter triggers a re-render; the new value is not available until that render.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.- The dependency array controls when the effect re-runs:
[]for once,[val]for every change toval, nothing for every render (rare). loadingstate prevents rendering before data arrives. A ternary that swaps out the whole section is cleaner than sprinkling optional chaining everywhere.Intl.NumberFormatformats currency without a third-party library. Construct it outside the render function so it is not rebuilt on every render.
Further Reading and Watching
- useEffect (React docs) -- the official reference including escape hatches, cleanup functions, and the strict mode double-invoke behavior
- useState (React docs) -- covers batching, functional updates, and why you should not mutate state directly
- useEffect Explained (Fireship) -- visual walkthrough of the dependency array and common mistakes
Practice what you just read.