Skip to content

Context vs Redux Interview Questions & Answers

18 questions Updated 2026-06-24 Share:

Context vs Redux interview questions — when to use each, performance trade-offs, re-render behavior, middleware, devtools, and scaling considerations.

Read the in-depth guideReact Context vs Redux — Complete Interview Guide(opens in new tab)
18 of 18

The Context API is React's built-in mechanism for sharing values across a component tree without passing props through every intermediate layer. You create a context with createContext, wrap a subtree in its Provider, and any descendant can read the value with useContext.

const ThemeContext = createContext('light')  // default value

function App() {
  return (
    <ThemeContext.Provider value="dark">
      <Layout />                 {/* no prop threading needed */}
    </ThemeContext.Provider>
  )
}

function Button() {
  const theme = useContext(ThemeContext)  // reads 'dark' directly
  return <button className={theme}>Click</button>
}

Its primary use case is ambient, infrequently-updated global data — the authenticated user, current locale, colour theme, or feature flags. These values are read often but change rarely, so the re-render cost is low.

Rule of thumb: Context is a dependency-injection mechanism, not a state manager. Reach for it when you want to avoid prop drilling, not when you need a sophisticated state architecture.

Context is a transport layer — it teleports a value from a Provider to any consumer without passing props. It owns no opinion about how state is structured or updated; you still call useState/useReducer yourself.

Redux is a state architecture — a single, centralised store with strict rules: state is read-only, updates happen through serialisable actions, and reducers are pure functions that produce the next state. Middleware, selectors, and DevTools are first-class concepts.

// Context — you manage the state, Context just shares it
const [user, setUser] = useState(null)
<UserContext.Provider value={{ user, setUser }}>...</UserContext.Provider>

// Redux — the store manages everything; components just dispatch/select
dispatch({ type: 'auth/login', payload: { id: 1, name: 'Ada' } })
const user = useSelector(state => state.auth.user)

Think of it this way: Context answers "how do I share state?" — Redux answers "how do I manage state?"

Rule of thumb: If you find yourself bolting a reducer, middleware, and selector logic onto Context, you've reinvented Redux — just use Redux.

Every component that calls useContext(MyContext) re-renders whenever the Provider's value reference changes — regardless of whether the specific slice of that value the component uses actually changed. React has no built-in selector granularity for Context.

const AppContext = createContext()

function Provider({ children }) {
  const [user, setUser] = useState(null)
  const [cart, setCart] = useState([])

  // New object every render — both user AND cart consumers re-render
  // even if only `cart` changed
  return (
    <AppContext.Provider value={{ user, setUser, cart, setCart }}>
      {children}
    </AppContext.Provider>
  )
}

function UserBadge() {
  const { user } = useContext(AppContext)  // re-renders on every cart update too
  return <span>{user?.name}</span>
}

This becomes a real problem when the context value updates frequently (e.g. every keystroke, every animation frame, a live-updating cart) and many components are subscribed.

Rule of thumb: Context re-render overhead is acceptable for values that change a few times per session; it becomes a bottleneck for values that change multiple times per second or that are consumed by dozens of components.

Context shines for infrequently-updated, app-wide ambient data where you need broad access but not granular subscriptions:

  • Auth user — set on login/logout, read in many places, almost never changes during a session.
  • Theme / colour mode — toggled once in a while, consumed by many components for styling.
  • Locale / i18n — changes only when the user switches language.
  • Feature flags — loaded once at startup.
  • UI preferences — sidebar collapsed, modal stack.
// A practical, well-scoped auth context
const AuthContext = createContext(null)

export function AuthProvider({ children }) {
  const [user, setUser] = useState(null)
  const login  = useCallback(async (creds) => { /* ... */ }, [])
  const logout = useCallback(() => setUser(null), [])

  // Memoised so reference only changes when user changes
  const value = useMemo(() => ({ user, login, logout }), [user, login, logout])
  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
}

Rule of thumb: If the value changes less than once per user action, Context is probably the right tool.

Redux earns its keep when several of these conditions are true:

  1. High-frequency updates — shopping carts, real-time feeds, form state with many interdependent fields.
  2. Complex async — sequences of API calls with loading/error/retry states that need to be cancelled, debounced, or coordinated.
  3. Cross-cutting derived state — selectors that combine slices from multiple parts of the store.
  4. Large team — enforced conventions (action types, reducer boundaries) prevent accidental coupling.
  5. Audit / replay requirements — you need a full log of every state change for debugging or analytics.
// Redux Toolkit slice — clear action/reducer contract
const cartSlice = createSlice({
  name: 'cart',
  initialState: { items: [], status: 'idle' },
  reducers: {
    addItem:    (state, action) => { state.items.push(action.payload) },
    removeItem: (state, action) => { state.items = state.items.filter(i => i.id !== action.payload) },
  },
})

Rule of thumb: If you start adding "Redux-like" patterns to Context (reducers, action types, middleware), stop and use Redux Toolkit instead.

The key insight is that each useContext call subscribes to one context. If you split your monolithic context into smaller, focused contexts, a component that consumes only one will not re-render when another changes.

// ❌ One big context — UserBadge re-renders on every cart change
<AppContext.Provider value={{ user, cart }}>

// ✅ Separate contexts — UserBadge only subscribes to UserContext
const UserContext = createContext(null)
const CartContext = createContext(null)

function Providers({ children }) {
  const [user] = useState(null)
  const [cart, setCart] = useState([])
  return (
    <UserContext.Provider value={user}>
      <CartContext.Provider value={{ cart, setCart }}>
        {children}
      </CartContext.Provider>
    </UserContext.Provider>
  )
}

function UserBadge() {
  const user = useContext(UserContext)  // never re-renders for cart updates
  return <span>{user?.name}</span>
}

A good heuristic: each context should have a single reason to change. If you find yourself subscribing to the same context from unrelated components, split it.

Rule of thumb: One concern per context; keep Providers lean so their value references are stable.

Combining useReducer with Context gives you Redux's core pattern — a single dispatch function, a reducer, and a read-only state view — with no external dependency.

const CounterStateContext   = createContext(null)
const CounterDispatchContext = createContext(null)  // separate so consumers that only dispatch don't re-render on state changes

function counterReducer(state, action) {
  switch (action.type) {
    case 'increment': return { count: state.count + 1 }
    case 'decrement': return { count: state.count - 1 }
    default: throw new Error(`Unknown action: ${action.type}`)
  }
}

export function CounterProvider({ children }) {
  const [state, dispatch] = useReducer(counterReducer, { count: 0 })
  return (
    <CounterStateContext.Provider value={state}>
      <CounterDispatchContext.Provider value={dispatch}>
        {children}
      </CounterDispatchContext.Provider>
    </CounterStateContext.Provider>
  )
}

// Consuming components
const { count } = useContext(CounterStateContext)   // reads state
const dispatch  = useContext(CounterDispatchContext) // stable reference, never re-renders for state changes

This pattern works well for small-to-medium apps. What it still lacks: middleware, DevTools, and selector memoisation.

Rule of thumb: Context + useReducer is a great choice up to ~5 reducers; beyond that the manual wiring overhead starts to rival Redux Toolkit's setup cost.

Middleware sits between dispatch and the reducer, intercepts every action, and can transform, delay, cancel, or trigger side effects before passing the action along. The two dominant middleware are redux-thunk (async action creators) and redux-saga (complex async flows with generators).

// redux-thunk: dispatch a function instead of a plain object
export const fetchUser = (id) => async (dispatch, getState) => {
  dispatch({ type: 'users/fetchStart' })
  try {
    const user = await api.getUser(id)
    dispatch({ type: 'users/fetchSuccess', payload: user })
  } catch (err) {
    dispatch({ type: 'users/fetchFailure', payload: err.message })
  }
}

// RTK wraps this with createAsyncThunk for zero boilerplate
export const fetchUser = createAsyncThunk('users/fetch', (id) => api.getUser(id))

Context has no equivalent. You can put an async function in a Context value, but you lose the action-log, DevTools integration, cancellation tokens, and the ability to chain/cancel at the middleware layer. Replicating saga-level coordination (race conditions, debounce, takeLatest) in Context requires re-implementing what middleware already provides.

Rule of thumb: The moment you need async sequencing beyond a single async/await call, Redux middleware (or React Query for data fetching) pays for itself immediately.

Redux DevTools is a browser extension (and built-in RTK feature) that gives you a full observable record of your application's state history. Key features:

  • Action log — every dispatched action listed in order.
  • State diff — exactly what changed in the store for each action.
  • Time-travel debugging — jump to any past state by clicking an entry in the log; the UI re-renders as if it's at that point in time.
  • Action replay — replay a sequence of actions to reproduce a bug.
  • Import/export state — share a state snapshot with a team member.
// RTK configures DevTools automatically in development
import { configureStore } from '@reduxjs/toolkit'

const store = configureStore({
  reducer: rootReducer,
  // devTools: true by default in dev, false in production
})

Context has no equivalent. A console.log in a reducer or a React DevTools component inspection is the closest you can get, but neither gives you time-travel or serialisable action history.

Rule of thumb: If you've ever said "I wish I could rewind to see what caused this bug," that's the DevTools use case — and it's only available with Redux.

Modern Redux Toolkit (RTK) has dramatically closed the boilerplate gap. Creating a slice, wiring a store, and connecting a component now requires fewer files than a well-structured Context setup.

// RTK: one createSlice call replaces action types + action creators + reducer
const counterSlice = createSlice({
  name: 'counter',
  initialState: { value: 0 },
  reducers: {
    increment: state => { state.value += 1 },  // Immer under the hood — mutate safely
    decrement: state => { state.value -= 1 },
  },
})

export const { increment, decrement } = counterSlice.actions
export default counterSlice.reducer
// Context equivalent needs: createContext, Provider component,
// useState or useReducer, custom hooks, and manual memoisation —
// often spread across 3–4 files for a non-trivial piece of state.

The honest comparison in 2024: RTK setup is ~20 lines; a well-structured Context + useReducer setup for the same feature is similar or larger. The Redux tax is now mostly in learning the mental model, not in line count.

Rule of thumb: Don't avoid Redux for boilerplate reasons; the RTK API is concise. Avoid it because your state needs are genuinely simple.

Every time a Provider's parent re-renders, its value prop is a new object literal — which triggers re-renders in all consumers even if the data is identical. useMemo stabilises the reference so consumers only re-render when the underlying data actually changes.

function UserProvider({ children }) {
  const [user, setUser]   = useState(null)
  const [token, setToken] = useState(null)

  // ❌ New object every render, even if user/token haven't changed
  // return <Ctx.Provider value={{ user, setUser, token, setToken }}>

  // ✅ Stable reference — consumers re-render only when user or token changes
  const value = useMemo(
    () => ({ user, setUser, token, setToken }),
    [user, token]  // setUser/setToken are stable (from useState)
  )

  return <UserContext.Provider value={value}>{children}</UserContext.Provider>
}

When is it required? Whenever the Provider's parent can re-render for reasons unrelated to the context value — e.g., it lives at the top of the tree where any ancestor state change would cascade down.

Rule of thumb: Always wrap a Context value object in useMemo if the Provider is not at the very root of the tree; skip it only when the Provider is a static singleton that never re-renders.

When you call useContext(MyContext), you subscribe to the entire context value. There is no built-in way to say "re-render only if state.count changed" — you always get the whole object, and any change to any part of it triggers a re-render.

// With Redux + useSelector — granular subscription
const count = useSelector(state => state.counter.count)
// Only re-renders when state.counter.count changes

// With Context — no selector, full value
const { count, user, cart, theme } = useContext(AppContext)
// Re-renders whenever ANY of these change

Redux's useSelector uses strict equality (or a custom comparator) on the selector's return value. If state.counter.count hasn't changed, the component skips re-rendering even if other parts of the store did. Libraries like use-context-selector backport this behaviour to Context, but at that point you're carrying the complexity of Redux without its ecosystem.

Rule of thumb: If you need subscription granularity finer than "the whole context value", Redux (or Zustand/Jotai) is the right tool.

Zustand and Jotai occupy the pragmatic middle ground: they provide granular subscriptions and minimal boilerplate without Redux's architectural overhead.

Zustand is a single-store solution. You define state and actions together in a create call; components subscribe to slices with a selector:

import { create } from 'zustand'

const useStore = create(set => ({
  count: 0,
  increment: () => set(state => ({ count: state.count + 1 })),
}))

function Counter() {
  const count     = useStore(state => state.count)      // granular — re-renders only when count changes
  const increment = useStore(state => state.increment)  // stable reference
  return <button onClick={increment}>{count}</button>
}

Jotai uses an atomic model: each atom is an independent piece of state. Components subscribe to individual atoms, so updates are naturally scoped to only the atoms a component reads.

Neither has Redux DevTools baked in (though Zustand has a devtools middleware) or first-class saga/thunk support.

Rule of thumb: Reach for Zustand when Context starts hurting and Redux feels like overkill; reach for Jotai when you want fine-grained atomic state without a centralised store.

Context — wrap the component under test in its Provider and pass a controlled value. No special setup, no mock store.

// Testing a component that reads from Context
test('shows user name', () => {
  render(
    <UserContext.Provider value={{ user: { name: 'Ada' } }}>
      <UserBadge />
    </UserContext.Provider>
  )
  expect(screen.getByText('Ada')).toBeInTheDocument()
})

Redux — use RTK's configureStore with a real (or pre-populated) store, or use renderWithProviders from @reduxjs/toolkit/react:

test('increments counter', async () => {
  const { store } = renderWithProviders(<Counter />)
  await userEvent.click(screen.getByRole('button', { name: /increment/i }))
  expect(store.getState().counter.value).toBe(1)
})

Redux gives you direct access to store.getState() and store.dispatch() in tests, making it easy to assert against the store as well as the UI. Context is simpler to mock but harder to inspect state independently of the rendered output.

Rule of thumb: Context tests are simpler to write; Redux tests are more powerful for asserting state transitions independently from rendering.

Multiple contexts let you co-locate state near the component subtrees that use it. Each context has an independent update cycle, so they don't interfere with each other. The cost is coordination: sharing data across contexts requires lifting it to a common ancestor, which can recreate prop-drilling at the context level.

// Fine — independent, non-overlapping concerns
<AuthContext.Provider value={auth}>
  <ThemeContext.Provider value={theme}>
    <NotificationsContext.Provider value={notifications}>
      <App />
    </NotificationsContext.Provider>
  </ThemeContext.Provider>
</AuthContext.Provider>

One Redux store centralises everything. Any slice can read from any other slice in a selector. The downside is that everything is global by default — there's no encapsulation boundary.

RTK's createSlice recovers some modularity (each slice is a self-contained module), but ultimately they all merge into one global state tree.

Rule of thumb: Multiple contexts for independent concerns; a Redux store when slices need to derive state from each other or share middleware.

A useful mental checklist — before reaching for a global state solution, ask these questions:

Does only ONE component need this state?     → useState, keep it local
Do a FEW co-located components need it?      → lift state to nearest common ancestor
Do MANY unrelated components need it?        → Context (if infrequent updates)
Is it updated FREQUENTLY or needs middleware? → Redux / Zustand
Is it SERVER state (loading, caching)?        → React Query / SWR

Over-globalising state is a common mistake: putting form field values, modal-open flags, and tooltip visibility into Redux adds noise to the action log and couples unrelated parts of the app. These are ephemeral UI state — local useState is almost always the right choice.

Rule of thumb: Default to local state; promote to global only when sharing across unrelated subtrees becomes genuinely painful.

In a Server Component world (Next.js App Router), Context Providers must be Client Components — they rely on React's runtime reconciler, which only runs in the browser. Trying to render a Context Provider in a Server Component throws an error.

// app/providers.tsx — must be a Client Component
'use client'
import { ThemeContext } from './ThemeContext'

export function Providers({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState('dark')
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  )
}

// app/layout.tsx — Server Component that wraps with Providers
import { Providers } from './providers'
export default function RootLayout({ children }) {
  return <html><body><Providers>{children}</Providers></body></html>
}

Additional gotchas:

  • Hydration mismatch — if Context initialises from browser-only APIs (localStorage, cookies), the server-rendered HTML will differ from the client hydration pass, causing warnings.
  • Per-request isolation — in traditional SSR, a singleton Context module is shared across requests. Each request must create its own Provider instance; never store per-user data in a module-level variable.

Rule of thumb: In Next.js App Router, keep Providers in a single 'use client' file at the root layout and pass server-fetched data in as props rather than initialising them from async operations inside the Provider.

A strong interview answer covers three axes: frequency of updates, complexity of async logic, and team/scale requirements.

Context is right when:
  ✓ State changes infrequently (theme, auth, locale)
  ✓ No async middleware needed
  ✓ Small-to-medium app or isolated subtree
  ✓ You want zero extra dependencies

Redux (RTK) is right when:
  ✓ State updates frequently or drives complex UI
  ✓ Async sequences: loading/error states, cancellation, polling
  ✓ Derived data across multiple slices (selectors)
  ✓ Large team needs enforced conventions
  ✓ Time-travel debugging has real value

Middle ground (Zustand/Jotai):
  ✓ Granular subscriptions without Redux boilerplate
  ✓ Stores need to be shared but you don't want a full Redux setup

Interviewers want to hear that you understand the trade-offs, not just "Context is simpler." Mention re-render behaviour, selector granularity, and middleware — those are the differentiators that signal real experience.

Rule of thumb: Start with local state, promote to Context for ambient global data, reach for Redux when your state has behaviour (async, derived data, audit trail) that Context can't model cleanly.

More ways to practice

The self-quiz is live. Get notified when mock interviews and new question packs drop.

or
Join our WhatsApp Channel