TypeScript State Management — Complete Guide
In this tutorial, you will learn about TypeScript State Management. We cover key concepts, practical examples, and best practices to help you master this topic.
State management libraries with TypeScript provide typed stores that enforce correct state shapes, actions, and selectors at compile time — preventing entire categories of bugs in complex React applications.
What You'll Learn
- Zustand with typed stores and actions
- Redux Toolkit with createSlice and typed hooks
- Jotai atoms with TypeScript
- Comparing state management approaches
Why It Matters
As applications grow, prop drilling and context re-renders become bottlenecks. State management libraries provide efficient, type-safe global state with minimal boilerplate and excellent developer experience through full TypeScript integration.
Real-World Use
DodaTech's Durga Antivirus Pro dashboard uses Zustand for its scan state — the store holds active scans, scan history, and real-time threat data. Zustand's TypeScript support ensures that components only access valid properties and dispatch correct actions.
Learning Path
flowchart LR A[React Context] --> B[State Management] B --> C[React Advanced] B --> D[You Are Here] C --> E[Node Setup] E --> F[Express APIs]
Zustand
npm install zustand
import { create } from 'zustand';
interface ScanStore {
scans: string[];
activeScan: string | null;
addScan: (id: string) => void;
setActiveScan: (id: string | null) => void;
clearScans: () => void;
}
const useScanStore = create<ScanStore>((set) => ({
scans: [],
activeScan: null,
addScan: (id) => set((state) => ({ scans: [...state.scans, id] })),
setActiveScan: (id) => set({ activeScan: id }),
clearScans: () => set({ scans: [], activeScan: null }),
}));
// In components
function ScanList() {
const scans = useScanStore((state) => state.scans);
const addScan = useScanStore((state) => state.addScan);
return (
<div>
{scans.map(id => <div key={id}>{id}</div>)}
<button onClick={() => addScan(`scan-${Date.now()}`)}>Add Scan</button>
</div>
);
}
Zustand with Async Actions
interface AuthStore {
user: User | null;
token: string | null;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
}
const useAuthStore = create<AuthStore>((set) => ({
user: null,
token: null,
login: async (email, password) => {
const res = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
const data = await res.json();
set({ user: data.user, token: data.token });
},
logout: () => set({ user: null, token: null }),
}));
Zustand with Middleware
import { persist } from 'zustand/middleware';
const useConfigStore = create(
persist<ConfigStore>(
(set) => ({
theme: 'light' as const,
notifications: true,
setTheme: (theme) => set({ theme }),
toggleNotifications: () => set((s) => ({ notifications: !s.notifications })),
}),
{ name: 'app-config' }
)
);
Redux Toolkit
npm install @reduxjs/toolkit react-redux
import { createSlice, configureStore, PayloadAction } from '@reduxjs/toolkit';
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
interface ScanState {
scans: { id: string; threats: string[]; status: string }[];
loading: boolean;
}
const initialState: ScanState = {
scans: [],
loading: false,
};
const scanSlice = createSlice({
name: 'scans',
initialState,
reducers: {
addScan(state, action: PayloadAction<{ id: string; threats: string[] }>) {
state.scans.push({ ...action.payload, status: 'completed' });
},
setLoading(state, action: PayloadAction<boolean>) {
state.loading = action.payload;
},
clearScans(state) {
state.scans = [];
},
},
});
const store = configureStore({
reducer: { scans: scanSlice.reducer },
});
type RootState = ReturnType<typeof store.getState>;
type AppDispatch = typeof store.dispatch;
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
// In component
function ScanDashboard() {
const scans = useAppSelector((state) => state.scans.scans);
const dispatch = useAppDispatch();
return <div>{scans.length} scans</div>;
}
Jotai
npm install jotai
import { atom, useAtom } from 'jotai';
const countAtom = atom(0);
const scanAtom = atom<{ id: string; status: string } | null>(null);
// Derived atom
const scanStatusAtom = atom((get) => {
const scan = get(scanAtom);
return scan?.status ?? 'idle';
});
// Async atom
const userAtom = atom(async () => {
const response = await fetch('/api/user');
return response.json();
});
function Counter() {
const [count, setCount] = useAtom(countAtom);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
Comparison
| Library | Bundle Size | Boilerplate | TypeScript | When to Use |
|---|---|---|---|---|
| Zustand | ~1KB | Minimal | Excellent | Simple to medium apps |
| Redux Toolkit | ~11KB | Moderate | Excellent | Large apps, team standards |
| Jotai | ~3KB | Minimal | Good | Atomic state, Recoil-like |
| Context | 0KB | Low | Good | Simple shared state |
Common Mistakes
1. Not Memoizing Selectors in Zustand
// Bad — creates new function each render
const value = useScanStore((state) => state.scans.length > 0);
// Good — stable selector
const hasScans = useScanStore((state) => state.scans.length > 0);
2. Mutating State Outside Immer (Redux Toolkit)
Redux Toolkit uses Immer — mutations in createSlice reducers are safe. In Zustand, use the spread operator or Immer middleware.
3. Putting Non-Serializable Values in Redux
Redux requires serializable state. Functions, Promises, and class instances belong outside the store.
4. Overusing Global State
Not everything needs global state. Server state belongs in React Query or SWR, not in a global store.
5. Forgetting to Extract Typed Hooks for Redux
// Export these once from your store file
export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
Practice Questions
What is the main advantage of Zustand over Context? Zustand components only re-render when their selected state changes, not when any part of the store changes.
What does
PayloadAction<T>represent in Redux Toolkit? A typed action with apayloadof typeT.What is a derived atom in Jotai? An atom computed from other atoms, updating automatically when dependencies change.
How do you persist Zustand state? Use the
persistmiddleware with a storage backend (default: localStorage).
Challenge: Create a Zustand store for a shopping cart with items, addItem, removeItem, total (derived), and clearCart actions. Use TypeScript throughout.
FAQ
Mini Project: Scan Dashboard Store
import { create } from 'zustand';
interface Threat { name: string; severity: 'low' | 'medium' | 'high'; }
interface ScanItem { id: string; file: string; threats: Threat[]; timestamp: number; }
interface DashboardStore {
scans: ScanItem[];
isScanning: boolean;
addScan: (file: string) => void;
clearCompleted: () => void;
totalThreats: () => number;
}
const useDashboardStore = create<DashboardStore>((set, get) => ({
scans: [],
isScanning: false,
addScan: (file) => set((state) => ({
scans: [...state.scans, {
id: `scan-${Date.now()}`,
file,
threats: Math.random() > 0.5
? [{ name: 'Trojan.Generic', severity: 'high' }]
: [],
timestamp: Date.now(),
}],
})),
clearCompleted: () => set({ scans: [] }),
totalThreats: () => get().scans.reduce((sum, s) => sum + s.threats.length, 0),
}));
What's Next
Now explore advanced React patterns:
| Lesson | Description |
|---|---|
| {{< ref "/programming-languages/typescript/40-react-context" >}} | Review context |
| {{< ref "/programming-languages/typescript/42-react-advanced" >}} | forwardRef, HOCs, render props |
| {{< ref "/programming-languages/typescript/43-node-setup" >}} | Node.js with TypeScript |
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro