Skip to content

Context (provide / inject with extra steps) ​

Same job as Vue's provide/inject: pass data through the tree without prop-drilling. Slightly more boilerplate. Easy to overuse.

The pattern ​

tsx
import { createContext, useContext, useState, type ReactNode } from 'react'

type Theme = 'light' | 'dark'

const ThemeContext = createContext<Theme | null>(null)

export function ThemeProvider({ children }: { children: ReactNode }) {
  const [theme, setTheme] = useState<Theme>('light')

  return (
    <ThemeContext.Provider value={theme}>
      <button type="button" onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
        Toggle
      </button>
      {children}
    </ThemeContext.Provider>
  )
}

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

Comments for the code above:

  • createContext + Provider ≈ provide
  • useContext ≈ inject
  • Wrapping a custom hook that throws if missing is a nice habit (fail loud)

Consuming it ​

tsx
function Headline() {
  const theme = useTheme()
  return <h1 data-theme={theme}>Hello</h1>
}

// somewhere near the root
<ThemeProvider>
  <Headline />
</ThemeProvider>

Passing value + setter ​

Often you want read + update. Pack an object (or split contexts - advanced optimization):

tsx
type ThemeCtx = {
  theme: Theme
  setTheme: (t: Theme) => void
}

const ThemeContext = createContext<ThemeCtx | null>(null)

// Provider value:
<ThemeContext.Provider value={{ theme, setTheme }}>

Watch out: a new object every render re-renders all consumers. For theme toggles it's usually fine. For hot high-frequency values, people memoize or use a store (Zustand) instead.

When Context is enough ​

  • Theme, locale, current user session
  • Dependency injection for a small app module
  • Avoiding 4 layers of prop drilling for one value

When I'd skip Context ​

  • Server cache / async lists → TanStack Query
  • Client global UI state with lots of updates → Zustand
  • Anything that changes every keystroke and fans out to a huge tree

As you can see - Context is glue, not a state management framework. Vue folks who used provide/inject for everything sometimes try to recreate Pinia with Context. Don't. Use a store.

Next: React Router - your Vue Router instincts mostly transfer.

Best,

-- ł.