Skip to content

TypeScript State Management — Complete Guide

DodaTech Updated 2026-06-28 6 min read

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

  1. 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.

  2. What does PayloadAction<T> represent in Redux Toolkit? A typed action with a payload of type T.

  3. What is a derived atom in Jotai? An atom computed from other atoms, updating automatically when dependencies change.

  4. How do you persist Zustand state? Use the persist middleware 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

Do I need a state management library for every React app?

No. Start with local state and lifting state up. Add Zustand or Context when prop drilling becomes painful.

What is the difference between Zustand and Redux?

Zustand is simpler (no reducers, no actions, no dispatch). Redux has more structure and middleware ecosystem.

Can I use Zustand with React Native?

Yes. Zustand works in any React environment.

What is Immer in Redux Toolkit?

Immer lets you write reducers as mutable code (.push, assignment) that is internally converted to immutable updates.

Should I put all state in the store?

No. Keep local state local. Use stores for truly shared global state.

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