Top 41 Redux / State Management Interview Questions & Answers (2026)
About Redux / State Management
Top 50 Redux and state management interview questions covering Redux Toolkit, Zustand, Jotai, React Query, context API, and modern state management patterns. Companies hiring for Redux / State Management roles test this knowledge at every stage — from first-round screens that check the basics to final-round conversations about real-world trade-offs.
What to Expect in a Redux / State Management Interview
Expect a mix of conceptual and practical Redux / State Management questions: clear definitions and core-concept checks for junior roles, hands-on scenario questions for mid-level roles, and architecture or trade-off discussions for senior roles. Interviewers usually move from foundational topics toward the kind of problems you'd actually face on the job.
How to Use This Guide
Work through the Redux / State Management questions in order — Beginner, then Intermediate, then Advanced — so each concept builds on the last. Every question has its own page; bookmark the ones that trip you up and revisit them the day before your interview.
Curated by Tech Baithak Editorial Team · Last updated: June 2026
Core concepts every Redux / State Management developer must know.
01
What is Redux?
Redux is a predictable state container for JavaScript applications. It stores the entire application state in a single store (a plain JavaScript object). State can only be changed by dispatching actions (plain objects describing what happened), which are handled by pure functions called reducers. Three core principles: Single source of truth: the entire app state lives in one store. State is read-only: the only way to change state is to dispatch an action. Changes are made with pure functions: reducers are pure functions — given the same state and action, they always return the same new state with no side effects. Originally created by Dan Abramov and Andrew Clark in 2015, Redux became the dominant React state management solution for several years before alternatives emerged. It excels at complex state that multiple components need access to and when you need predictable, debuggable state transitions.
02
What are Redux actions?
Actions in Redux are plain JavaScript objects that describe what happened in the application. They must have a type property (a string describing the event) and optionally a payload (the data associated with the action). Example: { type: "todos/todoAdded", payload: { id: 1, text: "Buy milk", completed: false } }. Action creators: functions that return action objects: const addTodo = (text) => ({ type: "todos/todoAdded", payload: { id: uuid(), text } }). Dispatch: store.dispatch(addTodo("Buy milk")). Action type conventions: "feature/action" — the Redux Toolkit naming convention using slices. Actions should describe events that happened (userLoggedIn), not the state change to make (setLoggedIn). This distinction makes actions more useful for debugging and analytics. With Redux Toolkit's createSlice, action creators are generated automatically from reducer definitions.
03
What is a Redux reducer?
A reducer is a pure function that takes the current state and an action, and returns the next state. Signature: (state, action) => newState. Key rules: No side effects: no API calls, no random values, no mutations. Immutability: return a new state object instead of mutating the existing one. Predictable: same inputs always produce the same output. Example: function todosReducer(state = [], action) { switch (action.type) { case "todos/added": return [...state, action.payload]; case "todos/removed": return state.filter(t => t.id !== action.payload); default: return state; } }. The default case returns current state for unknown actions — important because Redux initializes state by dispatching a dummy action. Immutability: Redux Toolkit's createReducer and createSlice use Immer under the hood, allowing "mutating" syntax that is actually converted to immutable updates: state.push(action.payload) inside RTK reducers is safe.
04
What is the Redux store?
The Redux store is the central object that holds the application's complete state tree. Create it: const store = configureStore({ reducer: { todos: todosReducer, user: userReducer } }). Key store methods: getState(): returns the current state. dispatch(action): sends an action through reducers to update state. subscribe(listener): registers a callback called after every state change. replaceReducer(): for code splitting. The store is the single source of truth — there is one store per Redux app. With Redux Toolkit's configureStore: sets up Redux DevTools automatically, enables Immer for immutable updates, adds Redux Thunk middleware by default, and includes helpful development checks. In React apps, the store is provided via the <Provider store={store}> wrapper component from react-redux, making it accessible anywhere in the component tree via hooks like useSelector and useDispatch.
05
What is Redux Toolkit?
Redux Toolkit (RTK) is the official, opinionated toolset for Redux development, created to address Redux's historical boilerplate problems. It is now the standard way to write Redux. Key utilities: configureStore(): simplified store setup with good defaults (DevTools, Thunk middleware). createSlice(): generates action creators and action types from reducer functions: const counterSlice = createSlice({ name: "counter", initialState: 0, reducers: { increment: state => state + 1, addAmount: (state, action) => state + action.payload } }). createAsyncThunk(): handle async operations with auto-generated pending/fulfilled/rejected actions. createEntityAdapter(): normalized state management for collections of entities. RTK Query: powerful data fetching and caching solution built into RTK. Benefits: eliminates ~75% of Redux boilerplate, enforces best practices, includes Immer for immutable updates, and provides excellent TypeScript support. All new Redux apps should use RTK — writing "hand-written" Redux is no longer recommended.
06
What is the difference between Redux and React Context API?
Both Redux and React Context API share state across components, but they serve different purposes. React Context: built-in React mechanism for passing data deep into the component tree without prop drilling. Best for: relatively static data (theme, locale, current user) or data that changes infrequently. Context re-renders all consumers whenever the value changes — a performance concern for frequently-updated state. No built-in state management logic (no reducers, middleware, DevTools). Redux: dedicated state management library with predictable state transitions, middleware support, DevTools for time-travel debugging, and optimized subscriptions (only components that use a specific slice re-render). Best for: complex global state, frequent updates, state that multiple independent components need, or when you need middleware (logging, async operations). Modern advice: use Context for simple cases; use Redux Toolkit (or Zustand) for complex global state. React Query/TanStack Query handles server state better than both — many apps need minimal client state when server state is handled separately.
07
What are Redux Toolkit slices?
A slice in Redux Toolkit is a collection of Redux reducer logic and actions for a single feature. Created with createSlice(): const counterSlice = createSlice({ name: "counter", initialState: { value: 0 }, reducers: { increment(state) { state.value += 1; }, decrement(state) { state.value -= 1; }, setAmount(state, action) { state.value = action.payload; } } });. RTK auto-generates: action creators (counterSlice.actions.increment()), action types ("counter/increment"), and the reducer function (counterSlice.reducer). The "mutating" syntax inside reducers (state.value += 1) is safe because RTK uses Immer — it detects mutations and produces a new immutable state. Use Immer-safe mutations: state.items.push(item), state.items.splice(index, 1). Add to store: configureStore({ reducer: { counter: counterSlice.reducer } }). Slices organize Redux code by feature domain rather than by type (actions folder, reducers folder).
08
What is useSelector and useDispatch in React-Redux?
These are the two primary React-Redux hooks for functional components. useSelector(selectorFn): subscribes to the Redux store and returns a selected value. const count = useSelector(state => state.counter.value). The component re-renders whenever the selected value changes. Use memoized selectors (Reselect) to avoid unnecessary re-renders. useDispatch(): returns the store's dispatch function. const dispatch = useDispatch(); dispatch(increment()). Both hooks require the component to be wrapped in a <Provider store={store}>. Before hooks, connect() higher-order component was used: connect(mapStateToProps, mapDispatchToProps)(Component) — still valid but verbose. TypeScript: define typed hooks to avoid repeating types: export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector; export const useAppDispatch = () => useDispatch<AppDispatch>(). Always use the typed versions in TypeScript projects for better autocompletion and type safety.
09
What is Redux Thunk?
Redux Thunk is a middleware that allows dispatching functions (instead of just action objects) — enabling async operations in Redux. A thunk is a function that returns another function: const fetchUser = (id) => async (dispatch) => { dispatch(loadingStarted()); try { const user = await api.getUser(id); dispatch(userLoaded(user)); } catch (err) { dispatch(loadingFailed(err.message)); } }. Dispatch: dispatch(fetchUser(42)). Thunk middleware intercepts the function, calls it with dispatch and getState. With RTK's createAsyncThunk, the boilerplate is reduced: const fetchUser = createAsyncThunk("users/fetchById", async (id) => { return await api.getUser(id); }). RTK auto-generates fetchUser.pending, fetchUser.fulfilled, and fetchUser.rejected action types. Handle in slice: extraReducers: (builder) => builder.addCase(fetchUser.fulfilled, (state, action) => { state.user = action.payload; }).
10
What is Zustand?
Zustand (German for "state") is a small, fast, and unopinionated state management library for React. It is one of the most popular Redux alternatives. Create a store: import { create } from 'zustand'; const useCounterStore = create((set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), reset: () => set({ count: 0 }) }));. Use in components: const { count, increment } = useCounterStore();. Key features: No Provider needed: works without wrapping the app in a Provider component. Minimal boilerplate: create stores with a simple function. Outside React: can be used outside components: useCounterStore.getState().increment(). Subscriptions: components only re-render when the selected state changes. Middleware: supports devtools, persist (localStorage), and immer. TypeScript: excellent type inference. Zustand is ideal for most medium-complexity global state needs without Redux's ceremony.
11
What is Jotai?
Jotai (Japanese for "state") is an atomic state management library for React inspired by Recoil. Instead of one store, Jotai uses atoms — tiny units of state. Define atoms: import { atom } from 'jotai'; const countAtom = atom(0); const userAtom = atom({ name: "", email: "" });. Use in components: const [count, setCount] = useAtom(countAtom);. useAtomValue(countAtom): read only. useSetAtom(countAtom): write only. Derived atoms: compute from other atoms: const doubledAtom = atom((get) => get(countAtom) * 2);. Async atoms: const userAtom = atom(async () => await fetchUser()); — works with React Suspense. Key features: Bottom-up: components subscribe to only the atoms they use — granular re-renders. No Provider required (provider-less mode). Tiny bundle: ~2KB. Jotai is excellent for fine-grained state where different parts of the UI need different atoms, and for Suspense-integrated async state.
12
What is React Query (TanStack Query)?
TanStack Query (formerly React Query) is a powerful data fetching and synchronization library that manages server state — data that comes from an API and needs to be kept in sync. Key features: Automatic caching: query results are cached by query key. Background refetching: data is refreshed in the background when the window regains focus, on a schedule, or when stale. Loading/error states: isLoading, isError, data from useQuery. Mutations: useMutation for create/update/delete. Cache invalidation: queryClient.invalidateQueries to refetch after a mutation. Usage: const { data, isLoading, error } = useQuery({ queryKey: ["user", id], queryFn: () => fetchUser(id) }). React Query distinguishes server state (remote, async, potentially stale) from client state (local, synchronous). Many apps using Redux for API data can replace it with React Query + a minimal local state solution (Zustand/Context), reducing complexity dramatically.
13
What is the Redux Devtools Extension?
The Redux DevTools Extension is a browser extension that provides powerful debugging capabilities for Redux applications. Key features: State inspector: view the complete state tree at any point in time. Action history: see every dispatched action with its payload. Time-travel debugging: step backward and forward through actions to replay bugs. Action replay: replay specific actions to reproduce issues. Import/Export state: share state snapshots for bug reproduction. Diff view: see exactly what changed in state after each action. Setup with RTK: automatically enabled in development — RTK's configureStore includes DevTools integration by default. For manual setup: const store = createStore(reducer, window.__REDUX_DEVTOOLS_EXTENSION__?.()). Action creators logging: custom action names appear in the DevTools, making it easy to trace what happened. The DevTools extension is one of Redux's biggest advantages over simpler state management libraries — it makes complex state transitions completely transparent and debuggable.
14
What is Recoil?
Recoil is Facebook's (Meta's) experimental state management library for React, introducing the atom-based model. Core primitives: atom: a unit of state: const textState = atom({ key: 'textState', default: '' });. selector: derived state from atoms or other selectors: const charCountState = selector({ key: 'charCount', get: ({get}) => get(textState).length });. Use: const [text, setText] = useRecoilState(textState);. Advantages: fine-grained subscriptions — components only re-render when their subscribed atoms change. React-compatible — uses Suspense for async selectors. Atom effects: side effects on atom changes (logging, persistence). Limitations: experimental status for years, slower adoption, Meta has not committed to long-term maintenance. Jotai (inspired by Recoil) is considered a more production-ready alternative with a similar API but smaller bundle size and no Provider requirement. Recoil is suitable for React apps with highly granular state dependencies.
15
What is MobX?
MobX is a reactive state management library that automatically tracks dependencies and updates the UI when state changes. It uses observables, actions, and computed values. Define a store: import { makeObservable, observable, action, computed } from 'mobx'; class CounterStore { count = 0; constructor() { makeObservable(this, { count: observable, doubled: computed, increment: action }); } get doubled() { return this.count * 2; } increment() { this.count++; } }. In React: const Counter = observer(() => { const store = useLocalObservable(() => new CounterStore()); return <button onClick={store.increment}>{store.count}</button>; });. The observer HOC auto-subscribes to any observables accessed in the render. MobX is popular in enterprise Angular-like codebases for its OOP style and minimal ceremony. Key philosophy: "anything that can be derived from state should be derived automatically." MobX-State-Tree (MST) adds structural conventions on top of MobX.
16
What is the difference between Redux and MobX?
Redux and MobX take fundamentally different approaches to state management. Redux: explicit, unidirectional. Actions are explicitly dispatched. Reducers are pure functions producing new state. State updates are tracked via action history — excellent for debugging with DevTools. Verbose but predictable. Best for complex apps requiring deep debugging, time-travel, or strict data flow. MobX: implicit, reactive. State mutations are automatically detected and propagate to all observers. More OOP-friendly. Less boilerplate — no actions/reducers required for simple mutations. Harder to debug (implicit updates, no action history). Best for apps where developer productivity and minimal boilerplate are priorities. Mental model: Redux = functional, immutable, explicit. MobX = OOP, mutable observables, implicit reactivity. Team considerations: Redux has a larger community and more tooling. MobX is more familiar to developers from Angular/Vue OOP backgrounds. Most experienced React teams choose Redux (with RTK) or Zustand over MobX for new projects due to Redux's superior debugging story.
17
What are selectors in Redux?
Selectors are functions that extract and compute derived data from the Redux store state. Simple selector: const selectUser = (state) => state.auth.user;. Use in component: const user = useSelector(selectUser). Why use selectors: encapsulate state structure (if state shape changes, update only the selector), enable memoization for performance, make component code cleaner. Reselect library: creates memoized selectors: const selectActiveUsers = createSelector([selectAllUsers], (users) => users.filter(u => u.active)). The memoized selector only recomputes when selectAllUsers changes — if the same input is passed again, it returns the cached result. RTK includes Reselect: import { createSelector } from "@reduxjs/toolkit". RTK selectFromResult: RTK Query provides optimized per-component selectors. inputSelectorsCount: Reselect accepts multiple input selectors, and the result function receives all their results. Selectors are the "lenses" into your Redux state — always access state through selectors rather than directly.
18
What is RTK Query?
RTK Query is a powerful data fetching and caching tool built into Redux Toolkit — RTK's answer to React Query. Define an API: import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react"; const api = createApi({ reducerPath: "api", baseQuery: fetchBaseQuery({ baseUrl: "/api" }), endpoints: (builder) => ({ getUser: builder.query({ query: (id) => \`/users/\${id}\` }), createUser: builder.mutation({ query: (body) => ({ url: "/users", method: "POST", body }), invalidatesTags: ["Users"] }) }) });. Generated hooks: const { data, isLoading } = useGetUserQuery(id); const [createUser] = useCreateUserMutation();. Features: automatic caching, background refetching, cache invalidation with tags, optimistic updates, pagination. Key advantage over React Query: since RTK Query integrates with the Redux store, all cached data is visible in Redux DevTools and can be accessed by regular Redux selectors. For teams already using Redux, RTK Query is the natural choice for server state. For teams not using Redux, React Query (TanStack Query) is simpler to set up.
19
What is the Context API for state management?
The React Context API enables sharing values between components without prop-drilling. Create: const ThemeContext = React.createContext("light");. Provide: <ThemeContext.Provider value="dark"><App /></ThemeContext.Provider>. Consume: const theme = useContext(ThemeContext);. For mutable state: combine with useState or useReducer: const [state, dispatch] = useReducer(reducer, initialState); <StateContext.Provider value={{ state, dispatch }}>. Performance issue: every component consuming the context re-renders when the context value changes — even if it only uses a small part of the value. Solutions: split contexts by concern, memoize with useMemo, or use a state management library with selective subscriptions. Best uses: theme (light/dark), locale, current user, feature flags. Avoid for: frequently changing state (cartItem count on every keystroke), complex state with many derived values. Context is a low-overhead, built-in solution for sharing configuration and rarely-changing state.
20
What is useReducer in React?
useReducer is a React hook for managing complex state logic locally — it is Redux-like state management scoped to a single component or subtree. Signature: const [state, dispatch] = useReducer(reducer, initialState);. Define a reducer: function reducer(state, action) { switch (action.type) { case "increment": return { count: state.count + 1 }; case "reset": return initialState; default: return state; } }. Dispatch: dispatch({ type: "increment" }). vs useState: use useReducer when state update logic is complex (multiple sub-values, next state depends on previous), when you need to pass the dispatch function down (stable reference), or when multiple state updates should happen together atomically. vs Redux: useReducer is scoped to the component tree below — not global. For global state shared across many components, use Redux or another library. Combine useReducer + Context to build a lightweight Redux-like system without any library.
Practical knowledge for developers with hands-on experience.
01
How do you handle async operations in Redux Toolkit with createAsyncThunk?
createAsyncThunk is RTK's utility for defining async thunk action creators. It auto-generates lifecycle actions. Define: const fetchUser = createAsyncThunk("users/fetchById", async (userId, { rejectWithValue }) => { try { return await api.getUser(userId); } catch (err) { return rejectWithValue(err.response.data); } });. Handle in slice: extraReducers: (builder) => { builder .addCase(fetchUser.pending, (state) => { state.loading = true; }) .addCase(fetchUser.fulfilled, (state, action) => { state.loading = false; state.user = action.payload; }) .addCase(fetchUser.rejected, (state, action) => { state.loading = false; state.error = action.payload; }); }. Dispatch: dispatch(fetchUser(42)). rejectWithValue: pass error data to the rejected action. thunkAPI: second parameter gives access to dispatch, getState, signal (AbortSignal for cancellation). Cancellation: const promise = dispatch(fetchUser(42)); promise.abort();. Auto-generated action types: users/fetchById/pending, users/fetchById/fulfilled, users/fetchById/rejected.
02
What is Zustand's middleware system?
Zustand supports middleware for extending store behavior. The middleware pattern wraps the store creator. Key built-in middleware: devtools: connect to Redux DevTools: import { devtools } from "zustand/middleware"; const useStore = create(devtools((set) => ({ count: 0, increment: () => set((s) => ({ count: s.count + 1 }), false, "increment") }))). The third argument to set is the action name for DevTools. persist: automatically save/restore state: create(persist((set) => ({ count: 0 }), { name: "counter-storage", storage: createJSONStorage(() => localStorage) })). immer: enable mutable syntax: create(immer((set) => ({ items: [], add: (item) => set((s) => { s.items.push(item); }) }))). subscribeWithSelector: fine-grained subscriptions: useStore.subscribe((state) => state.count, (count) => console.log(count)). Combine middleware: create(devtools(persist(immer((set) => (...))))) — compose right-to-left.
03
What is Reselect and memoized selectors?
Reselect is a library for creating memoized, composable selectors for Redux. A memoized selector caches the last computation — if inputs haven't changed, it returns the cached result without recomputing. Basic usage: const selectFilteredTodos = createSelector( [selectAllTodos, selectFilter], (todos, filter) => { console.log("Computing..."); return todos.filter(t => t.status === filter); } );. If todos and filter haven't changed, console.log doesn't run. Why it matters: without memoization, a selector running filter() every render creates a new array reference — useSelector would trigger a re-render even if the filtered result is identical. Reselect v5 (RTK default): includes createSelectorCreator for custom memoization, and supports weakly-referenced caches. Best practices: create granular selectors and compose them, always use memoized selectors for derived data, define selectors in the slice file alongside the reducer for co-location.
04
How does normalized state shape work in Redux?
Normalized state stores entities as a dictionary (byId) keyed by their unique ID, plus an array of IDs for ordering. This avoids nested, duplicated data. Normalized: { users: { ids: [1, 2], entities: { 1: { id: 1, name: "Alice" }, 2: { id: 2, name: "Bob" } } } }. Benefits: O(1) lookup by ID, no data duplication, easy updates. RTK's createEntityAdapter provides this automatically: const usersAdapter = createEntityAdapter(); const usersSlice = createSlice({ name: "users", initialState: usersAdapter.getInitialState(), reducers: { usersReceived: usersAdapter.setAll, userAdded: usersAdapter.addOne, userUpdated: usersAdapter.updateOne } });. Built-in selectors: usersAdapter.getSelectors((state) => state.users) — provides selectAll, selectById, selectIds, selectTotal. Normalization is critical for state with deeply nested or frequently updated relationships. Without it, updating a user object buried in multiple places requires complex nested spreads.
05
What is the Flux architecture pattern?
Flux is Facebook's application architecture pattern that Redux is based on. It enforces a strict unidirectional data flow. Components: Action: object describing an event. Dispatcher: central hub that receives all actions and dispatches to registered stores. Store: holds application state and logic, responds to actions from the dispatcher. View: React components that render state from stores and create actions on user interactions. Flow: User interaction → Action → Dispatcher → Store (state update) → View re-render. Redux simplification: Redux replaced the Dispatcher with a single reducer function, stores multiple states in a single object, and eliminated the need for multiple stores. Redux is often called "Flux done right." Why Flux/Redux: traditional MVC breaks down when complex applications have many components sharing state — circular data flows and cascading updates make debugging impossible. Flux's unidirectional flow makes the application state predictable: for any action, you know exactly which store changes and how.
06
How do you test Redux applications?
Testing Redux follows a layered strategy. Reducer tests: pure functions — easiest to test: it("increments count", () => { const state = { count: 0 }; const action = increment(); expect(reducer(state, action)).toEqual({ count: 1 }); }). Selector tests: pass mock state: expect(selectActiveUsers({ users: mockUsers })).toHaveLength(2). Thunk tests: use configureStore with mock reducers: dispatch the thunk and check dispatched actions. Or use a mock axios and test against expected state. RTK Query tests: use setupServer from msw (Mock Service Worker) to intercept API calls, then use RTK Query's testing utilities. Component integration tests: wrap components with a real or mock store: render(<Provider store={testStore}><MyComponent /></Provider>). Use renderWithProviders helper. Test philosophy: test behavior not implementation. Prefer integration tests (component + store) over isolated unit tests for each layer — they catch more real bugs and are less brittle to refactoring.
07
What is the immer library and how does it work with Redux?
Immer is a library that lets you write "mutating" code that actually produces immutable updates. It uses JavaScript Proxy to track mutations on a draft state, then produces a new immutable state based on those mutations. Without Immer: case "addTodo": return { ...state, todos: [...state.todos, action.payload] }. With Immer: case "addTodo": state.todos.push(action.payload) — the same result. RTK's createSlice and createReducer use Immer internally. Rules: Either mutate the draft or return a new value — not both. Return undefined implicitly (no return) to commit mutations. Return a new value explicitly to replace state entirely. Limitations: Map and Set have special handling (immer's enableMapSetPlugin). Non-draftable types (Date, RegExp) must be returned, not mutated. current(state) in reducers gives a plain snapshot of the draft for debugging. Immer dramatically reduces the cognitive overhead of writing reducers for nested state structures.
08
How do you implement optimistic updates with Redux Toolkit?
Optimistic updates immediately update the UI before the server confirms, creating a responsive feel. With RTK Query: updatePost: builder.mutation({ query: ({ id, ...patch }) => ({ url: \`/posts/\${id}\`, method: "PATCH", body: patch }), async onQueryStarted({ id, ...patch }, { dispatch, queryFulfilled }) { const patchResult = dispatch(api.util.updateQueryData("getPost", id, (draft) => { Object.assign(draft, patch); })); try { await queryFulfilled; } catch { patchResult.undo(); } } }). How it works: updateQueryData optimistically updates the cache. If the request fails, patchResult.undo() reverts the cache. With regular thunks: dispatch the optimistic action first, then the async action. On success, the real data replaces the optimistic data. On failure, dispatch a rollback action. Key consideration: always handle errors and revert — unreverted optimistic updates on failure create data inconsistencies. Show error notifications when rollback occurs so users know their action failed.
09
What is Valtio?
Valtio is a simple, proxy-based state management library for React and vanilla JavaScript. Created by Daishi Kato (also the creator of Jotai and Zustand). Create state: import { proxy, useSnapshot } from 'valtio'; const state = proxy({ count: 0, text: '' });. Mutate directly (outside React): state.count++; state.text = "hello". In React components, use useSnapshot for reactivity: const snap = useSnapshot(state); <div>{snap.count}</div>. The snapshot is a read-only version of the proxy — mutations must happen on state, not snap. Deep reactivity: nested objects are automatically proxied: state.user.name = "Alice" triggers re-renders. Derived state: const derived = derive({ doubled: get => get(state).count * 2 }). Use cases: Valtio is excellent for React apps that want MobX-like mutable state without class syntax. Its proxy-based approach feels natural coming from Vue's reactivity. The main trade-off: mutations are implicit, making debugging harder than Redux's explicit action model.
10
How do you handle forms state in React?
Form state management options in React: Uncontrolled forms: use native HTML form behavior with ref for values. Minimal re-renders. Good for simple forms. Controlled forms: React state mirrors every input. Flexible but causes re-render on every keystroke. React Hook Form: the most popular form library. Uses uncontrolled inputs under the hood for performance — minimal re-renders. const { register, handleSubmit, formState: { errors } } = useForm(); <input {...register("email", { required: true, pattern: /\S+@\S+\.\S+/ })} />. Integrates with Zod/Yup for schema validation. Formik: older but still widely used controlled-form library with field arrays, async validation. TanStack Form: new, framework-agnostic form library from the creators of TanStack Query. When to use Redux for forms: almost never — form state is local UI state that doesn't need to be global. Use form libraries or local useState. Only put the form's submitted data in global state if other parts of the app need it.
11
What is the difference between local state, global state, and server state?
Distinguishing state types helps choose the right tool. Local (UI) state: state that belongs to a single component and its direct children. Examples: modal open/closed, input value, tab selection, accordion expand/collapse. Use: useState, useReducer. Never needs to go into Redux. Global (shared) state: state that multiple unrelated components across the app need. Examples: current user, authentication status, shopping cart, theme preference, notification count. Use: Redux (complex), Zustand (moderate), Context (simple). Server state: data fetched from a remote server — async, potentially stale, shared with the server. Examples: user profile from API, list of products, order history. Use: TanStack Query, RTK Query, SWR. Server state has unique challenges: caching, background updates, error handling, stale data, pagination. Misclassification is the source of most state management complexity. Many apps using Redux for API data can simplify enormously by moving to React Query (server state) + Zustand (client state) — Redux becomes unnecessary for most use cases.
12
What are Redux middleware and how do you write a custom one?
Redux middleware provides an extension point between dispatching an action and the moment it reaches the reducer. It intercepts every dispatched action, enabling logging, crash reporting, async operations, and more. The middleware signature: const myMiddleware = (store) => (next) => (action) => { console.log("Before:", store.getState()); const result = next(action); console.log("After:", store.getState()); return result; }. Register: configureStore({ reducer, middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(myMiddleware) }). Common middleware: Redux Thunk (async), Redux-Saga (generator-based async), redux-logger (console logs), redux-observable (RxJS). Custom middleware examples: API middleware that intercepts type: "API_CALL" actions and makes fetch requests, analytics middleware that logs specific actions to analytics services, authorization middleware that adds auth headers. The three-level currying (store => next => action => ...) enables middleware composition — each middleware wraps the next, forming a chain.
Deep expertise questions for senior and lead roles.
01
How do you architect state management for large-scale React applications?
Large-scale React app state architecture follows the principle of co-location and minimal global state. Architecture layers: Component state (useState/useReducer): UI interactions, form state, local toggles. Never elevate to global unless needed. Server state (TanStack Query/RTK Query): all API data — caching, background sync, optimistic updates. This should be the largest category in most apps. Global client state (Zustand/Redux): truly shared UI state — current user, theme, auth status, shopping cart, notifications. This should be the smallest category. URL state: filters, pagination, current view — prefer storing in URL params (persistent across refreshes, shareable). Folder structure: feature-based (all state for a feature together): features/users/usersSlice.ts, features/users/usersSelectors.ts. Anti-patterns to avoid: everything in Redux (use React Query for API data), prop-drilling past 2-3 levels (elevate to context or global), duplicated state (derive from existing state), mutable global state (immutability is non-negotiable). The goal: each piece of state has exactly one canonical home.
02
What is Redux-Saga and how does it differ from Thunks?
Redux-Saga is a middleware library for handling complex async side effects using ES6 generators. Sagas are long-running background tasks that watch for actions and respond. Define a saga: function* fetchUserSaga(action) { try { const user = yield call(api.getUser, action.payload); yield put(userLoaded(user)); } catch (error) { yield put(loadingFailed(error.message)); } } function* watchFetchUser() { yield takeEvery("users/fetchRequested", fetchUserSaga); }. Key effects: call: call a function (async). put: dispatch an action. takeEvery: listen for actions. takeLatest: cancel previous, handle only latest. all: run effects in parallel. race: cancel others when first completes. Sagas vs Thunks: Thunks are simple functions — easy to write, hard to test complex flows. Sagas are testable (generators return effect descriptors, not actual API calls) and excellent for complex flows (retry, cancellation, race conditions, debouncing). Sagas are harder to learn but more powerful for complex async orchestration. For simple API calls, use createAsyncThunk. For complex workflows with cancellation/retry, use Sagas.
03
What is XState and state machines in UI development?
XState is a JavaScript library for building finite state machines and statecharts — a formal, visual approach to modeling complex UI state. Define a machine: const authMachine = createMachine({ id: "auth", initial: "idle", states: { idle: { on: { LOGIN: "loading" } }, loading: { invoke: { src: "loginService", onDone: "authenticated", onError: "error" } }, authenticated: { on: { LOGOUT: "idle" } }, error: { on: { RETRY: "loading" } } } }). Use in React: const [state, send] = useMachine(authMachine); send("LOGIN"). Benefits: Impossible states prevented: in the loading state, LOGIN event is ignored (defined behavior, not a bug). Visual tooling: XState Visualizer shows the state chart graphically. Formal correctness: all transitions are explicit. Testability: machines are pure data — test without a browser. Use cases: complex forms with multi-step validation, auth flows, onboarding wizards, media players, collaborative editing states. XState adds complexity but eliminates entire classes of bugs related to invalid state combinations.
04
How does TanStack Query's caching and staleness model work?
TanStack Query uses a sophisticated cache with staleness tracking. Key concepts: Query key: uniquely identifies a cache entry. ["user", id] is a different cache entry than ["user", 42]. staleTime: how long data is considered fresh. During this period, no background refetch occurs. Default: 0 (immediately stale). staleTime: 5 * 60 * 1000 = data stays fresh for 5 minutes. gcTime (cacheTime): how long unused cache data is retained in memory. Default: 5 minutes. refetch triggers: when stale, data is refetched on: component mount, window focus, network reconnect, manual invalidateQueries, interval (refetchInterval). Cache invalidation: queryClient.invalidateQueries({ queryKey: ["users"] }) — marks all user queries as stale and triggers background refetch for any currently rendered query. Dependent queries: useQuery({ queryKey: ["orders", userId], enabled: !!userId }) — only fetches when userId is truthy. Prefetching: queryClient.prefetchQuery loads data before navigation. Query deduplication: multiple components subscribing to the same key share one request.
05
What are the patterns for sharing state between micro-frontends?
Sharing state between micro-frontends (independently deployed UI modules) requires special patterns since each micro-frontend may have its own React instance. Custom events / Window events: broadcast state changes as browser custom events. The simplest cross-framework solution: window.dispatchEvent(new CustomEvent("user:updated", { detail: { user } })). Each micro-frontend listens with window.addEventListener. URL as state: put shared state (current user ID, tenant) in the URL — all micro-frontends read from the URL independently. Shared state store in shell app: the shell application (the container) maintains a shared state object and injects it into each micro-frontend via props or context during mounting. Event bus: a shared singleton event emitter (accessible via a well-known global or import from a shared package). Module Federation: with Webpack Module Federation, share a single React instance and Redux store across micro-frontends. When to avoid sharing state: prefer each micro-frontend to be self-contained with its own data fetching. Shared state creates coupling — the opposite of the micro-frontend goal. Pass only the minimum necessary (user ID, not the full user object).
06
How do you handle real-time state updates with WebSockets and Redux?
Integrating WebSocket real-time updates with Redux: Middleware approach: create middleware that manages the WebSocket connection: const socketMiddleware = (store) => { const socket = new WebSocket(WS_URL); socket.onmessage = (event) => { const data = JSON.parse(event.data); store.dispatch(messageReceived(data)); }; return (next) => (action) => { if (action.type === "socket/send") socket.send(JSON.stringify(action.payload)); return next(action); }; }. RTK Query with WebSocket: use onCacheEntryAdded in endpoints to manage subscriptions: async onCacheEntryAdded(arg, { updateCachedData, cacheDataLoaded, cacheEntryRemoved }) { const ws = new WebSocket(...); ws.onmessage = (event) => updateCachedData((draft) => { draft.messages.push(JSON.parse(event.data)); }); await cacheEntryRemoved; ws.close(); }. Optimistic updates: immediately update state when sending, confirm on server acknowledgement, revert on error. Reconnection: implement exponential backoff reconnection in the middleware with status tracking in the Redux state. State synchronization: use sequence numbers or vector clocks for conflict detection when multiple clients modify the same state.
07
What are the advanced patterns for selector composition?
Advanced selector patterns for scalable Redux: Parameterized selectors with factory functions: const makeSelectTodoById = () => createSelector([selectAllTodos, (_, id) => id], (todos, id) => todos.find(t => t.id === id));. Create an instance per component: const selectTodo = useMemo(makeSelectTodoById, []); const todo = useSelector(s => selectTodo(s, id)); — each component instance has its own memoized selector cache. Selector composition: build complex selectors from simple ones: const selectFilteredSortedUsers = createSelector([selectActiveUsers, selectSortOrder], (users, order) => [...users].sort(...)). Cross-slice selectors: selectors that combine data from multiple slices: const selectUserWithOrders = createSelector([selectUser, selectOrdersByUserId], (user, orders) => ({ ...user, orders })). Reselect v5 weakMemo: cache unlimited recent results using WeakMap — better for parameterized selectors with many possible inputs. createSelectorCreator: custom memoization strategy (e.g., Lodash's isEqual for deep comparison). Always profile before optimizing selectors — premature memoization adds complexity without benefit.
08
What is the proxy state pattern (Valtio / Immer) versus immutable state?
The proxy state pattern and immutable state pattern represent fundamentally different approaches. Immutable state (Redux, Zustand): state never changes — updates create new objects. Enables reference equality checks (prev === next means no change), time-travel debugging (save old state references), and structural sharing. All React state management best practices are built around immutability. Debugging: see exactly what changed between two snapshots. Proxy state (Valtio, MobX, Vue): Proxy intercepts property access and mutation. State appears mutable but reactivity is tracked automatically. Less boilerplate for updates. Familiar for developers from Vue/MobX. Immer's hybrid: writes look mutable, produces immutable results. Best of both worlds — mutating syntax, immutable semantics. Trade-offs: Proxy state is more ergonomic for deeply nested updates. Immutable state is more predictable and debuggable. Proxy-based libraries can have surprising behavior when state is shared across module boundaries (Proxy objects vs plain objects). For large teams, immutable state (enforced by TypeScript readonly types) reduces a class of mutation bugs. For smaller projects, proxy-based Valtio offers excellent DX with minimal ceremony.
09
How do you migrate from Redux to a modern state management solution?
Migrating large Redux codebases requires a strangler fig approach — incrementally replace Redux while both coexist. Step 1: Identify state categories: audit what's in Redux. Most Redux state is either: server data (should be React Query/RTK Query), or client UI state (should be Zustand/local state). Step 2: Migrate server state first: replace thunks/sagas that fetch API data with RTK Query or React Query. These are the most impactful — they eliminate loading/error boilerplate in reducers. Step 3: Migrate local state: Redux state used by only 1-2 components → local useState. State passed as props from Redux → lift to component. Step 4: Replace remaining global state: the remaining global client state (auth, theme, cart) → Zustand store or Redux Toolkit slice. Coexistence: Redux and React Query can coexist. RTK Query and React Query can both run. Migrate slice by slice, not all at once. Benefits post-migration: ~60-70% less boilerplate, faster builds (smaller bundle), better performance (targeted subscriptions), simpler testing. The migration ROI is highest when moving Redux API data to React Query.