Context versus Redux, Redux Toolkit, Zustand, TanStack Query, client state versus server state, and how to justify the choice you made.
14 questions2 fresher, 9 mid-level, 3 senior
State management is where interviews stop testing recall and start testing judgement. There is no right answer to "Context or Redux", and an interviewer who asks it is listening for the trade-off, not the verdict. The strongest answers name the constraint first: how many components read this, how often does it change, and did it come from a server.
If you have shipped an app, use it. "We started with context, it re-rendered the whole tree on every keystroke, so we moved the search state into a store" is worth more than any definition.
1.What are the state management options in React, and how do you choose between them?
Mid-level
Local useState, lifted state, context, a client store such as Redux Toolkit or Zustand, and a server state library such as TanStack Query. Choose by asking who needs the value, how often it changes, and whether it came from a server.
`useState` in the component. The default. Most state is used in one place and belongs there.
Lifted state. Two or three components need it: move it to their nearest common parent.
URL state. Filters, tabs, pagination and the current record belong in the URL, so a link reproduces the view.
Context. A value a whole subtree needs and that rarely changes: theme, locale, the current user.
A client store. Large, widely shared, frequently updated state written from many places.
A server state library. Anything fetched: it needs caching, revalidation, deduplication and error handling that none of the above provide.
Likely follow-up questions
Where would you keep the current search filter?
Does every app need Redux?
2.Is the Context API a state management solution?
Mid-level
Not on its own. Context is a way to transport a value down the tree without prop drilling. It has no opinion on how state is stored or updated, and no re-render optimisation, so it is dependency injection rather than state management.
The state still lives in a useState or useReducer somewhere. Context only moves the value. That is enough for a theme, and it starts to hurt when the value changes often, because every consumer re-renders whenever the provider value changes, regardless of which field they read.
The mistake that makes context look slow.
// A new object every render: every consumer re-renders
<AppContext.Provider value={{ user, setUser }}>
// Stable between renders
const value = useMemo(() => ({ user, setUser }), [user]);
<AppContext.Provider value={value}>
Split contexts by concern and by update frequency: a rarely changing AuthContext, a frequently changing CartContext.
A common trick is two contexts, one for the state and one for the dispatch function, so components that only dispatch never re-render.
useReducer plus context is a reasonable small scale Redux, minus devtools, middleware and selector based subscriptions.
Likely follow-up questions
How would you stop unrelated consumers re-rendering?
Redux is a predictable state container. Its three principles are a single source of truth, state that is read only and changed only by dispatching actions, and changes described by pure reducer functions.
Store. One object tree holding the application state.
Action. A plain object with a type describing what happened, not what to change.
Reducer.(state, action) => newState, pure, with no side effects and no mutation.
Dispatch. The only way to trigger a change.
Selector. A function that reads a slice of the store, so components do not depend on its shape.
The pay-off is traceability. Every change is an action with a name, so you can log them, replay them, time travel through them in devtools, and write a test that asserts a reducer's output for a given action. The cost is indirection and boilerplate, which is what Redux Toolkit exists to remove.
Likely follow-up questions
Why must reducers be pure?
What is time travel debugging?
4.What is Redux Toolkit and why is it the recommended way to use Redux?
Mid-level
Redux Toolkit is the official, batteries-included Redux package. createSlice generates actions and reducers together, Immer lets you write mutating syntax that produces immutable updates, and the store comes preconfigured with thunks and devtools.
The classic complaint about Redux was that adding one field touched an action type file, an action creator file, a reducer and a constant. createSlice collapses that into one declaration. RTK Query, which ships in the same package, then covers data fetching and caching.
Likely follow-up questions
What is Immer doing under the hood?
What is RTK Query?
5.What is the difference between Redux and the Context API?
Mid-level
Context passes a value down the tree and re-renders every consumer when it changes. Redux is a full state container with selector based subscriptions, middleware and devtools, so components re-render only when the slice they select actually changes.
Context
Redux Toolkit
What it is
Value transport
State container
Re-render granularity
Every consumer
Only components whose selected slice changed
Async and side effects
Your problem
Middleware: thunks, listeners, RTK Query
Debugging
React DevTools
Action log, time travel, state diffs
Setup cost
None
A store, slices, a provider
The honest summary: context is right for low frequency, tree wide values. Redux earns its setup cost when state is updated from many places, when you need to see why it changed, or when the team is large enough that a convention is worth more than brevity.
Likely follow-up questions
Could you build Redux with context and useReducer?
6.What is the difference between client state and server state?
Senior
Client state is owned by the browser and always current: form input, which modal is open, the selected theme. Server state is a cache of data that lives somewhere else, so it can be stale, shared and updated without you knowing.
This distinction is the most useful thing to say in a state management interview, because most "we have too much global state" problems are server state stuffed into a client store.
Server state needs caching, deduplication of concurrent requests, background revalidation, retries, and an explicit loading and error state. A client store gives you none of that.
Server state can be stale the moment it arrives, so the question is not "what is the value" but "how fresh is this value".
Putting fetched data in Redux means hand writing the loading flags, the invalidation and the refetch on focus, which is exactly what TanStack Query or RTK Query already do.
Likely follow-up questions
Where would you keep the list of products from an API?
7.What is TanStack Query and what problem does it solve?
Mid-level
TanStack Query, formerly React Query, is an async state manager for server data. It caches responses by key, deduplicates in-flight requests, revalidates in the background, and gives you loading, error and stale states without writing them yourself.
Two components asking for the same key share one request and one cache entry.
staleTime controls how long a cached value is treated as fresh; after that a background refetch keeps the UI up to date without a spinner.
Mutations can invalidate query keys, so the affected screens refetch automatically.
It removes the most repetitive code in a React app: the isLoading / error / data triple written by hand in every component.
Likely follow-up questions
How does it decide when to refetch?
How would you do optimistic updates with it?
8.What is Zustand and when would you pick it over Redux Toolkit?
Mid-level
Zustand is a small store built on hooks: you create a store with a function and components subscribe with a selector. Pick it when you want selector based subscriptions without a provider, slices or action conventions.
The trade-off is convention. Redux Toolkit imposes a shape that a new team member will recognise and that devtools understand fully; Zustand gets out of your way and leaves the structure to you. On a small team or in a library, Zustand's lack of ceremony wins. On a large app with many contributors, the convention is the feature.
Likely follow-up questions
How does Zustand avoid re-rendering everything?
9.What is Redux middleware, and what is the difference between thunk and saga?
Senior
Middleware sits between dispatching an action and the reducer receiving it, which is where side effects and logging live. Thunks let you dispatch a function that performs async work; sagas describe long-running flows as generator functions.
// Thunk: an async function that dispatches
const loadUser = (id) => async (dispatch) => {
dispatch(userLoading());
try {
dispatch(userLoaded(await api.getUser(id)));
} catch (error) {
dispatch(userFailed(error.message));
}
};
Thunk is the default in Redux Toolkit and covers the common case: fire a request, dispatch the result.
Saga shines for orchestration: cancelling an in-flight request, debouncing, racing two effects, retrying with backoff, complex sequences that respond to other actions.
Listener middleware, built into Redux Toolkit, covers much of what saga was used for without generators, and is the current recommendation for new projects.
Likely follow-up questions
When would a saga be worth the learning curve?
10.Why must state be treated as immutable in React and Redux?
Fresher
Both detect change by comparing references. If you mutate an object in place, the reference is unchanged, so React skips the re-render and Redux's reducers and selectors see no change.
// Nothing happens: same array reference
items.push(newItem);
setItems(items);
// New reference: React re-renders
setItems([...items, newItem]);
Reference comparison is what makes React.memo, useMemo dependencies and Redux selectors cheap: comparing two pointers is one operation, comparing two deep trees is not. Immutability is the price of that speed.
Likely follow-up questions
How do you update a nested object immutably?
What does Object.is compare?
11.What is lifting state up, and what is state colocation?
Fresher
Lifting state up means moving a value to the closest common ancestor of the components that need it. Colocation is the opposite instinct: keep state as close as possible to where it is used, and only lift when something forces you to.
The two are a pair, and the second is the one candidates forget. State that lives higher than it needs to re-renders more of the tree than necessary and makes components harder to move or reuse.
Lift only the value the two children actually share.
12.What is derived state, and why is copying props into state a bug?
Mid-level
Derived state is any value you can compute from existing props or state. Copying a prop into state creates a second copy that stops updating when the prop changes, so the UI silently goes stale.
// Bug: fullName never updates when the props change
const [fullName, setFullName] = useState(`${first} ${last}`);
// Correct: compute during render
const fullName = `${first} ${last}`;
The general rule is that state should hold the minimum set of values from which everything else can be computed. If you can calculate it, calculate it. If the calculation is genuinely expensive, wrap it in useMemo, which is still not state.
Likely follow-up questions
How would you reset a form when the selected record changes?
When is it acceptable to initialise state from a prop?
13.How would you persist state to localStorage, and what breaks?
Mid-level
Read it lazily on first render and write it in an effect. What breaks is server rendering: localStorage does not exist on the server, so reading it during the initial render causes a hydration mismatch.
function usePersistedState(key, initial) {
const [value, setValue] = useState(initial);
// Read after mount so the server and the first client render agree
useEffect(() => {
const stored = window.localStorage.getItem(key);
if (stored !== null) setValue(JSON.parse(stored));
}, [key]);
useEffect(() => {
window.localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
Writes are synchronous and block the main thread, so do not persist on every keystroke; debounce.
Stored data is untrusted and may be from an older version of the app, so validate it before use.
There is a size limit of a few megabytes, and it is per origin, shared across tabs.
Likely follow-up questions
What is a hydration mismatch?
How would you sync state across tabs?
14.When would you say an application does not need a global state library at all?
Senior
When the shared state is server data, URL state, or a handful of tree wide values. A server state library plus the URL plus a small amount of context covers a surprising number of production apps.
Data from an API belongs in a query cache, not a store.
Filters, tabs, sorting and the selected record belong in the URL, which makes views shareable and the back button correct for free.
Form state belongs in the form, usually in a form library.
What is genuinely left is often theme, auth and a couple of UI flags, which context handles.
This is a judgement question, so finish with the condition that would change your mind: many writers to the same state, a need to audit how state changed, or an app large enough that a shared convention matters more than the setup cost.