Skip to content

useContext Interview Questions & Answers

16 questions Updated 2026-06-23 Share:

React useContext interview questions — creating and consuming context, avoiding unnecessary re-renders, splitting contexts, and context vs state management.

Read the in-depth guideReact useContext Hook — Complete Guide for Interviews(opens in new tab)
16 of 16

useContext reads the current value of a Context object and subscribes the component to updates. When the nearest matching <Provider> above it re-renders with a new value, the consumer re-renders with that new value too.

const theme = useContext(ThemeContext)
// reads the value from <ThemeContext.Provider value={...}> above

It replaces the older <ThemeContext.Consumer> render-prop API with a simple function call. The context must be created with React.createContext and the component must be rendered inside the matching Provider.

Rule of thumb: useContext solves "prop drilling" — passing data through many layers of components that don't need it themselves.

Call createContext with a default value (used when no Provider is found), then wrap the component tree with a <Provider> and pass the current value.

// 1. create
export const ThemeContext = createContext('light')

// 2. provide
function App() {
  const [theme, setTheme] = useState('dark')
  return (
    <ThemeContext.Provider value={theme}>
      <Page />
    </ThemeContext.Provider>
  )
}

// 3. consume
function Button() {
  const theme = useContext(ThemeContext)
  return <button className={theme}>click</button>
}

The default value passed to createContext is only used when a component reads the context without any Provider above it — useful for testing in isolation.

The default value is used when a consumer has no matching Provider anywhere above it in the tree. It does not apply when a Provider passes undefined or null as its value — those are valid values that override the default.

const Ctx = createContext('default')

// no Provider -> 'default'
function App() {
  return <Child />
}

// Provider with undefined -> consumer sees undefined, not 'default'
<Ctx.Provider value={undefined}>
  <Child /> {/* sees undefined */}
</Ctx.Provider>

Providing a realistic default value helps when testing components in isolation without wrapping them in a Provider.

Every component that calls useContext(Ctx) re-renders when the value prop on the nearest matching <Ctx.Provider> changes — React compares by Object.is.

// new object every render -> every consumer re-renders even if data is identical
<UserContext.Provider value={{ user, setUser }}>
  ...
</UserContext.Provider>

// stable object -> consumers only re-render when user actually changes
const ctxValue = useMemo(() => ({ user, setUser }), [user])
<UserContext.Provider value={ctxValue}>
  ...
</UserContext.Provider>

This is the #1 context performance trap. An object/array literal in the Provider's value is a new reference every render, causing all consumers to re-render even when the data is unchanged. Memoize the value object to prevent this.

Use context for data that is globally shared across many components at different nesting levels where passing props becomes verbose: user auth, theme, locale, feature flags. Avoid it for state that only flows two or three levels deep — prop drilling that shallow is cleaner and more explicit.

// fine as props: parent -> child -> grandchild (2 levels)
<Modal title="Delete?" onConfirm={handleDelete} />

// better as context: user object needed in nav, sidebar, avatar, settings
const { user } = useContext(AuthContext)

Context is not a performance tool — every consumer re-renders on value changes. If you need selective subscriptions, reach for a state management library.

Include the setter function in the context value alongside the data. Consumers call the setter, which lives in the Provider's parent and causes it to re-render with the new value — which propagates to all consumers.

const ThemeContext = createContext(null)

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

function Toggle() {
  const { theme, setTheme } = useContext(ThemeContext)
  return <button onClick={() => setTheme(t => t === 'dark' ? 'light' : 'dark')}>{theme}</button>
}

The setter has stable identity (it's a useState setter), so including it in the value doesn't cause extra renders — just make sure to memoize the value object.

Yes — call useContext once per context. There's no limit and each subscription is independent.

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

Each call registers a separate subscription. The component re-renders whenever any of the context values changes. If too many contexts cause unwanted renders, split responsibilities or memoize the component with React.memo (though context updates bypass memo).

Split the context by change frequency: separate read-rarely data (user profile) from changes-often data (notifications count). Consumers that only need stable data won't re-render when the volatile part updates.

// one big context -> all consumers re-render when count changes
<AppContext.Provider value={{ user, notifCount }}>

// split by frequency
<UserContext.Provider value={user}>       {/* changes rarely */}
  <NotifContext.Provider value={notifCount}>  {/* changes often */}
    <App />
  </NotifContext.Provider>
</UserContext.Provider>

Components reading only UserContext are unaffected by notification updates. This is the recommended pattern over a single monolithic context.

No. React.memo skips re-renders when props don't change, but it has no effect on context updates — a memoized component still re-renders when a context value it reads changes.

const MemoButton = React.memo(function Button() {
  const theme = useContext(ThemeContext) // still re-renders on ThemeContext change
  return <button className={theme}>click</button>
})

To avoid unnecessary renders, memoize the context value at the Provider level (so it doesn't change unnecessarily) rather than trying to memo the consumers.

Context suits simple, low-frequency global data: auth session, theme, locale. It re-renders all consumers on any change and has no built-in selectors or middleware.

State management libraries (Redux Toolkit, Zustand, Jotai) suit complex, frequently-changing shared state where you need selective subscriptions (only re-render components that care about specific slices), devtools, or middleware like logging and async thunks.

// context: good for this
const { user } = useContext(AuthContext)   // stable, infrequent

// library: better for this
const cartCount = useSelector(state => state.cart.items.length) // selective

The context API is built in and zero-cost; a library adds a dependency but pays for itself with high-churn data.

Both read the same context value, but useContext is simpler — you call it and get the value directly instead of rendering a component with a render-prop callback.

// old: Consumer render prop
<ThemeContext.Consumer>
  {theme => <button className={theme}>click</button>}
</ThemeContext.Consumer>

// new: useContext
const theme = useContext(ThemeContext)

useContext also avoids the nesting ("callback hell") when consuming multiple contexts. Functionally they are equivalent; useContext is the idiomatic modern approach.

Encapsulate the context, Provider, and a custom hook in one file so consumers never import the raw context object — they only call the hook. The hook also enforces that the component is inside the Provider.

const AuthContext = createContext(null)

export function AuthProvider({ children }) {
  const [user, setUser] = useState(null)
  return <AuthContext.Provider value={{ user, setUser }}>{children}</AuthContext.Provider>
}

export function useAuth() {
  const ctx = useContext(AuthContext)
  if (!ctx) throw new Error('useAuth must be used within AuthProvider')
  return ctx
}

Consumers call useAuth() directly, which gives a clear error if they're accidentally rendered outside the Provider rather than silently getting null.

Wrap the component under test in the Provider with a test value, or create a helper wrapper function for your testing library.

function renderWithAuth(ui, { user = testUser } = {}) {
  return render(
    <AuthProvider initialUser={user}>{ui}</AuthProvider>
  )
}

test('shows username', () => {
  renderWithAuth(<Profile />)
  expect(screen.getByText('Ada')).toBeInTheDocument()
})

This is why the "Provider + custom hook" pattern is useful: you can also supply a mock Provider in tests that returns controlled values without hitting real auth.

Yes, if you capture a context value in a callback or effect and the context value later changes, the closure holds the old value. The fix is the same: include the context value in the dependency array of effects and callbacks.

const { user } = useContext(AuthContext)
useEffect(() => {
  log(user.id) // safe: re-runs when user changes
}, [user])

const handleClick = useCallback(() => {
  post(user.id) // safe: recreated when user changes
}, [user])

useContext itself is always fresh (it reads the current value), but captured closures aren't — always list context-derived values in your dependency arrays.

Yes — render another Provider with a different value lower in the tree. All consumers below it receive the new value instead of the one from above.

<ThemeContext.Provider value="dark">
  <Header />  {/* sees 'dark' */}
  <ThemeContext.Provider value="light">
    <Sidebar />  {/* sees 'light' */}
  </ThemeContext.Provider>
</ThemeContext.Provider>

This is how libraries like react-query allow nested query clients. You cannot "block" a context — only override it with a new Provider further down.

Pass a function to useState inside the Provider's component, just as you would for any expensive initial computation.

function AuthProvider({ children }) {
  const [user, setUser] = useState(() => JSON.parse(localStorage.getItem('user')))
  return (
    <AuthContext.Provider value={{ user, setUser }}>
      {children}
    </AuthContext.Provider>
  )
}

The lazy initializer runs once, so the localStorage parse only happens on mount. This is especially useful for context that hydrates from storage or a cookie without re-parsing on every render.

More ways to practice

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

or
Join our WhatsApp Channel