Skip to content

Context API Interview Questions & Answers

15 questions Updated 2026-06-24 Share:

React Context API interview questions — createContext, useContext, Provider, re-render behaviour, multiple contexts, and when to use Context vs. Redux.

Read the in-depth guideReact Context API — A Complete Guide(opens in new tab)
15 of 15

Context provides a way to pass data through the component tree without threading props through every intermediate level — solving what is known as prop drilling.

// Without Context — every layer must pass `theme` down
<App theme="dark">
  <Layout theme="dark">
    <Sidebar theme="dark">
      <Button theme="dark" />
    </Sidebar>
  </Layout>
</App>

// With Context — Button reads theme directly
const ThemeContext = createContext('light')

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

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

Rule of thumb: Use Context for truly global or widely shared data (theme, locale, auth user). Don't reach for it to avoid one or two levels of prop passing.

createContext(defaultValue) creates a Context object. The default value is used only when a component reads the context without a matching Provider above it in the tree — it is not the initial value of the Provider.

// Default value is 'light' — used only outside any Provider
const ThemeContext = createContext('light')

function App() {
  return (
    <ThemeContext.Provider value="dark">
      <Child />   {/* reads 'dark' */}
    </ThemeContext.Provider>
  )
}

function Orphan() {
  // No Provider above → reads the default 'light'
  const theme = useContext(ThemeContext)
  return <div>{theme}</div>
}

Rule of thumb: Set the default value to something that makes the component usable in isolation (e.g. in tests or Storybook) without wrapping it in a Provider.

Call useContext(MyContext) at the top level of the component. It returns the current context value provided by the nearest matching Provider up the tree.

import { createContext, useContext } from 'react'

const UserContext = createContext(null)

function Greeting() {
  const user = useContext(UserContext)
  if (!user) return <p>Please log in</p>
  return <p>Hello, {user.name}</p>
}

function App() {
  const [user] = useState({ name: 'Alice' })
  return (
    <UserContext.Provider value={user}>
      <Greeting />
    </UserContext.Provider>
  )
}

Rule of thumb: useContext is the modern replacement for the legacy Context.Consumer render-prop API. Use it exclusively in function components.

Every component that calls useContext(MyContext) re-renders when the Provider's value prop changes (by reference). Intermediate components that don't consume the context are not re-rendered.

const CountCtx = createContext(0)

function Parent() {
  const [count, setCount] = useState(0)
  return (
    // Object literal creates a new reference every render — all consumers re-render
    <CountCtx.Provider value={{ count, setCount }}>
      <Middle />
    </CountCtx.Provider>
  )
}

function Middle() {
  // does NOT use CountCtx → NOT re-rendered when count changes
  return <Consumer />
}

function Consumer() {
  const { count } = useContext(CountCtx)   // re-renders on every count change
  return <span>{count}</span>
}

Rule of thumb: Memoize the context value (useMemo) when the Provider's parent re-renders frequently to avoid creating a new reference each time.

Every time the Provider's parent re-renders, a new object or array literal creates a new reference. React's shallow equality check sees a new reference → all consumers re-render even if the data hasn't changed.

// ❌ New object every render → all consumers re-render needlessly
<AuthContext.Provider value={{ user, logout }}>

// ✅ Stable reference — only changes when user or logout changes
const authValue = useMemo(() => ({ user, logout }), [user, logout])
<AuthContext.Provider value={authValue}>

If the value is a primitive (string, number) memoisation is unnecessary because primitives are compared by value.

Rule of thumb: Whenever you pass an object or array as a context value, wrap it in useMemo.

Yes. Call useContext once per context. Compose Providers by nesting them at the top of the tree — order only matters if a Provider reads from a sibling context.

function App() {
  return (
    <ThemeContext.Provider value="dark">
      <AuthContext.Provider value={currentUser}>
        <LocaleContext.Provider value="en">
          <Router />
        </LocaleContext.Provider>
      </AuthContext.Provider>
    </ThemeContext.Provider>
  )
}

function Header() {
  const theme  = useContext(ThemeContext)
  const user   = useContext(AuthContext)
  const locale = useContext(LocaleContext)
  // ...
}

If nesting becomes unwieldy, extract a single AppProviders wrapper component that composes them all.

Rule of thumb: Create one context per concern; don't cram unrelated values into a single context object.

Use Context when data is needed by many components at different nesting levels and passing it via props would require threading it through many intermediate layers that don't need it themselves.

Signal Prefer
1–2 levels of passing Props
Truly global (theme, locale, auth) Context
Frequently changes, many subscribers External store
// Props are fine — shallow tree, one consumer
<Page><Card title={title} /></Page>

// Context fits — auth needed everywhere, changes rarely
<AuthContext.Provider value={user}>
  <Navbar /><Routes /><Footer />
</AuthContext.Provider>

Rule of thumb: If you're passing the same prop through 3+ levels that don't use it, that's the signal to consider Context.

Aspect Context Redux / Zustand
Built-in Yes External dependency
Re-render granularity All consumers of the context Selector-level (only subscribe to what you need)
DevTools None Time-travel, action log
Async logic Manual Built-in middleware (thunk, saga)
Boilerplate Low Medium (RTK reduces it)

Context is best for infrequently changing global values (theme, locale, authenticated user). For frequently updating shared state with many subscribers, a store library avoids cascade re-renders.

Rule of thumb: Don't reach for Redux just because you have global state. Context + useReducer covers a lot of ground; add a store when you need fine-grained subscriptions or middleware.

A custom hook hides the context import, adds a helpful error message when used outside the Provider, and creates a stable API so consumers don't need to know which context backs it.

// context/ThemeContext.jsx
const ThemeContext = createContext(null)

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

export function useTheme() {
  const ctx = useContext(ThemeContext)
  if (!ctx) throw new Error('useTheme must be used inside ThemeProvider')
  return ctx
}

// Consumer — no context import needed
const { theme, setTheme } = useTheme()

Rule of thumb: Always export a useFoo() hook instead of exporting the raw Context object.

Provide separate contexts for state and dispatch (or read vs. write). Components that only dispatch won't re-render when state changes, and vice versa.

const CountStateCtx   = createContext(null)
const CountDispatchCtx = createContext(null)

function CountProvider({ children }) {
  const [count, dispatch] = useReducer(reducer, 0)
  return (
    <CountDispatchCtx.Provider value={dispatch}>
      <CountStateCtx.Provider value={count}>
        {children}
      </CountStateCtx.Provider>
    </CountDispatchCtx.Provider>
  )
}

// Only re-renders when count changes
function Counter()   { return <span>{useContext(CountStateCtx)}</span> }

// Never re-renders due to count changes (dispatch is stable)
function IncrBtn()   { return <button onClick={() => useContext(CountDispatchCtx)({ type: 'inc' })}>+</button> }

Rule of thumb: If your context value has both frequently-changing state and stable callbacks, put them in separate contexts.

Keep useReducer in a Provider component. Pass state via one context and the stable dispatch via another (or together if updates are infrequent).

const StoreCtx = createContext(null)

function reducer(state, action) {
  switch (action.type) {
    case 'SET_USER': return { ...state, user: action.payload }
    case 'LOGOUT':   return { ...state, user: null }
    default:         return state
  }
}

export function StoreProvider({ children }) {
  const [state, dispatch] = useReducer(reducer, { user: null })
  const value = useMemo(() => ({ state, dispatch }), [state])
  return <StoreCtx.Provider value={value}>{children}</StoreCtx.Provider>
}

export const useStore = () => useContext(StoreCtx)

This pattern gives you Redux-like action dispatch without adding a dependency — ideal for small to medium apps.

Rule of thumb: useReducer + Context is the sweet spot before you need Redux's devtools or middleware.

Use useMemo for objects/arrays passed as the context value, and useCallback for functions, so React sees a stable reference and skips re-rendering consumers.

function AuthProvider({ children }) {
  const [user, setUser] = useState(null)

  const logout = useCallback(() => {
    setUser(null)
    api.logout()
  }, [])                         // stable function reference

  const value = useMemo(
    () => ({ user, logout }),
    [user, logout]               // recompute only when user changes
  )

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

Rule of thumb: Any context value that is an object or contains functions should be memoised at the Provider level.

Using null (or undefined) as the default forces consumers to handle the "no Provider" case explicitly and makes it easy to detect misuse. A non-null default value makes the component work outside a Provider, which is useful for testing and Storybook.

// null default — consumers must check or use a custom hook with an error
const AuthContext = createContext(null)

// Non-null default — component works in isolation; good for theming
const ThemeContext = createContext({ mode: 'light', toggle: () => {} })

Choose based on intent:

  • If the component cannot work without a Provider → default null, throw in the custom hook.
  • If the component can work standalone → provide a real fallback.

Rule of thumb: Auth/session contexts default to null; config/theme contexts default to a sensible fallback object.

As low as possible while still wrapping all consumers. Placing it too high (e.g. at app root) means unrelated subtrees re-render when the context value changes.

// ❌ Too high — the entire app re-renders when modal state changes
function App() {
  const [isOpen, setIsOpen] = useState(false)
  return (
    <ModalContext.Provider value={{ isOpen, setIsOpen }}>
      <Header /><Main /><Footer />
    </ModalContext.Provider>
  )
}

// ✅ Scoped to the subtree that needs it
function CheckoutPage() {
  const [isOpen, setIsOpen] = useState(false)
  return (
    <ModalContext.Provider value={{ isOpen, setIsOpen }}>
      <CheckoutForm /><ConfirmModal />
    </ModalContext.Provider>
  )
}

Rule of thumb: The Provider should be the lowest ancestor that contains all the components that consume the context.

Wrap the component under test in the Provider and supply a controlled test value. With React Testing Library:

import { render, screen } from '@testing-library/react'
import { ThemeContext } from './ThemeContext'
import ThemedButton from './ThemedButton'

function renderWithTheme(ui, theme = 'light') {
  return render(
    <ThemeContext.Provider value={theme}>
      {ui}
    </ThemeContext.Provider>
  )
}

test('applies dark class in dark mode', () => {
  renderWithTheme(<ThemedButton />, 'dark')
  expect(screen.getByRole('button')).toHaveClass('dark')
})

Alternatively, if you use the custom-hook pattern, wrap in the real Provider component from your codebase.

Rule of thumb: Never mock context directly — always wrap with the real Provider so tests exercise the full integration.

More ways to practice

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

or
Join our WhatsApp Channel