01The 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.
02Redux 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;
});
},
});03Custom 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