Skip to main content

🧱 Part 9 — Global State with Redux Toolkit (RTK + TypeScript) 🗂️


Some state doesn't come from the server and shouldn't be fetched via HTTP—think theme (light/dark), active filters, sidebar open/closed, currently selected task ID, etc. This is global/app state. For this, Redux Toolkit (RTK) gives you a clean, predictable, and type‑safe pattern.

Quick mental model:

  • React state → great for local component-only state.
  • Redux (RTK) → great for app-wide UI state that multiple components need.
  • TanStack Query → great for server data (caching, refetching, syncing).

🧩 Core Concepts (explained simply)

  • Store: a single object tree that holds your app’s global state.
  • Slice: a focused piece of state (e.g., ui, filters) with its reducers and actions created by RTK.
  • Reducer: a pure function (state, action) → newState. In RTK you write mutating-looking code; under the hood Immer produces an immutable update for you.
  • Action: a plain object { type, payload } describing what happened.
  • Selector: a function that reads/derives data from the store (can be memoized).
  • Dispatch: how you send actions to update the store.

0) Install

pnpm add @reduxjs/toolkit react-redux
pnpm add -D @types/react-redux

1) Design the state shape (keep it simple & serializable)

We’ll manage a UI slice that controls theme, modal visibility, and task list filters.

// src/store/uiSlice.ts
import { createSlice, type PayloadAction } from '@reduxjs/toolkit';

// 1) Define the TypeScript shape of this slice of state
export type UIState = {
  theme: 'light' | 'dark';          // app theme
  modalOpen: boolean;               // global modal visibility
  filter: {
    search: string;                 // text filter for tasks
    showDone: boolean;              // whether to show completed tasks
  };
};

// 2) Initial state — keep it minimal, serializable, and UI-focused
const initialState: UIState = {
  theme: 'light',
  modalOpen: false,
  filter: {
    search: '',
    showDone: true,
  },
};

// 3) Create the slice — RTK generates action creators & action types for you
const uiSlice = createSlice({
  name: 'ui',
  initialState,
  reducers: {
    // Immer lets us "mutate" state safely (it produces an immutable copy)
    toggleTheme(state) {
      state.theme = state.theme === 'light' ? 'dark' : 'light';
    },
    setModal(state, action: PayloadAction<boolean>) {
      state.modalOpen = action.payload;
    },
    setSearch(state, action: PayloadAction<string>) {
      state.filter.search = action.payload;
    },
    setShowDone(state, action: PayloadAction<boolean>) {
      state.filter.showDone = action.payload;
    },
    // Example reducer with a structured payload
    setFilter(state, action: PayloadAction<{ search?: string; showDone?: boolean }>) {
      if (action.payload.search !== undefined) state.filter.search = action.payload.search;
      if (action.payload.showDone !== undefined) state.filter.showDone = action.payload.showDone;
    },
  },
});

// 4) Export actions & reducer
export const { toggleTheme, setModal, setSearch, setShowDone, setFilter } = uiSlice.actions;
export default uiSlice.reducer;

Why this is nice:

  • No action type strings to handwrite (RTK does it for you).
  • PayloadAction<T> gives typed payloads and better DX.
  • Code looks mutable but stays safely immutable via Immer.

2) Create the store and infer types automatically

// src/store/index.ts
import { configureStore } from '@reduxjs/toolkit';
import ui from './uiSlice';

// configureStore sets up Redux DevTools and good defaults (like thunk middleware)
export const store = configureStore({
  reducer: { ui },
  // middleware: (getDefault) => getDefault(), // customize here if needed
  // devTools: process.env.NODE_ENV !== 'production', // optional explicit toggle
});

// 🔹 Inferred types from the store itself — no duplication
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

Tip: Avoid writing these types manually; always infer from the created store.


3) Typed hooks (avoid re-typing in every component)

// src/store/hooks.ts
import { useDispatch, useSelector, type TypedUseSelectorHook } from 'react-redux';
import type { RootState, AppDispatch } from './index';

// Use these across your app for correct types automatically
export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

4) Provide the store to React

// src/main.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import { store } from '@/store';
import AppRouter from '@/app/AppRouter';

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <Provider store={store}>
      <AppRouter />
    </Provider>
  </React.StrictMode>,
);

5) Selectors (read from the store) and memoized selectors

// src/store/selectors.ts
import { createSelector } from '@reduxjs/toolkit';
import type { RootState } from './index';

// Basic selectors (simple reads)
export const selectTheme = (s: RootState) => s.ui.theme;
export const selectModalOpen = (s: RootState) => s.ui.modalOpen;
export const selectFilter = (s: RootState) => s.ui.filter;

// Memoized derived selector (example):
// If you had tasks in Redux, you could filter them by the current UI filter.
// Here we just show the pattern — replace `s.tasks.items` with your real selector.
export const selectVisibleTasks = createSelector(
  [(s: RootState) => (s as any).tasks?.items ?? [], selectFilter],
  (tasks, filter) => {
    const search = filter.search.toLowerCase();
    return tasks.filter((t: any) => {
      const matchesText = t.title.toLowerCase().includes(search);
      const matchesDone = filter.showDone || !t.done;
      return matchesText && matchesDone;
    });
  },
);

Use memoized selectors when computing derived data to avoid unnecessary re-renders.


6) Using Redux in components (with comments)

// src/components/ThemeToggle.tsx
import { useAppDispatch, useAppSelector } from '@/store/hooks';
import { toggleTheme } from '@/store/uiSlice';

export function ThemeToggle() {
  const dispatch = useAppDispatch();
  const theme = useAppSelector((s) => s.ui.theme); // read from store

  return (
    <button
      className="border px-3 py-1 rounded"
      onClick={() => dispatch(toggleTheme())} // send action to store
    >
      Current: {theme} (click to toggle)
    </button>
  );
}
// src/components/FilterBar.tsx
import { useAppDispatch, useAppSelector } from '@/store/hooks';
import { setFilter } from '@/store/uiSlice';

export function FilterBar() {
  const d = useAppDispatch();
  const filter = useAppSelector((s) => s.ui.filter);

  return (
    <div className="flex gap-3 items-center">
      <input
        className="border rounded px-2 py-1"
        placeholder="Search tasks…"
        value={filter.search}
        onChange={(e) => d(setFilter({ search: e.target.value }))}
      />
      <label className="flex items-center gap-2 text-sm">
        <input
          type="checkbox"
          checked={filter.showDone}
          onChange={(e) => d(setFilter({ showDone: e.target.checked }))}
        />
        Show completed
      </label>
    </div>
  );
}
// src/components/Modal.tsx
import { useAppDispatch, useAppSelector } from '@/store/hooks';
import { setModal } from '@/store/uiSlice';

export function Modal() {
  const dispatch = useAppDispatch();
  const open = useAppSelector((s) => s.ui.modalOpen);

  if (!open) return null; // render nothing when closed

  return (
    <div className="fixed inset-0 bg-black/50 flex items-center justify-center">
      <div className="bg-white p-6 rounded shadow-lg">
        <p>This is a global modal controlled by Redux.</p>
        <button className="mt-3" onClick={() => dispatch(setModal(false))}>Close</button>
      </div>
    </div>
  );
}

7) Middleware, DevTools, and good practices

  • DevTools: configureStore enables Redux DevTools in dev by default—inspect actions/state timeline.
  • Middleware: use middleware: (getDefault) => getDefault().concat(myMiddleware) for logging/analytics.
  • Serializable state: keep non‑serializable values out of Redux (e.g., don’t store DOM nodes, class instances, or Promises). Use IDs instead.
  • Normalization: if you store large collections, normalize by ID (RTK has createEntityAdapter for this).
  • Colocation: only move state to Redux when multiple distant components need it. Don’t lift state “just because.”

8) When Redux vs React Query?

  • Use Redux for client/app UI state (theme, layout, filters, wizard steps, drawer open state, selected IDs, temporary UI preferences).
  • Use React Query for server state (data fetched from APIs—caching, refetching, background sync, mutations).
  • It’s normal—and recommended—to use both in one app, each for what it’s best at.

✅ Wrap‑Up

You built a fully typed Redux Toolkit setup:

  • Clear state shape (UIState) + reducers using Immer
  • Slices that auto‑generate action creators
  • A configured store with inferred RootState/AppDispatch
  • Typed hooks (useAppSelector, useAppDispatch)
  • Real world components (ThemeToggle, FilterBar, Modal) wired end‑to‑end

Next up you can extend the pattern with createEntityAdapter for normalized lists or add persistence with redux-persist (optional). If you’re good here, we can move on to Context API & Dependency Injection for lightweight, slice‑like scoping without Redux.


Comments

Popular posts from this blog

🌟 Dot net Microservices interview questions

Here is a comprehensive list of 200 .NET microservices coding questions covering all core microservices concepts and cross-cutting concerns relevant for designing, building, deploying, and maintaining .NET-based distributed systems. 🧩 A. Microservices Fundamentals (20) Build a microservice in .NET 8 that exposes a simple CRUD API. Implement communication between two microservices using REST. How would you design microservices for an e-commerce application? Create a microservice that handles user registration and login. How do you isolate domain logic in a microservice? How to apply the "Single Responsibility Principle" in microservices? Design a service registry/discovery mechanism using custom middleware. Implement a service that handles file uploads and metadata separately. Build a stateless microservice and explain its benefits. Implement health check endpoints in .NET 8. Demonstrate versioning in a microservice API. Add Swagger/OpenAPI support to your m...

⚡ Part 1: Introduction to Generics in C#

🌍 Why Do We Need Generics? Imagine you want to create a stack (like a pile of books 📚): You can push items on top You can pop items off the top If we write a stack for integers : public class IntStack { private int[] items = new int[10]; private int index = 0; public void Push(int item) => items[index++] = item; public int Pop() => items[--index]; } 👉 Problem: This only works for int . What if we want a string stack ? Or a Customer stack ? We’d have to duplicate code for every type. 😢 ✅ Solution: Generics Generics let us create type-safe reusable code without duplication. We can say: “I don’t care what type it is yet — I’ll decide later.” 1) Generic Classes Here’s a generic stack : // Generic class "Stack<T>" // The <T> is a placeholder for any type public class Stack<T> { private T[] items = new T[10]; // Array of type T private int index = 0; // Push adds an item of type T public void P...

🚪 Part 9: API Gateway for .NET 8 Microservices (Ocelot & YARP)

Once you have multiple microservices (Products, Orders, Payments…), exposing each one directly to clients gets messy: Different base URLs Duplicated auth logic No unified rate limiting / caching Hard to evolve routes or aggregate data 👉 Enter the API Gateway — your single front door for all microservices. An API Gateway handles: ✅ Routing & path rewriting ✅ Load balancing, retries, circuit breakers ✅ Authentication & Authorization (JWT, OAuth2) ✅ Rate limiting & caching ✅ Aggregation (compose results from multiple services) In this post we’ll implement two strong options: Ocelot → config-driven, mature, DevOps-friendly YARP (Yet Another Reverse Proxy) → Microsoft’s code-first, extensible gateway ⚖️ Ocelot vs YARP — When to Choose Ocelot → JSON config, minimal C#, built-in QoS (rate limit, circuit breaker). Perfect for teams that like DevOps config-as-code. YARP → full C# control, middleware-friendly, can embed into broader apps (e.g. add dashb...