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 aboveProvider
function App() {
return (
<ThemeContext value={{ color: '#fff', bg: '#111' }}>
<Page />
</ThemeContext>
)
}
// React 19: pass `value` directly — no .Provider neededConsume 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 conditionallyTyped 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 missingMultiple Contexts
function App() {
return (
<AuthContext value={auth}>
<ThemeContext value={theme}>
<Page />
</ThemeContext>
</AuthContext>
)
}
// nest providers — each is independentBeginner 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 defaultIntermediate 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 calleduseReducer + 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 aboveCompose 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 rootMemoize 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 renderConditional 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-levelContext + 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 reducersNested 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 pagePersist 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 mountLast updated on