Note The default value is used only when a component reads the context but there is no matching Provider above it in the tree. Pass a realistic shape as the default so TypeScript inference and fallback behavior work correctly.
import{useState}from'react';import{ThemeContext}from'./theme-context';functionThemeProvider({ children }){const[mode, setMode]=useState('light');const contextValue ={
mode,
toggleTheme:()=>setMode(m => m ==='light'?'dark':'light'),};return(<ThemeContext.Provider value={contextValue}>{children}</ThemeContext.Provider>);}
Note Wrap the Provider at the highest point where descendant components need the value. If the value is a new object on every render, wrap it with useMemo to prevent unnecessary re-renders of consumers.
provide contextcontext providerwrap with providerpass context value
Consuming Context
Syntax
import{useContext}from'react';const value =useContext(MyContext);
Note useContext always looks for the nearest Provider above the calling component. If none is found, it uses the default value from createContext. The component re-renders whenever the provided value changes.
useContextconsume contextread contextaccess context value
functionAppProviders({ children }){const auth =useAuthState();const theme =useThemeState();const locale =useLocaleState();return(<AuthContext.Provider value={auth}><ThemeContext.Provider value={theme}><LocaleContext.Provider value={locale}>{children}</LocaleContext.Provider></ThemeContext.Provider></AuthContext.Provider>);}// Usage at app root<AppProviders><App/></AppProviders>
Note Split unrelated data into separate contexts. A single monolithic context forces every consumer to re-render when any part of the value changes, even if they only use a small slice.
// Encapsulate context + hook in a moduleconstCtx=createContext(null);exportfunctionuseMyContext(){const ctx =useContext(Ctx);if(!ctx)thrownewError('Missing Provider');return ctx;}
Example
import{ createContext,useContext,useState}from'react';constCartContext=createContext(null);exportfunctionCartProvider({ children }){const[items, setItems]=useState([]);functionaddItem(product){setItems(prev =>[...prev,{...product, qty:1}]);}functionremoveItem(id){setItems(prev => prev.filter(item => item.id!== id));}const totalItems = items.reduce((sum, i)=> sum + i.qty,0);return(<CartContext.Provider value={{ items, addItem, removeItem, totalItems }}>{children}</CartContext.Provider>);}exportfunctionuseCart(){const context =useContext(CartContext);if(!context){thrownewError('useCart must be used within a CartProvider');}return context;}
Note Exporting a custom hook that throws when the Provider is missing catches mistakes early and gives a clear error message instead of undefined values that silently break downstream.
context custom hookcontext with stateprovider patterncart contextglobal state context
Avoiding Unnecessary Re-renders
Syntax
// Memoize the value objectconst value =useMemo(()=>({ data, handler }),[data]);// Split read-only data from dispatch<DataContext.Provider value={data}><DispatchContext.Provider value={dispatch}>
Example
import{ createContext,useContext,useMemo,useState}from'react';constCountContext=createContext(null);constCountDispatchContext=createContext(null);functionCountProvider({ children }){const[count, setCount]=useState(0);// dispatch never changes identityconst increment =useMemo(()=>()=>setCount(c => c +1),[]);return(<CountContext.Provider value={count}><CountDispatchContext.Provider value={increment}>{children}</CountDispatchContext.Provider></CountContext.Provider>);}// Components reading only dispatch won't re-render when count changesfunctionIncrementButton(){const increment =useContext(CountDispatchContext);return<button onClick={increment}>+1</button>;}
Note Splitting context into a data context and a dispatch context is a powerful pattern. Components that only call actions (dispatch) will not re-render when the data changes, because the dispatch reference stays stable.