# Redux from First Principles: Architecture, Evolution, and a React TypeScript Bug Tracker

Redux can look more complicated than it really is. Terms such as *slice*, *reducer*, *dispatch*, *selector*, *Provider*, and *hook* arrive all at once, and the relationship between them is not always obvious.

The easiest way to understand Redux is to answer two questions in order:

1.  What architectural problem was Redux designed to solve?
    
2.  What exactly happens after a user clicks a button?
    

We will answer the first by tracing Redux’s evolution from Flux and relating it carefully to Observer, Mediator, Publish/Subscribe, Command, and state-transition concepts. We will answer the second by following one event through a very small application.

In this article, we will build a bug tracker with exactly three actions:

*   `bugAdded`
    
*   `bugResolved`
    
*   `bugRemoved`
    

The user can report a bug, resolve it, and remove it. The application is deliberately small, but it contains the complete Redux architecture used by much larger applications.

By the end, you will understand not only *how* the code works, but also why each piece exists and which design-pattern comparisons are accurate—and which require qualification.

Here is the journey:

![](https://cdn.hashnode.com/uploads/covers/6a6415e9acac207015b9c8cb/9c718259-d162-4aab-af3e-1d6b69728308.png align="center")

## The Redux Mental Model

Redux is a predictable container for shared application state.

Its core rule is:

> Components do not directly modify shared state. They dispatch actions describing what happened. Reducers calculate the next state, and subscribed components render the result.

The shortest useful formula is:

```text
Event → Dispatch → Reduce → Store → Notify → Select → Render
```

Or mathematically:

```text
nextState = reducer(currentState, action)
```

The overall architecture is unidirectional:

![](https://cdn.hashnode.com/uploads/covers/6a6415e9acac207015b9c8cb/1113c336-0801-4baf-a058-aed87701a0a1.png align="center")

There is no direct path from the component to the state:

```ts
// Do not mutate Redux data in a component.
bugs.push(newBug);
```

Instead, the component reports an event:

```ts
dispatch(bugAdded(description));
```

* * *

## Redux Terminology in Plain English

| Term | Meaning |
| --- | --- |
| State | The application data currently held by Redux |
| Store | The object that owns the Redux state |
| Action | A plain object describing what happened |
| Action creator | A function that creates an action |
| Dispatch | The store function that sends an action through Redux |
| Reducer | A function that determines the next state |
| Case reducer | The handler for one particular action inside a slice |
| Slice | State, actions, and reducer logic for one feature |
| Selector | A function that reads or derives something from state |
| Provider | Makes the Redux store available to the React tree |
| Hook | Connects a function component to a React or library capability |

* * *

## How Redux Evolved

Redux makes more sense when we see it as an answer to the growing complexity of interactive user interfaces rather than as an arbitrary collection of APIs.

![](https://cdn.hashnode.com/uploads/covers/6a6415e9acac207015b9c8cb/4c6fa72c-b1b0-447c-a356-dd3ad347c1cd.png align="center")

### Stage 1: Increasing pressure on MVC-style applications

MVC itself is not the problem. The difficulty appears when a large client application permits shared state to change through many paths:

```text
View A updates a model
→ Model change affects View B
→ Server response changes the model again
→ View C triggers another dependent update
```

As the number of interactions grows, answering “What changed this value?” becomes harder. A predictable direction of travel becomes valuable.

### Stage 2: Flux and unidirectional flow

Flux introduced a disciplined application flow:

```text
Action → Dispatcher → Stores → Views
   ↑                            │
   └────── user interaction ────┘
```

Traditional Flux commonly had:

*   Actions describing events
    
*   A central dispatcher
    
*   Multiple stores containing state and update logic
    
*   Views that read stores and initiate new actions
    

![](https://cdn.hashnode.com/uploads/covers/6a6415e9acac207015b9c8cb/32636253-a008-4d52-8f06-22a9440784c7.png align="center")

The central dispatcher also had a mediator-like quality: views and stores did not need to coordinate through direct references.

### Stage 3: Redux simplifies and tightens Flux

Redux preserved unidirectional flow but changed the mechanics:

| Traditional Flux | Redux |
| --- | --- |
| Usually multiple independent stores | Usually one store and one state tree |
| Separate dispatcher | `store.dispatch()` |
| Stores contain state and update behavior | Store owns state; reducers define transitions |
| Store-specific change emitters | Store-level subscription mechanism |
| Implementations may vary | Small, explicit core contract |

Redux’s conceptual flow is:

```text
Action → store.dispatch() → Root reducer → Next state → Subscribers
```

The state transition became an explicit function:

```ts
const nextState = reducer(currentState, action);
```

This helped make updates traceable, replayable, testable, and friendly to developer tooling.

### Stage 4: Redux Toolkit becomes the standard approach

Early Redux code required substantial manual ceremony: action-type constants, action creators, switch statements, immutable object copies, and store setup. Redux Toolkit standardized common practices and reduced that code with `configureStore()`, `createSlice()`, strong TypeScript inference, and Immer.

### Stage 5: RTK Query addresses server data

Client state and server data have different concerns. Server data involves caching, freshness, request deduplication, loading states, invalidation, and refetching. RTK Query added a purpose-built layer for those concerns while remaining part of the Redux Toolkit ecosystem.

Our Bug Tracker will focus first on client state. That gives us the clearest possible view of Redux’s core mechanics.

* * *

## Why Redux Toolkit?

Modern Redux applications should normally use Redux Toolkit. It provides:

*   `configureStore()` for store configuration
    
*   `createSlice()` for state, action creators, and reducer logic
    
*   Immer-powered immutable updates
    
*   Good TypeScript inference
    
*   Sensible defaults for development checks and middleware
    

Older Redux code often manually defines string constants, action creators, switch statements, and immutable copies. Redux Toolkit keeps the same Redux principles while removing much of that ceremony.

* * *

## Create the Project

Create a Vite React TypeScript application:

```bash
npm create vite@latest redux-bug-tracker -- --template react-ts
cd redux-bug-tracker
npm install
npm install @reduxjs/toolkit react-redux
npm run dev
```

Our source structure is feature-oriented:

```text
src/
├── app/
│   ├── store.ts
│   └── hooks.ts
├── features/
│   └── bugs/
│       ├── bugsSlice.ts
│       ├── bugSelectors.ts
│       ├── BugForm.tsx
│       └── BugList.tsx
├── App.tsx
├── main.tsx
└── styles.css
```

The `app` directory contains application-wide Redux plumbing. The `features/bugs` directory owns everything specific to the bugs feature.

* * *

## Step 1: Model the State with TypeScript

Create `src/features/bugs/bugsSlice.ts` and begin with the domain model:

```ts
export interface Bug {
  id: string;
  description: string;
  resolved: boolean;
}

interface BugsState {
  items: Bug[];
}

const initialState: BugsState = {
  items: [],
};
```

A bug is plain serializable data:

```ts
{
  id: "bug-101",
  description: "Checkout button does nothing",
  resolved: false
}
```

The feature begins with an empty list. Once registered in the store, the root state will look like this:

![](https://cdn.hashnode.com/uploads/covers/6a6415e9acac207015b9c8cb/7b38952d-9b62-4011-9c98-7712a8294d99.png align="center")

## Step 2: Create the Slice and Its Three Actions

Here is the complete slice:

```ts
import {
  createSlice,
  nanoid,
  type PayloadAction,
} from "@reduxjs/toolkit";

export interface Bug {
  id: string;
  description: string;
  resolved: boolean;
}

interface BugsState {
  items: Bug[];
}

const initialState: BugsState = {
  items: [],
};

const bugsSlice = createSlice({
  name: "bugs",
  initialState,

  reducers: {
    bugAdded: {
      reducer: (
        state,
        action: PayloadAction<Bug>,
      ) => {
        state.items.push(action.payload);
      },

      prepare: (description: string) => ({
        payload: {
          id: nanoid(),
          description,
          resolved: false,
        },
      }),
    },

    bugRemoved: (
      state,
      action: PayloadAction<string>,
    ) => {
      state.items = state.items.filter(
        bug => bug.id !== action.payload,
      );
    },

    bugResolved: (
      state,
      action: PayloadAction<string>,
    ) => {
      const bug = state.items.find(
        bug => bug.id === action.payload,
      );

      if (bug) {
        bug.resolved = true;
      }
    },
  },
});

export const {
  bugAdded,
  bugRemoved,
  bugResolved,
} = bugsSlice.actions;

export default bugsSlice.reducer;
```

`createSlice()` generates several related things:

```text
bugsSlice
├── actions
│   ├── bugAdded
│   ├── bugRemoved
│   └── bugResolved
├── caseReducers
│   ├── bugAdded
│   ├── bugRemoved
│   └── bugResolved
└── reducer
    └── complete bugs slice reducer
```

The `name` participates in the generated action types:

| Action creator | Generated action type |
| --- | --- |
| `bugAdded(...)` | `bugs/bugAdded` |
| `bugRemoved(...)` | `bugs/bugRemoved` |
| `bugResolved(...)` | `bugs/bugResolved` |

### The difference between an action creator and an action

This is an action creator call:

```ts
bugResolved("bug-101");
```

It returns an action object resembling:

```ts
{
  type: "bugs/bugResolved",
  payload: "bug-101"
}
```

Creating that object does not update the state. Redux processes it only when it is dispatched:

```ts
dispatch(bugResolved("bug-101"));
```

### Why `bugAdded` uses `prepare`

The UI should need to provide only the description:

```ts
dispatch(bugAdded("Checkout button does nothing"));
```

The slice owns the rules for constructing a new bug:

```ts
prepare: (description: string) => ({
  payload: {
    id: nanoid(),
    description,
    resolved: false,
  },
}),
```

This keeps ID creation and the initial status out of the UI. The case reducer receives a fully constructed, correctly typed `Bug`.

### Why apparent mutation is safe here

These lines look mutable:

```ts
state.items.push(action.payload);
bug.resolved = true;
```

Redux Toolkit uses Immer. Inside a Toolkit reducer, Immer tracks the changes and produces a new immutable state. Conceptually, resolving a bug is equivalent to:

```ts
return {
  ...state,
  items: state.items.map(bug =>
    bug.id === action.payload
      ? { ...bug, resolved: true }
      : bug,
  ),
};
```

The concise mutation-style syntax is allowed inside the reducer. Components must still never mutate selected state.

* * *

## Step 3: Configure the Store

Create `src/app/store.ts`:

```ts
import { configureStore } from "@reduxjs/toolkit";
import bugsReducer from "../features/bugs/bugsSlice";

export const store = configureStore({
  reducer: {
    bugs: bugsReducer,
  },
});

export type RootState =
  ReturnType<typeof store.getState>;

export type AppDispatch =
  typeof store.dispatch;
```

Read this configuration as:

> Create a Redux store with a `bugs` state section, managed by `bugsReducer`.

The two sides of this line have different jobs:

```ts
bugs: bugsReducer
```

| Expression | Meaning |
| --- | --- |
| Left-side `bugs` | Property name in the root state |
| Right-side `bugsReducer` | Function that manages that property |

That is why a selector later reads:

```ts
state.bugs.items
```

### Where was `bugsReducer` defined?

The slice file ends with:

```ts
export default bugsSlice.reducer;
```

The store imports that default export and chooses a local name:

```ts
import bugsReducer from "../features/bugs/bugsSlice";
```

A default import can choose its local name. These would all import the same value:

```ts
import bugsReducer from "../features/bugs/bugsSlice";
import reducer from "../features/bugs/bugsSlice";
import bugStateUpdater from "../features/bugs/bugsSlice";
```

`bugsReducer` is simply the clearest name.

### The root reducer

`configureStore()` combines registered slice reducers into a root reducer. Conceptually, it creates something like:

```ts
function rootReducer(currentState, action) {
  return {
    bugs: bugsReducer(
      currentState?.bugs,
      action,
    ),
  };
}
```

During initialization, Redux supplies `undefined` as the previous slice state. The reducer responds with `initialState`, producing:

```ts
{
  bugs: {
    items: []
  }
}
```

The types are derived from the actual store rather than manually duplicated:

```ts
export type RootState =
  ReturnType<typeof store.getState>;

export type AppDispatch =
  typeof store.dispatch;
```

If another reducer is added later, `RootState` updates automatically.

* * *

## Step 4: Create Typed Hooks

Create `src/app/hooks.ts`:

```ts
import {
  useDispatch,
  useSelector,
} from "react-redux";

import type {
  AppDispatch,
  RootState,
} from "./store";

export const useAppDispatch =
  useDispatch.withTypes<AppDispatch>();

export const useAppSelector =
  useSelector.withTypes<RootState>();
```

A hook is a function that connects a React function component to a React or library-managed capability.

In this application:

```text
useState()        → local component state
useAppDispatch()  → Redux store dispatch
useAppSelector()  → Redux state selection and subscription
```

The custom hooks add our application-specific TypeScript types to React Redux’s hooks. As a result, TypeScript understands the store shape and accepted actions throughout the UI.

* * *

## Step 5: Provide the Store to React

In `src/main.tsx`:

```tsx
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { Provider } from "react-redux";
import App from "./App";
import { store } from "./app/store";
import "./styles.css";

createRoot(
  document.getElementById("root")!,
).render(
  <StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </StrictMode>,
);
```

![](https://cdn.hashnode.com/uploads/covers/6a6415e9acac207015b9c8cb/8e283d34-f0ed-4676-8c01-eaa0f68f46b8.png align="center")

The `Provider` places the store into React Context so descendants can access it:

This is DI-like context-based dependency provision:

*   The store is created outside the components.
    
*   The `Provider` supplies it to the component tree.
    
*   Hooks consume capabilities from it.
    
*   Components do not construct or directly import the store instance.
    

It is not traditional constructor injection, but it applies the same central principle: dependencies are supplied from outside the consumer.

* * *

## Step 6: Dispatch `bugAdded` from the Form

Create `src/features/bugs/BugForm.tsx`:

```tsx
import {
  useState,
  type FormEvent,
} from "react";

import { useAppDispatch } from "../../app/hooks";
import { bugAdded } from "./bugsSlice";

export default function BugForm() {
  const [description, setDescription] =
    useState("");

  const dispatch = useAppDispatch();

  const reportBugHandler = (
    event: FormEvent<HTMLFormElement>,
  ) => {
    event.preventDefault();

    const trimmedDescription =
      description.trim();

    if (!trimmedDescription) return;

    dispatch(bugAdded(trimmedDescription));
    setDescription("");
  };

  return (
    <form onSubmit={reportBugHandler}>
      <label htmlFor="description">
        Bug description
      </label>

      <input
        id="description"
        value={description}
        onChange={event =>
          setDescription(event.target.value)
        }
        placeholder="Checkout button does nothing"
      />

      <button type="submit">
        Report bug
      </button>
    </form>
  );
}
```

The text currently being typed belongs only to `BugForm`, so local state is appropriate:

```ts
const [description, setDescription] = useState("");
```

The submitted bug is shared application state, so it enters Redux:

```ts
dispatch(bugAdded(trimmedDescription));
```

This line contains two calls. Expanded, it is:

```ts
const action = bugAdded(trimmedDescription);
dispatch(action);
```

First, `bugAdded()` creates the action. Then `dispatch()` sends that action to the store.

The `dispatch` variable is obtained here:

```ts
const dispatch = useAppDispatch();
```

It is, in practical terms, the store’s dispatch function accessed through React Redux:

```ts
// Conceptual equivalent
const dispatch = store.dispatch;
```

Using the hook is preferable inside a component because it obtains the store supplied by `Provider`, keeps the component less tightly coupled, and remains easy to test.

* * *

## Step 7: Create Selectors

Create `src/features/bugs/bugSelectors.ts`:

```ts
import type { RootState } from "../../app/store";

export const selectAllBugs = (
  state: RootState,
) => state.bugs.items;

export const selectOpenBugCount = (
  state: RootState,
) =>
  state.bugs.items.filter(
    bug => !bug.resolved,
  ).length;
```

A selector is a query over Redux state:

```text
RootState → selected or derived value
```

`selectAllBugs` returns stored state. `selectOpenBugCount` derives a number from stored state.

We intentionally do not store the open count separately:

```ts
// Avoid unnecessary duplicated state.
interface BugsState {
  items: Bug[];
  openBugCount: number;
}
```

If the count can always be calculated from `items`, deriving it avoids synchronization bugs.

* * *

## Step 8: Subscribe, Render, Resolve, and Remove

Create `src/features/bugs/BugList.tsx`:

```tsx
import {
  useAppDispatch,
  useAppSelector,
} from "../../app/hooks";

import {
  selectAllBugs,
  selectOpenBugCount,
} from "./bugSelectors";

import {
  bugRemoved,
  bugResolved,
} from "./bugsSlice";

export default function BugList() {
  const bugs =
    useAppSelector(selectAllBugs);

  const openBugCount =
    useAppSelector(selectOpenBugCount);

  const dispatch = useAppDispatch();

  return (
    <section>
      <h2>Reported bugs</h2>
      <span>{openBugCount} open</span>

      {bugs.length === 0 ? (
        <p>No bugs reported yet.</p>
      ) : (
        <ul>
          {bugs.map(bug => (
            <li key={bug.id}>
              <strong>{bug.description}</strong>

              <small>
                {bug.resolved
                  ? "Resolved"
                  : "Open"}
              </small>

              {!bug.resolved && (
                <button
                  onClick={() =>
                    dispatch(
                      bugResolved(bug.id),
                    )
                  }
                >
                  Resolve
                </button>
              )}

              <button
                onClick={() =>
                  dispatch(
                    bugRemoved(bug.id),
                  )
                }
              >
                Remove
              </button>
            </li>
          ))}
        </ul>
      )}
    </section>
  );
}
```

This line is where `BugList` reads and effectively subscribes to the selected bug array:

```ts
const bugs = useAppSelector(selectAllBugs);
```

It means:

> Run `selectAllBugs` against the current store, return its result, and rerender this component when that selected result changes.

The component does not subscribe to `bugAdded`, `bugResolved`, or any reducer. It subscribes to the **selected state value**.

Conceptually, React Redux performs behavior similar to:

```ts
let previousSelection =
  selectAllBugs(store.getState());

store.subscribe(() => {
  const nextSelection =
    selectAllBugs(store.getState());

  if (nextSelection !== previousSelection) {
    previousSelection = nextSelection;
    rerenderBugList();
  }
});
```

The actual implementation is more sophisticated and manages React’s lifecycle correctly, but this is the essential idea.

* * *

## Step 9: Assemble the UI

In `src/App.tsx`:

```tsx
import BugForm from "./features/bugs/BugForm";
import BugList from "./features/bugs/BugList";

export default function App() {
  return (
    <main>
      <header>
        <p>React + TypeScript + Redux Toolkit</p>
        <h1>Bug Tracker</h1>
      </header>

      <BugForm />
      <BugList />
    </main>
  );
}
```

`App` does not pass bugs or event handlers through props. `BugForm` dispatches actions, while `BugList` selects the state it needs.

* * *

## Trace One Click End to End

Assume the user clicks **Resolve** for `bug-101`.

![](https://cdn.hashnode.com/uploads/covers/6a6415e9acac207015b9c8cb/d52268c9-d79b-4849-996d-b1131cd79fa8.png align="center")

### 1\. The click handler runs

```tsx
onClick={() =>
  dispatch(bugResolved(bug.id))
}
```

### 2\. The action creator creates an action

```ts
bugResolved("bug-101");
```

produces:

```ts
{
  type: "bugs/bugResolved",
  payload: "bug-101"
}
```

### 3\. The store dispatches the action

```ts
dispatch(action);
```

### 4\. Redux calls the reducer tree

Conceptually:

```ts
const nextState = rootReducer(
  currentState,
  action,
);
```

### 5\. The matching case reducer runs

```ts
const bug = state.items.find(
  bug => bug.id === action.payload,
);

if (bug) {
  bug.resolved = true;
}
```

### 6\. The reducer returns the next state

Before:

```ts
{
  id: "bug-101",
  description: "Checkout button does nothing",
  resolved: false
}
```

After:

```ts
{
  id: "bug-101",
  description: "Checkout button does nothing",
  resolved: true
}
```

### 7\. The store saves the next state

The store replaces its current state reference with the state returned by the reducer tree.

### 8\. The store notifies subscribers

React Redux is listening for store updates and is notified after the dispatch completes.

### 9\. React Redux runs selectors again

```ts
selectAllBugs(store.getState());
selectOpenBugCount(store.getState());
```

### 10\. React Redux compares the results

The bug array has a new reference and the open count has changed. React Redux schedules `BugList` to rerender.

### 11\. The user sees the new UI

The status changes to **Resolved**, the Resolve button disappears, and the open-bug count decreases.

* * *

## Is There One Reducer per Action?

The answer depends on what we mean by *reducer*.

Inside our slice, there is normally one **case reducer** for each action defined in `reducers`:

| Action type | Case reducer |
| --- | --- |
| `bugs/bugAdded` | `bugAdded` handler |
| `bugs/bugResolved` | `bugResolved` handler |
| `bugs/bugRemoved` | `bugRemoved` handler |

Redux Toolkit combines those handlers into one **slice reducer**:

```text
bugsReducer
├── handles bugs/bugAdded
├── handles bugs/bugResolved
└── handles bugs/bugRemoved
```

At the store level, an action is sent through the entire reducer tree, not routed exclusively to one function:

![](https://cdn.hashnode.com/uploads/covers/6a6415e9acac207015b9c8cb/e1bcaf2e-6ccd-486d-b27e-6f1b6b01e2de.png align="center")

Every registered slice reducer receives the action. A reducer that does not recognize it returns its existing state unchanged.

One action may also affect multiple slices. For example, `bugAdded` could update the bug list and create an audit entry. Another slice can respond to an externally defined action using `extraReducers`:

```ts
const auditSlice = createSlice({
  name: "audit",
  initialState: [] as string[],
  reducers: {},

  extraReducers: builder => {
    builder.addCase(
      bugAdded,
      (state, action) => {
        state.push(
          `Added bug ${action.payload.id}`,
        );
      },
    );
  },
});
```

Therefore, the accurate model is:

*   One slice reducer handles many action types.
    
*   One case reducer usually handles one action type within that slice.
    
*   One action may be handled by multiple slice reducers.
    
*   Every registered slice reducer sees every dispatched action.
    

* * *

## The Architectural Pattern Map

Now that we have built the application and traced a click, we can interpret the architecture precisely. Redux is not a textbook implementation of one GoF pattern. It combines several ideas, and each one explains a different part of the runtime flow.

![](https://cdn.hashnode.com/uploads/covers/6a6415e9acac207015b9c8cb/3399cbd2-73d8-46ab-bdf7-8d1eeca984ca.png align="center")

The most useful separation is:

```text
Input side:
Command + mediator-like dispatch + pub/sub-like action broadcast

Core:
Reducer-driven state transition

Output side:
Observer notification + selector-based rendering
```

### Redux and Flux

Flux is an architectural pattern for unidirectional data flow. Traditional Flux commonly uses actions, a dispatcher, multiple stores, and views.

Redux is Flux-inspired, but normally uses:

*   One store
    
*   One state tree
    
*   No separate dispatcher object
    
*   Reducer functions for state transitions
    
*   Selectors for state queries
    

```text
Traditional Flux:
Action → Dispatcher → Stores → Views

Redux:
Action → store.dispatch → Reducer tree → Store → Views
```

The `store.dispatch()` method performs the dispatching role, so Redux does not need a separate dispatcher object.

### The Mediator connection

The Mediator pattern reduces direct communication between collaborating objects by routing coordination through a central object.

Without a mediator-like mechanism, components might know and call one another:

```text
BugForm → BugList
BugForm → AuditPanel
BugForm → NotificationPanel
```

That produces coupling. In Redux, the form knows only how to dispatch an action:

```ts
dispatch(bugAdded(description));
```

It does not know which components display bugs or which additional reducers and middleware may respond.

![](https://cdn.hashnode.com/uploads/covers/6a6415e9acac207015b9c8cb/58f724ae-672d-4d39-a3ac-8e0c2885555c.png align="center")

This gives `store.dispatch()` and the Redux pipeline a **mediator-like role**: components coordinate through a central mechanism rather than through direct references.

The qualification matters. A classic GoF Mediator often contains imperative coordination logic and directly instructs colleague objects:

```ts
class DialogMediator {
  notify(sender: Component, event: string) {
    if (event === "bugAdded") {
      this.bugList.refresh();
      this.auditPanel.record();
    }
  }
}
```

Redux normally does not command UI objects. It dispatches actions, reducers derive state, and components independently select what they need. Therefore, Redux offers mediator-like decoupling without being a textbook Mediator implementation.

### The Publish/Subscribe connection

In Publish/Subscribe, a publisher emits a message without knowing its consumers. An intermediary distributes the message to interested subscribers.

Our publisher is `BugForm`:

```ts
dispatch(bugAdded(description));
```

The component does not know that the action might be processed by:

*   `bugsReducer`
    
*   A future `auditReducer`
    
*   A future `notificationsReducer`
    
*   Logging or analytics middleware
    

The dispatched action travels through middleware and the reducer tree. Multiple parts of the application may respond to the same action.

For example, an audit slice can react to an action defined by the bugs slice:

```ts
const auditSlice = createSlice({
  name: "audit",
  initialState: [] as AuditEntry[],
  reducers: {},

  extraReducers: builder => {
    builder.addCase(
      bugAdded,
      (state, action) => {
        state.push({
          event: "BUG_ADDED",
          bugId: action.payload.id,
        });
      },
    );
  },
});
```

One published action can now influence several state slices:

![](https://cdn.hashnode.com/uploads/covers/6a6415e9acac207015b9c8cb/07623cc0-427e-4c80-9c54-fbec8948a7bb.png align="center")

This is **Publish/Subscribe-like broadcast behavior**, but Redux is not a distributed message broker. Redux is normally in-process, synchronous at its reducer core, non-durable by default, and local to one application runtime. It does not provide the topic retention, delivery guarantees, consumer offsets, or cross-process communication associated with Kafka or RabbitMQ.

There is also an important distinction between the two sides of Redux:

> Actions have pub/sub-like broadcast semantics, while store updates use Observer-style notification semantics.

### The Observer aspect

Redux directly supports subscription:

```ts
const unsubscribe = store.subscribe(() => {
  console.log(store.getState());
});

unsubscribe();
```

React Redux builds a smarter React integration on top of store subscriptions:

```ts
const bugs = useAppSelector(selectAllBugs);
```

| Observer concept | Redux/React Redux equivalent |
| --- | --- |
| Subject | Redux store |
| Observer | React Redux subscription/component |
| Subscribe | `useSelector()` infrastructure |
| Notify | Store notification after dispatch |
| Selected observation | Selector result |
| View update | Component rerender |

Observer explains how consumers learn that state may have changed.

`useAppSelector(selectAllBugs)` does **not** subscribe to the `bugAdded` action. It subscribes the component to store updates and tracks the value returned by `selectAllBugs`. After any action is processed, React Redux may rerun the selector; the component rerenders only when its selected result changes.

### Observer versus Publish/Subscribe

The terms are related, but they should not be treated as synonyms:

| Observer | Publish/Subscribe |
| --- | --- |
| An observer subscribes to a subject | A subscriber receives messages through an intermediary |
| The subject maintains subscriptions | Publishers do not know the consumers |
| Notification often means “state changed” | A message usually describes a particular event |
| Store-to-React notification resembles this | Action-to-consumers broadcast resembles this |

In our application:

```text
BugForm publishes: bugs/bugAdded
→ Redux processes the action
→ Store state changes
→ Store notifies observers
→ React Redux reruns selectors
→ BugList rerenders if its selection changed
```

### The State-pattern connection

Redux manages explicit states and transitions:

```text
Open bug --bugResolved--> Resolved bug
Bug exists --bugRemoved--> Bug absent
```

However, Redux is not normally a textbook GoF State pattern. The GoF pattern often encapsulates behavior in polymorphic state objects. Redux represents state as plain data and places transition logic in reducer functions.

It is more precise to say:

> Redux uses Observer-style notification around a reducer-driven state-transition system.

### The Command/message connection

An action resembles a command or event message:

```ts
{
  type: "bugs/bugResolved",
  payload: "bug-101"
}
```

The component describes *what happened*. The reducer decides *how state changes*.

### Middleware as a pipeline

Middleware can intercept dispatched actions before they reach reducers. It is useful for logging, asynchronous workflows, analytics, and other cross-cutting behavior.

Conceptually:

```text
dispatch
  → middleware A
  → middleware B
  → reducer tree
  → next state
```

Our small application needs no custom middleware, but `configureStore()` creates the middleware pipeline for us.

### Provider and dependency injection

This arrangement is DI-like:

```tsx
<Provider store={store}>
  <App />
</Provider>
```

The store is the dependency. `Provider` supplies it through React Context. `useAppDispatch()` and `useAppSelector()` consume capabilities from it.

This is better described as context-based dependency provision than traditional constructor injection, but the essential inversion is present: components use a dependency created elsewhere instead of constructing it themselves.

### The complete architectural map

| Redux element | Pattern or architectural concept |
| --- | --- |
| One-way application flow | Flux |
| Action object | Command or event message |
| `store.dispatch()` | Dispatcher and mediator-like coordination |
| Action reaching middleware and reducer tree | Publish/Subscribe-like broadcast |
| Middleware pipeline | Chain of Responsibility |
| Reducer | Deterministic state-transition function |
| Bug status changes | State-machine semantics |
| Store subscriptions | Observer |
| Selector | Query/projection |
| `Provider` | Context-based dependency provision |
| Immer updates | Immutable/functional state management |

No single row defines Redux completely. Together they explain why Redux provides both decoupling and predictability.

* * *

## Redux Compared with C# and MVVM

For developers coming from .NET, the following mapping is useful but approximate:

| React/Redux | C# or MVVM analogy |
| --- | --- |
| Store | Application state container |
| Slice | Feature-oriented state service |
| Action | Command/event message |
| Payload | Command parameters |
| Reducer | Deterministic state-transition handler |
| Selector | Read-only query/projection |
| `useSelector()` | Observable state subscription/binding |
| Store notification | `INotifyPropertyChanged`\-like notification |
| `Provider` | Composition root/context-provided dependency |

An Rx.NET-style approximation of a selector subscription might look like:

```csharp
stateObservable
    .Select(state => state.Bugs)
    .DistinctUntilChanged()
    .Subscribe(bugs => Render(bugs));
```

The React Redux equivalent is:

```ts
const bugs = useAppSelector(selectAllBugs);
```

Redux itself is not an Rx stream, but the comparison helps explain selection, change detection, and subscription.

* * *

## Local State or Redux State?

Not every value belongs in Redux.

Our application makes the distinction visible:

```ts
// Temporary value needed only by BugForm
const [description, setDescription] = useState("");
```

```ts
// Shared application data
dispatch(bugAdded(description));
```

Use the smallest appropriate owner:

| Requirement | Suitable choice |
| --- | --- |
| One component needs a simple value | `useState` |
| One component has complex local transitions | `useReducer` |
| A small value is needed through a subtree | React Context |
| Multiple areas share structured client state | Redux Toolkit |
| Backend data needs fetching and caching | RTK Query |

Examples normally kept local include input text, hover state, and a modal used by one component. Examples that may belong in Redux include the authenticated user, cart, shared filters, multi-page workflows, and application-wide preferences.

* * *

## Common Mistakes

### 1\. Mutating selected state in a component

Incorrect:

```ts
const bugs = useAppSelector(selectAllBugs);
bugs.push(newBug);
```

Correct:

```ts
dispatch(bugAdded(description));
```

### 2\. Calling a selector before passing it to the hook

Incorrect:

```ts
useAppSelector(selectAllBugs());
```

Correct:

```ts
useAppSelector(selectAllBugs);
```

We pass the selector function. React Redux calls it with `store.getState()`.

### 3\. Calling hooks conditionally

Incorrect:

```tsx
if (enabled) {
  const bugs = useAppSelector(selectAllBugs);
}
```

Hooks must be called at the component’s top level and in the same order on every render.

### 4\. Storing derivable values

Avoid storing `openBugCount` when it can be calculated from `items`.

### 5\. Importing the store directly into every component

Technically possible:

```ts
import { store } from "../../app/store";
store.dispatch(bugAdded(description));
```

Prefer the hook:

```ts
const dispatch = useAppDispatch();
dispatch(bugAdded(description));
```

### 6\. Assuming the store persists automatically

Redux is an in-memory state container. Refreshing this application clears the bugs. Persistence requires an intentional addition such as local storage or a backend API.

### 7\. Putting non-serializable objects in state

Prefer plain data. Avoid storing DOM elements, promises, functions, class instances, and similar values in Redux state.

* * *

## How the Three Actions Change State

| Action | Payload | Transition |
| --- | --- | --- |
| `bugAdded` | Description string supplied to the prepared action creator | Adds a new unresolved bug |
| `bugResolved` | Bug ID | Changes the matching bug to resolved |
| `bugRemoved` | Bug ID | Removes the matching bug |

![](https://cdn.hashnode.com/uploads/covers/6a6415e9acac207015b9c8cb/7a18f40b-04cb-45d4-94cf-65982d00c0dd.png align="center")

This visual is a domain-level state model. Internally, `bugRemoved` deletes the item from the array rather than storing an explicit `Removed` status.

* * *

## Testing the Mental Model Manually

Run the application and follow these steps:

1.  Open Redux DevTools if available.
    
2.  Enter `Checkout button does nothing`.
    
3.  Click **Report bug**.
    
4.  Find the `bugs/bugAdded` action.
    
5.  Inspect its generated `id`, description, and `resolved: false` payload.
    
6.  Click **Resolve**.
    
7.  Find the `bugs/bugResolved` action and its ID payload.
    
8.  Confirm that the selected bug now has `resolved: true`.
    
9.  Click **Remove**.
    
10.  Find the `bugs/bugRemoved` action and confirm the item disappears.
     

Each visible UI change should be traceable to an action and a deterministic state transition.

* * *

## How This Application Can Grow

Once the basic flow is comfortable, useful next steps are:

1.  Add severity and category fields.
    
2.  Add filters for open and resolved bugs.
    
3.  Add an action to reopen a bug.
    
4.  Add memoized selectors.
    
5.  Add unit tests for reducers and selectors.
    
6.  Add an audit slice that responds to bug actions.
    
7.  Persist bugs to local storage.
    
8.  Replace local persistence with an API.
    
9.  Use RTK Query for server-side bug data.
    
10.  Add async loading, success, and failure states.
     

The architecture remains the same even as the application grows:

```text
Component dispatches an action
→ reducer calculates state
→ store publishes an update
→ selectors project state
→ components rerender
```

* * *

## Final Takeaway

The Redux framework becomes much less mysterious when each responsibility is kept separate:

*   **Actions describe events.**
    
*   **Dispatch sends actions to the store.**
    
*   **Reducers calculate the next state.**
    
*   **The store owns the state.**
    
*   **Selectors read or derive values.**
    
*   **React Redux subscribes components to selections.**
    
*   **Provider supplies the store to the component tree.**
    
*   **Hooks give components access to these capabilities.**
    

Our complete pattern can be reduced to three central lines:

```ts
// Describe and dispatch an event
dispatch(bugAdded(description));
```

```ts
// Define the state transition
state.items.push(action.payload);
```

```ts
// Read and subscribe to selected state
const bugs = useAppSelector(selectAllBugs);
```

Redux is not merely Observer, Mediator, Publish/Subscribe, State, Command, Flux, or dependency injection. It combines ideas from all of them into a predictable unidirectional state-management architecture.

The most accurate concise description is:

> Redux is a Flux-inspired state container. It uses command-like actions, mediator-like dispatch, pub/sub-like action distribution, reducer-driven state transitions, and Observer-style **subject**/store subscriptions to update consumers predictably.

Once you can trace `bugAdded`, `bugResolved`, and `bugRemoved` from button click to rerender, you understand the foundation of Redux.
