React State Management: A 2025 Interview Guide
Your Interview Guide to React State Management
'How do you manage state in React?' This is one of the most common high-level questions. Your answer shows your seniority and architectural understanding.
There is no single right answer. The correct answer is, 'It depends on the type of state.'
In this guide, we'll explore the four main types of state and the tools to manage them.
1. Local State (UI State)
This is state that is only needed by a single component (or its direct children). Examples: Is a modal open? What is the value of an input field?
- Tool:
useStateoruseReducer. - Key Point: Don't use a global library for state that is local. It's overkill.
2. Global State (Client State)
This is state that needs to be shared across many components that are not directly related. Examples: The current user, the site's theme ('dark'/'light'), the items in a shopping cart.
- Tool for simple, low-frequency updates:
useContext. - Tool for complex, high-frequency updates: A dedicated library like Redux or Zustand.
3. Server State (Cached State)
This is data that comes from an API. It's not your app's state, you are just 'caching' a copy of it. Examples: A list of users, product details, a user's feed.
- Tool: React Query (TanStack Query) or SWR.
- Key Point: You should not store server state in
useStateor Redux. These tools solve caching, re-fetching, and loading/error states for you.
4. URL State
This is state that should be stored in the URL query parameters. Examples: Search filters, the current page number, a specific tab being open.
- Tool: React Router (using
useSearchParams). - Key Point: This makes your state shareable and bookmarkable.
In this cluster, we will dive into the most important tools for global and server state.
Context API vs. Redux: The Real-World Showdown
Interview Question: 'When do you use Context API, and when do you use Redux?'
This is a classic question. Your answer should show you understand the performance trade-offs.
1. React Context API
What it is: A built-in React feature for solving 'prop drilling.' It lets you pass a value deep down the component tree without passing it as a prop at every level.
When to use it: For global state that does not change often.
- User Authentication (the current user object).
- Site theme ('dark' or 'light').
- A shopping cart on a very simple site.
The Big Problem (The 'Gotcha'): When a value in a Context Provider changes, all components that consume (useContext) that context will re-render, even if they only care about a different part of the value.
Code Example: Context API
import React, { useContext, createContext, useState } from 'react';
// 1. Create context
const UserContext = createContext();
// 2. Create provider
function App() {
const [user, setUser] = useState({ name: 'Guest' });
return (
);
}
// 3. Consume in a child
function Header() {
const { user } = useContext(UserContext);
return Welcome, {user.name}
;
}2. Redux (with Redux Toolkit)
What it is: A powerful, external library for managing global application state. It uses a single store and 'reducers' to handle state changes predictably.
When to use it: For complex, high-frequency state changes that are shared across the app.
- A complex shopping cart (quantity, price, etc.).
- The state of a multi-step form.
- App-wide state that is modified from many different places.
The Big Advantage: Performance. When a value in the Redux store changes, only the components that are subscribed to that specific piece of state (using useSelector) will re-render. It is highly optimized.
Code Example: Redux Toolkit (Modern Redux)
// 1. Create a 'slice' of state
import { createSlice, configureStore, Provider } from '@reduxjs/toolkit';
import { useSelector, useDispatch } from 'react-redux';
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: (state) => { state.value += 1; },
},
});
const { increment } = counterSlice.actions;
// 2. Configure the store
const store = configureStore({ reducer: { counter: counterSlice.reducer } });
// 3. Provide the store
// In index.js:
// 4. Consume in a child
function Counter() {
// Selects ONLY the data you need
const count = useSelector((state) => state.counter.value);
const dispatch = useDispatch();
return ;
}The simple answer: 'I use Context for simple, low-frequency state like theme or auth. I use Redux when I have complex, high-frequency state, because its selector-based subscriptions are far more performant and prevent unnecessary re-renders.'
Introduction to Zustand: Is It the New Redux?
Interview Question: 'Have you used any state managers besides Redux?'
Mentioning Zustand is a great way to show you are up-to-date with the modern React ecosystem.
What it is: Zustand is a small, fast, and simple global state management library. It's often described as 'Redux-like power with Context-like simplicity.'
Why people like it:
- Minimal Boilerplate: You don't need Slices, Reducers, Actions, or Providers. You just create a store and use it.
- It's just a hook: You call
useStore()and get your state and actions. - Performant: Like Redux, it re-renders components only if the state they subscribe to actually changes.
Code Example: Zustand Store
Here is a complete store and component in one file. Compare this to the Redux example!
import React from 'react';
import create from 'zustand';
// 1. Create the store. That's it.
// It's a function that returns an object with your state
// and the functions that modify it.
const useCounterStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 })
}));
// 2. Use the hook in your component
function Counter() {
// You can select the entire store
// const store = useCounterStore();
// OR (better) select just what you need to optimize renders
const count = useCounterStore((state) => state.count);
const increment = useCounterStore((state) => state.increment);
return (
Count: {count}
);
}
// 3. Use it in another component... no Provider needed!
function Controls() {
const reset = useCounterStore((state) => state.reset);
return ;
}
The simple answer: 'Yes, I've used Zustand. It's a lightweight global state manager that uses a simple hook-based API. It's great for projects that need the performance of Redux without all the boilerplate, as it avoids the need for Providers and reducers.'
React Query (TanStack Query): Stop Managing Server State
Interview Question: 'How do you handle data fetching, caching, and loading states?'
If your answer is 'I use useState and useEffect', you're missing the modern, correct solution. The best answer is React Query (now TanStack Query).
The Problem: Server state (API data) is different from client state. It needs caching, re-fetching (to stay fresh), loading states, error states, and more. useState gives you none of this.
The Solution: React Query is a 'server state manager.' It handles all the hard parts of data fetching for you.
Code Example: 'Before' (useState) vs. 'After' (React Query)
'Before': The useEffect Mess
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
setError(null);
fetch(`https://api.example.com/users/${userId}`)
.then(res => res.json())
.then(setUser)
.catch(setError)
.finally(() => setLoading(false));
}, [userId]);
if (loading) return 'Loading...';
if (error) return 'An error occurred!';
return User: {user.name}
;
}
// What about caching? What about re-fetching when I re-focus the window?
// This is a lot of code to write for one query.'After': The useQuery Dream
First, set up the QueryClientProvider in your `App.js`.
// App.js (or index.js)
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient();
function App() {
return (
{/ Rest of your app /}
);
}
// --- Now in your component ---
// UserProfile.js
import { useQuery } from '@tanstack/react-query';
// 1. Create a fetch function
const fetchUser = async (userId) => {
const res = await fetch(`https://api.example.com/users/${userId}`);
return res.json();
};
function UserProfile({ userId }) {
// 2. Call the useQuery hook
const { data: user, isLoading, isError, error } = useQuery({
queryKey: ['user', userId], // A unique key for this query
queryFn: () => fetchUser(userId),
});
// 3. React Query handles all the states for you!
if (isLoading) return 'Loading...';
if (isError) return `An error occurred: ${error.message}`;
return User: {user.name}
;
}
// This code automatically gives you caching, re-fetching on window focus,
// and simple loading/error states.The simple answer: 'For server state, I don't use useState. I use React Query. It simplifies my components and gives me caching, background re-fetching, and loading/error states out of the box, which useEffect doesn't.'


