REDUXCONTEXT API
STATE MANAGEMENT AT SCALE
Redux Toolkit patterns, Context API tradeoffs, and when to use which approach.
The State Management Spectrum
Not all state is equal. URL state (React Router), server state (React Query/SWR), form state (react-hook-form), UI state (local useState), and global app state (Redux/Zustand) each have optimal tools. The mistake is using one tool for everything.
Redux Toolkit — When & Why
Use RTK when you have complex state logic shared across many components, need time-travel debugging, or want predictable state updates in a team.
tsx
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
// Async thunk for API calls
export const fetchProjects = createAsyncThunk(
'projects/fetch',
async (orgId: string) => {
const res = await api.get(`/orgs/${orgId}/projects`);
return res.data;
}
);
const projectsSlice = createSlice({
name: 'projects',
initialState: { items: [], status: 'idle' } as ProjectState,
reducers: {
projectUpdated(state, action) {
const idx = state.items.findIndex(p => p.id === action.payload.id);
if (idx !== -1) state.items[idx] = action.payload;
},
},
extraReducers: (builder) => {
builder
.addCase(fetchProjects.pending, (state) => { state.status = 'loading'; })
.addCase(fetchProjects.fulfilled, (state, action) => {
state.status = 'idle';
state.items = action.payload;
});
},
});Custom Hooks as State Machines
Sometimes the best state management is a well-designed custom hook that encapsulates complex logic and is testable in isolation.
tsx
// Track dirty state by diffing current vs saved config
function useFormDiff<T extends Record<string, unknown>>(saved: T) {
const [current, setCurrent] = useState<T>(saved);
const dirtyFields = useMemo(() => {
const dirty = new Set<string>();
for (const key of Object.keys(current)) {
if (JSON.stringify(current[key]) !== JSON.stringify(saved[key])) {
dirty.add(key);
}
}
return dirty;
}, [current, saved]);
const isDirty = dirtyFields.size > 0;
const reset = useCallback(() => setCurrent(saved), [saved]);
return { current, setCurrent, dirtyFields, isDirty, reset };
}
// Usage
const { current, setCurrent, isDirty } = useFormDiff(savedConfig);
// Show blinking save indicator when isDirty === true