FroquizFroquiz
HomeQuizzesSenior ChallengeGet CertifiedBlogAbout
Sign InStart Quiz
Sign InStart Quiz
Froquiz

The most comprehensive quiz platform for software engineers. Test yourself with 10000+ questions and advance your career.

LinkedIn

Platform

  • Start Quizzes
  • Topics
  • Blog
  • My Profile
  • Sign In

About

  • About Us
  • Contact

Legal

  • Privacy Policy
  • Terms of Service

Β© 2026 Froquiz. All rights reserved.Built with passion for technology
Blog & Articles

React State Management: useState, Context, Zustand, Redux and When to Use Each

Understand every React state management approach. Covers local state, lifting state, Context API, Zustand, Redux Toolkit, and how to choose the right tool for your app.

Yusuf SeyitoğluMarch 11, 20261 views10 min read

React State Management: useState, Context, Zustand, Redux and When to Use Each

State management is the most debated topic in React. The ecosystem has dozens of solutions, and new developers often reach for complex global state libraries before they need them. Understanding the full spectrum β€” from local state to Redux β€” and knowing when to use each is a skill that sets experienced React developers apart.

The State Management Spectrum

Think of state management as a spectrum from simple to complex:

code
useState β†’ lifted state β†’ Context β†’ Zustand/Jotai β†’ Redux Toolkit β”‚ β”‚ β”‚ β”‚ β”‚ simple shared global global complex local between read-heavy with actions enterprise state siblings

Start at the left. Move right only when you have a concrete reason.

1. Local State with useState

The right choice for most state. If only one component needs the data, keep it there.

jsx
function SearchBar() { const [query, setQuery] = useState(""); const [results, setResults] = useState([]); const [loading, setLoading] = useState(false); async function handleSearch(e) { e.preventDefault(); setLoading(true); const data = await searchApi(query); setResults(data); setLoading(false); } return ( <form onSubmit={handleSearch}> <input value={query} onChange={e => setQuery(e.target.value)} /> {loading && <Spinner />} {results.map(r => <ResultItem key={r.id} result={r} />)} </form> ); }

When to use local state

  • Form inputs
  • UI state (open/closed, selected tab, hover)
  • Data used only by one component
  • Loading and error states for a single fetch

2. Lifting State

When two sibling components need to share state, lift it to their closest common parent.

jsx
function App() { -- Lifted -- both children need this const [selectedUserId, setSelectedUserId] = useState(null); return ( <div> <UserList onSelect={setSelectedUserId} selectedId={selectedUserId} /> <UserDetail userId={selectedUserId} /> </div> ); }

Lifting state is often the right answer before reaching for a state library.

3. Context API

Context solves prop drilling β€” passing props through many layers just to reach a deeply nested component.

jsx
// ThemeContext.jsx const ThemeContext = createContext(); export function ThemeProvider({ children }) { const [theme, setTheme] = useState("light"); const toggle = () => setTheme(t => t === "light" ? "dark" : "light"); return ( <ThemeContext.Provider value={{ theme, toggle }}> {children} </ThemeContext.Provider> ); } export const useTheme = () => { const ctx = useContext(ThemeContext); if (!ctx) throw new Error("useTheme must be used within ThemeProvider"); return ctx; }; // Deep in the tree function ThemeButton() { const { theme, toggle } = useTheme(); // no prop drilling return <button onClick={toggle}>Current: {theme}</button>; }

Context performance caveat

Every consumer re-renders when the context value changes. Split contexts by update frequency to avoid unnecessary re-renders:

jsx
-- Bad: one context object changes frequently const AppContext = createContext({ user, theme, cart }); -- Good: separate contexts const UserContext = createContext(user); const ThemeContext = createContext(theme); const CartContext = createContext(cart);

When to use Context

  • Infrequently updated global data: current user, theme, locale, feature flags
  • Avoiding prop drilling 3+ levels deep
  • Small to medium apps with modest state complexity

4. Zustand

Zustand is a minimal, fast global state library. No boilerplate, no providers, works outside components.

javascript
// store.js import { create } from "zustand"; const useCartStore = create((set, get) => ({ items: [], addItem: (product) => set((state) => { const existing = state.items.find(i => i.id === product.id); if (existing) { return { items: state.items.map(i => i.id === product.id ? { ...i, qty: i.qty + 1 } : i ), }; } return { items: [...state.items, { ...product, qty: 1 }] }; }), removeItem: (id) => set((state) => ({ items: state.items.filter(i => i.id !== id), })), clearCart: () => set({ items: [] }), getTotal: () => get().items.reduce((sum, i) => sum + i.price * i.qty, 0), })); export default useCartStore;
jsx
// Any component, anywhere in the tree function CartIcon() { const items = useCartStore(state => state.items); return <span>Cart ({items.length})</span>; } function AddToCartButton({ product }) { const addItem = useCartStore(state => state.addItem); return <button onClick={() => addItem(product)}>Add to cart</button>; }

Why developers love Zustand

  • No Provider wrapper needed
  • Minimal boilerplate β€” store is just a function
  • Selectors prevent unnecessary re-renders: useStore(state => state.items) only re-renders when items changes
  • Can be used outside React (in utilities, event handlers, async code)

When to use Zustand

  • Global state shared across many components
  • Moderate complexity β€” enough to justify a library, not complex enough for Redux
  • When you want global state without Context re-render issues

5. Redux Toolkit

Redux Toolkit (RTK) is the modern, official Redux approach. It eliminates the verbosity of classic Redux while keeping its predictability.

javascript
// store/cartSlice.js import { createSlice } from "@reduxjs/toolkit"; const cartSlice = createSlice({ name: "cart", initialState: { items: [], total: 0 }, reducers: { addItem: (state, action) => { const existing = state.items.find(i => i.id === action.payload.id); if (existing) { existing.qty += 1; } else { state.items.push({ ...action.payload, qty: 1 }); } state.total = state.items.reduce((s, i) => s + i.price * i.qty, 0); }, removeItem: (state, action) => { state.items = state.items.filter(i => i.id !== action.payload); state.total = state.items.reduce((s, i) => s + i.price * i.qty, 0); }, }, }); export const { addItem, removeItem } = cartSlice.actions; export default cartSlice.reducer;
javascript
// store/index.js import { configureStore } from "@reduxjs/toolkit"; import cartReducer from "./cartSlice"; export const store = configureStore({ reducer: { cart: cartReducer }, });
jsx
// Component import { useSelector, useDispatch } from "react-redux"; import { addItem } from "./store/cartSlice"; function AddToCartButton({ product }) { const dispatch = useDispatch(); const itemCount = useSelector(state => state.cart.items.length); return ( <button onClick={() => dispatch(addItem(product))}> Add to Cart ({itemCount}) </button> ); }

RTK Query: data fetching built in

RTK Query handles server state β€” fetching, caching, invalidation β€” without extra libraries:

javascript
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react"; const api = createApi({ reducerPath: "api", baseQuery: fetchBaseQuery({ baseUrl: "/api" }), endpoints: (builder) => ({ getUsers: builder.query({ query: () => "/users" }), createUser: builder.mutation({ query: (body) => ({ url: "/users", method: "POST", body }), invalidatesTags: ["Users"], }), }), }); export const { useGetUsersQuery, useCreateUserMutation } = api; -- In component const { data: users, isLoading } = useGetUsersQuery();

When to use Redux Toolkit

  • Large applications with complex, interconnected state
  • Teams that benefit from strict state discipline and DevTools time-travel debugging
  • When you need RTK Query's powerful server state management
  • Codebases with many developers where predictable state updates prevent bugs

Choosing the Right Tool

ScenarioRecommendation
Form input, UI toggleuseState
Shared between 2-3 siblingsLift state
Global theme, auth, localeContext API
Global UI state, cart, filtersZustand
Large app, complex state, DevToolsRedux Toolkit
Server data (loading, caching)RTK Query or React Query

Practice React on Froquiz

State management is a key topic in mid-level and senior React interviews. Test your React knowledge on Froquiz across all difficulty levels.

Summary

  • Start with useState β€” it handles most UI state perfectly
  • Lift state when siblings need to share it, before reaching for global solutions
  • Context solves prop drilling for infrequently-updated global data
  • Zustand offers global state with minimal boilerplate and no Provider
  • Redux Toolkit is the right choice for large, complex apps β€” use RTK Query for server state
  • Matching the tool to the complexity prevents over-engineering

About Author

Yusuf Seyitoğlu

Author β†’

Other Posts

  • GraphQL Schema Design: Types, Resolvers, Mutations and Best PracticesMar 12
  • System Design Fundamentals: Scalability, Load Balancing, Caching and DatabasesMar 12
  • Java Collections Deep Dive: ArrayList, HashMap, TreeMap, LinkedHashMap and When to Use EachMar 12
All Blogs