Loading
A practical decision tree for choosing between useState, useReducer, Context, Zustand, and URL state in React applications.
Not all state is created equal. React apps fail when developers shove everything into one bucket. There are five distinct categories, and each has a correct home:
useState.useReducer.useSearchParams.The decision tree is simple: ask where the state needs to be visible, how long it needs to live, and whether it needs to survive a page refresh. That determines the layer.
useState is your default. If the state controls something visual and only one component cares about it, this is the answer.
When to move on from useState:
useState calls in one componentWhen state changes follow rules — especially when the next state depends on the current state and an action — useReducer brings order to chaos.
The reducer makes state transitions explicit and testable. You can write unit tests for formReducer without rendering anything.
When multiple components across different parts of the tree need the same state, prop drilling breaks down. Zustand gives you a store without the boilerplate of Redux.
Use Zustand when:
Zustand stores are just hooks. No providers, no wrappers, no context hell.
Filters, search queries, pagination, and sort orders belong in the URL. This makes the state shareable, bookmarkable, and browser-history compatible.
The rule: if a user would be confused that the state reset when they refreshed or shared the URL, it belongs in the URL.
When you add new state, run through this checklist:
useStateuseReduceruseSearchParams)persist middleware)The most common mistake is over-engineering state management from the start. Begin with useState. Move to useReducer when transitions get complex. Reach for Zustand only when you have genuine cross-component needs. Put shareable state in the URL. Every layer exists for a reason — skip the ones you don't need.
function Dropdown({ items }: { items: string[] }) {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<button onClick={() => setIsOpen(!isOpen)}>Toggle</button>
{isOpen && (
<ul>
{items.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
)}
</div>
);
}interface FormState {
values: Record<string, string>;
errors: Record<string, string>;
isSubmitting: boolean;
}
type FormAction =
| { type: "SET_FIELD"; field: string; value: string }
| { type: "SET_ERROR"; field: string; error: string }
| { type: "SUBMIT" }
| { type: "SUBMIT_SUCCESS" }
| { type: "SUBMIT_FAILURE"; errors: Record<string, string> };
function formReducer(state: FormState, action: FormAction): FormState {
switch (action.type) {
case "SET_FIELD":
return {
...state,
values: { ...state.values, [action.field]: action.value },
errors: { ...state.errors, [action.field]: "" },
};
case "SUBMIT":
return { ...state, isSubmitting: true };
case "SUBMIT_SUCCESS":
return { ...state, isSubmitting: false, errors: {} };
case "SUBMIT_FAILURE":
return { ...state, isSubmitting: false, errors: action.errors };
default:
return state;
}
}import { create } from "zustand";
interface ProgressStore {
completedLessons: Set<string>;
markComplete: (lessonId: string) => void;
isComplete: (lessonId: string) => boolean;
}
const useProgressStore = create<ProgressStore>((set, get) => ({
completedLessons: new Set(),
markComplete: (lessonId) =>
set((state) => ({
completedLessons: new Set([...state.completedLessons, lessonId]),
})),
isComplete: (lessonId) => get().completedLessons.has(lessonId),
}));"use client";
import { useSearchParams, useRouter } from "next/navigation";
function FilterBar() {
const searchParams = useSearchParams();
const router = useRouter();
const currentTag = searchParams.get("tag") ?? "all";
function setTag(tag: string): void {
const params = new URLSearchParams(searchParams.toString());
if (tag === "all") {
params.delete("tag");
} else {
params.set("tag", tag);
}
router.push(`?${params.toString()}`);
}
return (
<div>
{["all", "react", "typescript", "css"].map((tag) => (
<button key={tag} onClick={() => setTag(tag)} data-active={tag === currentTag}>
{tag}
</button>
))}
</div>
);
}// Zustand with persistence for offline support
import { create } from "zustand";
import { persist, createJSONStorage } from "zustand/middleware";
const useSettingsStore = create(
persist<SettingsState>(
(set) => ({
fontSize: 16,
theme: "dark",
setFontSize: (size: number) => set({ fontSize: size }),
setTheme: (theme: string) => set({ theme }),
}),
{
name: "user-settings",
storage: createJSONStorage(() => localStorage),
}
)
);