What Happens When You Type a URL and Press Enter
DNS lookups, TCP and TLS handshakes, HTTP requests, servers, and the browser's rendering pipeline. The whole journey of a web page, explained so it finally clicks.
Read →Most state problems come from putting state in the wrong place, not from picking the wrong library. Local, shared, server, URL and form state, and the simplest tool for each one.
"Which state management library should I use?" is one of the most asked React questions, and it's usually the wrong question. Most apps that feel messy don't need a different library; they need state to live in the right place. Once you separate state by kind, the choice gets simple.
Each one has a natural home.
useState and useReducerKeep state as close as possible to where it's used.
function Accordion({ title, children }) {
const [open, setOpen] = useState(false)
return (
<section>
<button onClick={() => setOpen(o => !o)} aria-expanded={open}>{title}</button>
{open && children}
</section>
)
}
When state has several related pieces and transitions, useReducer keeps the logic in one place.
Don't store values you can calculate. If you have items, you don't need separate itemCount state; compute it during render. Duplicated state drifts out of sync. (The effect-loop bugs this causes.)
When two components need the same state, lift it to their closest common parent and pass it down as props. That's often enough.
When props would travel through many layers, use context:
const ThemeContext = createContext('light')
function App() {
const [theme, setTheme] = useState('light')
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Layout />
</ThemeContext.Provider>
)
}
Context is great for values that change rarely: theme, current user, language. Every consumer re-renders when the value changes, so for frequently changing state across many components, a dedicated store is better.
Reach for a store when you have genuinely global client state that changes often or has complex update logic: a large cart, an editor's document, an offline queue.
All are good. Pick based on team preference and app size, not hype.
This is the big one. Data from your API isn't really your state; it's a cache of the server's state. It needs fetching, caching, deduplication, background refreshing, retries and invalidation after mutations. Building that by hand with useEffect and useState is where many apps get tangled.
Libraries like TanStack Query and SWR handle it:
const { data: orders, isLoading, error } = useQuery({
queryKey: ['orders', userId],
queryFn: () => fetchOrders(userId),
})
After moving server data into a library like this, many teams find they barely need a global store at all. In frameworks with Server Components, a lot of data fetching moves to the server entirely. (Rendering strategies.)
Filters, search queries, pagination, sorting and selected tabs belong in the URL:
/products?category=shoes&sort=price&page=2
Benefits: links can be shared and bookmarked, the back button works, refreshing keeps the view, and search engines can index meaningful pages. Read and write it with your router's search-params hooks.
Simple forms are fine with useState. For larger forms with validation, dynamic fields and error messages, a library like React Hook Form reduces re-renders and boilerplate, and pairs well with schema validation. (Validation done right.)
| State | Where it should live |
|---|---|
| Open/closed, hover, input text | useState in the component |
| Shared by a few nearby components | Lift to the common parent |
| Theme, user, locale | Context |
| Frequently changing global client state | Zustand, Redux Toolkit or similar |
| Data from the API | TanStack Query, SWR, or server-side fetching |
| Filters, search, pagination | The URL |
| Complex forms | A form library |
isLoading flagsUsually not. Many apps manage well with local state, context, a data-fetching library for server state and the URL. Redux Toolkit is a good choice for large apps with complex shared client state.
Client state exists only in the browser, like whether a modal is open. Server state is data owned by the server and cached on the client, which needs fetching, caching and synchronisation.
For values many components need that change infrequently, such as theme, locale or the current user. For frequently changing shared state, a dedicated store avoids unnecessary re-renders.
DNS lookups, TCP and TLS handshakes, HTTP requests, servers, and the browser's rendering pipeline. The whole journey of a web page, explained so it finally clicks.
Read →There are dozens of HTTP status codes, but about fifteen cover nearly everything. What each one means, when to return it, and the mix-ups (401 vs 403, 301 vs 302) that confuse everyone.
Read →A, AAAA, CNAME, MX, TXT and NS records in plain English, why DNS changes seem slow, and the troubleshooting commands that tell you what is really going on.
Read →