Skip to Content
Grab N GoState (Context API)

State (Context API)

Targeting React 19

Syntax

Create Context
import { createContext } from 'react' interface Theme { color: string bg: string } const ThemeContext = createContext<Theme>({ color: '#000', bg: '#fff', }) // default value used when no Provider is above
Provider
function App() { return ( <ThemeContext value={{ color: '#fff', bg: '#111' }}> <Page /> </ThemeContext> ) } // React 19: pass `value` directly — no .Provider needed
Consume with useContext
import { useContext } from 'react' function Header() { const theme = useContext(ThemeContext) return ( <header style={{ color: theme.color, background: theme.bg }}> Hello </header> ) }
use() Hook
import { use } from 'react' function Header() { const theme = use(ThemeContext) return ( <header style={{ color: theme.color, background: theme.bg }}> Hello </header> ) } // React 19: use() can be called conditionally
Typed Context + Null Default
const AuthContext = createContext<AuthState | null>(null) function useAuth() { const ctx = useContext(AuthContext) if (!ctx) throw new Error('useAuth must be inside AuthProvider') return ctx } // null default = crash-early if Provider is missing
Multiple Contexts
function App() { return ( <AuthContext value={auth}> <ThemeContext value={theme}> <Page /> </ThemeContext> </AuthContext> ) } // nest providers — each is independent

Beginner Patterns

Theme Toggle
interface ThemeCtx { dark: boolean toggle: () => void } const ThemeContext = createContext<ThemeCtx | null>(null) function ThemeProvider({ children }: { children: React.ReactNode }) { const [dark, setDark] = useState(false) return ( <ThemeContext value={{ dark, toggle: () => setDark((d) => !d) }}> {children} </ThemeContext> ) }
Consume Theme
function ThemeButton() { const ctx = use(ThemeContext) if (!ctx) throw new Error('Missing ThemeProvider') return ( <button onClick={ctx.toggle}> {ctx.dark ? 'Light mode' : 'Dark mode'} </button> ) }
Auth Context
interface AuthState { user: User | null login: (user: User) => void logout: () => void } const AuthContext = createContext<AuthState | null>(null) function AuthProvider({ children }: { children: React.ReactNode }) { const [user, setUser] = useState<User | null>(null) return ( <AuthContext value={{ user, login: setUser, logout: () => setUser(null), }}> {children} </AuthContext> ) }
Custom Hook
function useAuth() { const ctx = useContext(AuthContext) if (!ctx) throw new Error('useAuth must be inside AuthProvider') return ctx } // usage — clean, typed, safe function Profile() { const { user, logout } = useAuth() if (!user) return <p>Not logged in</p> return <button onClick={logout}>{user.name}</button> }
Static vs Dynamic Values
// STATIC — value never changes, no re-renders const config = { apiUrl: '/api', version: 2 } function App() { return ( <ConfigContext value={config}> <Page /> </ConfigContext> ) } // DYNAMIC — value changes, consumers re-render function App() { const [locale, setLocale] = useState('en') return ( <LocaleContext value={{ locale, setLocale }}> <Page /> </LocaleContext> ) }
Default Value Shortcut
// when the default is good enough, skip the Provider const FeatureFlags = createContext({ darkMode: true, betaUI: false, }) function Banner() { const flags = useContext(FeatureFlags) if (!flags.betaUI) return null return <p>Try the new UI!</p> } // works without a Provider — uses the default

Intermediate Patterns

Split State + Dispatch
const CountStateCtx = createContext(0) const CountDispatchCtx = createContext<React.Dispatch<Action> | null>(null) function CountProvider({ children }: { children: React.ReactNode }) { const [count, dispatch] = useReducer(reducer, 0) return ( <CountStateCtx value={count}> <CountDispatchCtx value={dispatch}> {children} </CountDispatchCtx> </CountStateCtx> ) } // readers don't re-render when only dispatch is called
useReducer + Context
type Action = | { type: 'inc' } | { type: 'dec' } | { type: 'set'; value: number } function reducer(state: number, action: Action) { switch (action.type) { case 'inc': return state + 1 case 'dec': return state - 1 case 'set': return action.value } } // pair with split state + dispatch pattern above
Compose Providers
function Providers({ children }: { children: React.ReactNode }) { return ( <AuthProvider> <ThemeProvider> <LocaleProvider> {children} </LocaleProvider> </ThemeProvider> </AuthProvider> ) } // app.tsx function App() { return <Providers><Page /></Providers> } // avoids deep nesting in the app root
Memoize Provider Value
function CartProvider({ children }: { children: React.ReactNode }) { const [items, setItems] = useState<Item[]>([]) const value = useMemo(() => ({ items, add: (item: Item) => setItems((prev) => [...prev, item]), clear: () => setItems([]), total: items.reduce((s, i) => s + i.price, 0), }), [items]) return <CartContext value={value}>{children}</CartContext> } // useMemo prevents new object on every render
Conditional use()
function MaybeThemed({ themed }: { themed: boolean }) { // use() can be called inside conditions — useContext cannot const style = themed ? use(ThemeContext) : { color: '#000', bg: '#fff' } return <div style={{ color: style.color }}>{/* ... */}</div> } // React 19 only — useContext must be top-level
Context + Server Components
// context only works in client components // pass server data down through props, wrap with context at the boundary // layout.tsx (server) export default async function Layout({ children }: { children: React.ReactNode }) { const user = await getUser() return <AuthProvider user={user}>{children}</AuthProvider> } // AuthProvider.tsx (client) 'use client' function AuthProvider({ user, children }: Props) { return <AuthContext value={{ user }}>{children}</AuthContext> }

Advanced Patterns

Generic Context Factory
function createSafeContext<T>(name: string) { const Ctx = createContext<T | null>(null) function useCtx() { const value = useContext(Ctx) if (!value) throw new Error(`use${name} must be inside ${name}Provider`) return value } function Provider({ value, children }: { value: T; children: React.ReactNode }) { return <Ctx value={value}>{children}</Ctx> } return [Provider, useCtx] as const } const [ToastProvider, useToast] = createSafeContext<ToastState>('Toast')
Scoped State via Key
function TabPanel({ id }: { id: string }) { const [state, setState] = useState({ filter: '' }) return ( <PanelContext value={{ state, setState }}> <PanelContent /> </PanelContext> ) } // each TabPanel gets its own context instance // state is isolated per panel automatically function App() { return tabs.map((t) => <TabPanel key={t.id} id={t.id} />) }
Middleware Pattern
function LoggingProvider({ children }: { children: React.ReactNode }) { const [state, rawDispatch] = useReducer(reducer, initialState) const dispatch = useCallback((action: Action) => { console.log('[dispatch]', action) rawDispatch(action) }, []) return ( <StateCtx value={state}> <DispatchCtx value={dispatch}> {children} </DispatchCtx> </StateCtx> ) } // wrap dispatch to add logging, analytics, etc.
Async Actions
function useCart() { const dispatch = useContext(CartDispatchCtx)! const checkout = useCallback(async () => { dispatch({ type: 'checkout_start' }) try { await api.post('/checkout') dispatch({ type: 'checkout_success' }) } catch { dispatch({ type: 'checkout_error' }) } }, [dispatch]) return { checkout } } // keep async logic in custom hooks, not in reducers
Nested Override
function App() { return ( <ThemeContext value={{ mode: 'light' }}> <Page /> <ThemeContext value={{ mode: 'dark' }}> <Sidebar /> {/* gets dark theme */} </ThemeContext> </ThemeContext> ) } // inner Provider overrides outer for its subtree // useful for themed sections within a page
Persist to Storage
function SettingsProvider({ children }: { children: React.ReactNode }) { const [settings, setSettings] = useState<Settings>(() => { const saved = localStorage.getItem('settings') return saved ? JSON.parse(saved) : defaults }) useEffect(() => { localStorage.setItem('settings', JSON.stringify(settings)) }, [settings]) return ( <SettingsContext value={{ settings, setSettings }}> {children} </SettingsContext> ) } // lazy initializer reads from storage once on mount
Last updated on