Reducer for related state
Keep state that changes together in one reducer, shared by Context.
From frontend-patterns, “Context + Reducer Pattern”.
Every change is a named action
Press Load, then pick a market.
<MarketProvider>Live
Action log0 actions
No actions
state = { markets: [], selectedMarket: null, loading: false }
type Action =
| { type: 'SET_MARKETS'; payload: Market[] }
| { type: 'SELECT_MARKET'; payload: Market }
| { type: 'SET_LOADING'; payload: boolean }
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'SET_MARKETS':
return { ...state, markets: action.payload }
case 'SELECT_MARKET':
return { ...state, selectedMarket: action.payload }
case 'SET_LOADING':
return { ...state, loading: action.payload }
default:
return state
}
}
const MarketContext = createContext<{
state: State
dispatch: Dispatch<Action>
} | undefined>(undefined)
export function MarketProvider({ children }: { children: React.ReactNode }) {
const [state, dispatch] = useReducer(reducer, {
markets: [],
selectedMarket: null,
loading: false
})
return (
<MarketContext.Provider value={{ state, dispatch }}>
{children}
</MarketContext.Provider>
)
}
Setters drift, a reducer holds
Press Load, Select ETH, then Refresh.
3 × useStateBad
Consistent
setMarkets(data) // forgot setLoading(false)useReducerGood
Consistent
dispatch({ type: 'SET_MARKETS', payload: data }) // same slipcase 'SET_MARKETS': {
const markets = action.payload
const stillListed = markets.some(m => m.id === state.selectedMarket?.id)
return {
...state,
markets,
loading: false,
selectedMarket: stillListed ? state.selectedMarket : null
}
}
case 'SELECT_MARKET':
return state.markets.some(m => m.id === action.payload.id)
? { ...state, selectedMarket: action.payload }
: state
Guarded hook fails fast
Untick the provider. Only one error says why.
Bare useContextBad
const { state } = useContext(MarketContext)Guarded useMarketsGood
const { state } = useMarkets()export function useMarkets() {
const context = useContext(MarketContext)
if (!context) throw new Error('useMarkets must be used within MarketProvider')
return context
}
Why it matters, pros and cons
Why it matters
Each transition is a named action in one pure function. Rules live there once.
Pros
- One place to read, test, fix.
- Typed
Actionunion catches bad dispatches. - Missing provider fails with a clear error.
Cons
- Overkill for one or two independent values.
- Every consumer re-renders on any field change.
- Server data belongs in a query library.