# Complex to Simple: Redux and Flux architecture for beginners

*Dmytro Bobryshev · 10 mar 2025*

> As applications grow, managing state becomes increasingly complex. Without a clear structure, developers can face uncontrolled data flow, difficult debugging, unpredictable behavior, and performance issues. Scattered state across the application makes it hard to track changes and maintain consistency. In this post, we explore state management approaches such as Context, Flux, Redux, and Redux Toolkit (RTK), and how they help create a more scalable and maintainable architecture.

---

## Problematics

Let’s imagine our application as a tree-like structure, where blocks represent components, and elements inside them represent state. We create components to separate logic, UI, and other concerns. As a result, each component has its own logic and state.

![problematics](https://cdn.sanity.io/images/z7im7se7/production/c8185b7e4c1c40d597367e8631f1e12e7c63a378-700x361.png)

For small applications, this approach works well. Components encapsulate their functionality, and their state remains isolated. However, as the application grows, the need for data sharing between different components — sometimes located in distant branches of the tree — becomes inevitable.

![problematics_2](https://cdn.sanity.io/images/z7im7se7/production/ca0358041f0f3de9d1eea50efeff248e537bf40c-700x399.png)

Press enter or click to view image in full size

In React, for example, data is typically passed from parent components to child components using props. But when we need to share data between components that are far apart in the tree, we run into the problem of “prop drilling” — having to pass props through multiple intermediate components that don’t actually need them.

![props_drilling](https://cdn.sanity.io/images/z7im7se7/production/c2fe9fc74b1934ce87019f7272b1a03c118dd121-700x361.png)

State updates make things even more complicated. If **Component A** needs to react to changes in **Component B**, which is located in a completely different part of the tree, managing such interactions becomes a non-trivial task without a centralized state management system.

This is exactly the problem that state management patterns like Flux and its popular implementation, Redux, aim to solve — providing a single, predictable data flow in the application.

## Context in React

Before we dive into how **Redux** works and what **Flux architecture** is, we first need to understand **React Context**.

**React Context** — is a mechanism that allows us to pass data through the component tree without manually passing props at every level. Context creates a kind of “global” state (or wrapper) for a specific part of the application.

With the Context API, we can create a data store that is accessible to all components in the tree, regardless of their position or nesting. This solves the problem of “prop drilling,” where props have to be passed through multiple intermediate components.

![global_state](https://cdn.sanity.io/images/z7im7se7/production/b509e634eec1c3e7c3252a0b0d15e8c7c7253ffd-700x463.png)

However, while **Context** is great for providing access to data, it has limitations when it comes to managing state changes:

- Context does not provide a standardized way to modify state.
- Updating context can trigger unnecessary re-renders of components.
- As the application grows, it becomes harder to track where and how state changes happen, especially since we can create multiple contexts and wrap different parts of the project.

![context](https://cdn.sanity.io/images/z7im7se7/production/0cc8e11ca7990da07a12474a681f06305e1e8f95-700x463.png)

## Flux Architecture

**Flux** — is an architectural pattern developed by [Facebook](https://www.facebook.com/) for working with [React](https://react.dev/). It ensures a one-way data flow, making the application state more predictable and easier to track.

Unlike the familiar Context-based flow, Flux introduces a more structured approach with additional elements:

- **Action**: An object that contains the type of action and the necessary data for modifying the state. Actions describe *what* happened but not *how* the state will change.
- **Dispatcher**: A central hub that distributes actions to the appropriate stores.
- **Store**: Holds the application state and the logic for modifying it. Unlike Context, a store doesn’t just store data — it also defines how it can be changed.
- **View**: Displays data from the **Store** and dispatches **Actions** when the user interacts with the interface.

![flux_way](https://cdn.sanity.io/images/z7im7se7/production/593387c125d1a9b0296d7d44c0e51fc6ae0a8d99-700x77.png)

### **How Data Updates Work:**

1. The user interacts with the **View** (e.g., clicks a button).
1. The View creates an Action and sends it to the Dispatcher.
1. The **Dispatcher** forwards the **Action** to all registered **Stores**.
1. The **Store** updates its state based on the **Action** received from the **Dispatcher**.
1. The **Store** notifies the **View** about the state change.
1. The **View** retrieves the updated data from the **Store** and re-renders.

![flux_way_2](https://cdn.sanity.io/images/z7im7se7/production/7c6e45b48b09220d8384057202301852ec2cd50e-700x197.png)

A logical question arises: what if we have multiple **Views**, and we want to share data from “**View #1”** with “**View #2” **? The answer is simple — we must go through the full cycle of the one-way data flow.

In other words, direct data transfer from **View #1** to **View #2** is *not allowed* and will *not* be used!

![wrong_flux](https://cdn.sanity.io/images/z7im7se7/production/35bade09089553d7bf18ef12716a7e1fef63756e-700x225.png)

## Redux

Redux is a state management library that implements the Flux architecture.

### Key Features of Redux:

- **Single Store** — The entire application state is stored in a single object.
- **Read-Only State** — State cannot be modified directly.
- **State Changes via Pure Functions (Reducers)** — Updates happen through reducers, which are pure functions that take the previous state and an action, then return a new state. Since applications can have multiple states, we can have multiple reducers. A simple analogy: **Reducers** work similarly to `useState()`, where a setter updates the previous state.
- **One-Way Data Flow** — Changes always follow a predictable pattern.

### Practical Example: Creating a Redux Application

Let’s go step by step and build a simple application with multiple states.

1. **Setting Up the Project**

```shell
$ pnpm create vite my-redux-app --template react

$ pnpm create vite
```

2. **Organizing Files**

A good practice is to place Redux-related code in a separate folder:

![orginize_files](https://cdn.sanity.io/images/z7im7se7/production/c629c74b646a05aa4f237768fec571515dc5133f-295x480.png)

3. **Creating the Store**

The **Store** is the heart of Redux — it holds all the application states.

```typescript
import { combineReducers, createStore } from "redux"
import { counterReducer } from "./features/counter/reducer"

/**
 * Root reducer
 * Combines all application reducers into a single state tree.
 */
const rootReducer = combineReducers({
  /**
   * Counter state slice
   */
  counter: counterReducer,

  /**
   * Theme state slice
   * Temporary static reducer (can be replaced with real reducer later)
   */
  theme: () => "light",
})

/**
 * Redux store
 * Centralized state container for the application.
 */
export const store = createStore(rootReducer)

/**
 * RootState type
 * Represents the complete state shape of the Redux store.
 */
export type RootState = ReturnType<typeof rootReducer>
```

> *Unlike React Context, where you can create multiple separate contexts for different parts of the state, Redux uses a ****single store**** for all states.*

**4. Connecting Redux to the Application**

```tsx


import React from 'react'
import { createRoot } from 'react-dom/client'
import { Provider } from 'react-redux'
import { store } from './redux/store'
import App from './App'

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

Just like with React Context, we need to wrap our application in a **Provider** and pass the store we created earlier as a prop. Key concepts from the `store`:

![browser_store](https://cdn.sanity.io/images/z7im7se7/production/9b16a37d067dc084b707f252ec4266f4d66729b5-700x96.png)

Press enter or click to view image in full size

- **observable** — An object that watches for state changes and notifies other parts of the app when updates occur.
- **dispatch** — A function that triggers reducers.
- **getState** — To get data from the store, we use the `getState` function, which might return something like `{ counter: 1, theme: "light" }`.

A **Reducer** is a pure function that takes the current state and an action, then returns a new state.

```typescript


/**
 * Define the action types (using enums as well)
 */
export enum CounterActionType {
  INCREMENT = "INCREMENT",
  DECREMENT = "DECREMENT",
}

/**
 * Types for actions
 */
export interface IncrementAction {
  type: CounterActionType.INCREMENT
}

export interface DecrementAction {
  type: CounterActionType.DECREMENT
}

export type CounterAction = IncrementAction | DecrementAction

/**
 * Action Creators
 */
export function incrementCounter(): IncrementAction {
  return { type: CounterActionType.INCREMENT }
}

export function decrementCounter(): DecrementAction {
  return { type: CounterActionType.DECREMENT }
}
```

> ***Important:**** A reducer must always return a new state and should never modify the existing one. A reducer must have a ****default value**** for the ****initial state****. Otherwise, we may encounter an error like:*

```shell
Uncaught Error: The slice reducer for key "counter" returned undefined during initialization.
If the state passed to the reducer is undefined, you must explicitly return the initial state.
The initial state may not be undefined. If you don't want to set a value for this reducer, you can use null instead of undefined.
```

Finally, don’t forget to import the reducer into our **store**.

```typescript
import { combineReducers, createStore } from "redux"
import { counterReducer } from "./features/counter/reducer"

const rootReducer = combineReducers({
  counter: counterReducer, // replace instead of `() => 1`
  theme: () => 'light'

export const store = createStore(rootReducer)

export type RootState = ReturnType<typeof rootReducer>
```

**6. Creating Selectors**

We will create two components:

- `DisplayCounter` — Displays the current counter value.
- `CounterControls` — Contains buttons to increase or decrease the counter.

First, we define the **counterSelector** function and pass it into the `useSelector(counterSelector)` hook. 

**Selectors** are functions that extract specific data from the state.

```typescript
import { RootState } from "../../store"

// Selector to get the counter value
export const counterSelector = (state: RootState) => state.counter
```

```tsx
import { useSelector } from "react-redux"
import { counterSelector } from "../redux/features/counter/selectors"

export const DisplayCounter = () => {
  /**
   * useSelector subscribes to changes in the store
   * and returns the right part of the state
   */
  const counter = useSelector(counterSelector)

  return <div>Counter value: {counter}</div>
}
```

**7. Defining Actions**

In the **CounterControls** component, we update the state using the `useDispatch(incrementCounter())` hook. 

**Actions** are simple objects with a `type` field that describes what happened in the application. For example, our action types will be:

- `"INCREMENT"`
- `"DECREMENT"`

```typescript
/**
 * Define the action types (using enums as well)
 */
export enum CounterActionType {
  INCREMENT = "INCREMENT",
  DECREMENT = "DECREMENT",
}

/**
 * Types for actions
 */
export interface IncrementAction {
  type: CounterActionType.INCREMENT
}

export interface DecrementAction {
  type: CounterActionType.DECREMENT
}

export type CounterAction = IncrementAction | DecrementAction

/**
 * Action Creators
 */
export function incrementCounter(): IncrementAction {
  return { type: CounterActionType.INCREMENT }
}

export function decrementCounter(): DecrementAction {
  return { type: CounterActionType.DECREMENT }
}
```

```tsx
// src/components/CounterControls.tsx

import { useDispatch } from "react-redux"
import {
  decrementCounter,
  incrementCounter,
} from "../redux/features/counter/actions"

export const CounterControls = () => {
  /**
   * useDispatch returns the dispatch function to send actions to the store
   */
  const dispatch = useDispatch()

  /**
   * Create actions using action creators
   */
  const increment = incrementCounter()
  const decrement = decrementCounter()

  /**
   * Handler functions for buttons
   */
  const onIncrement = () => dispatch(increment)
  const onDecrement = () => dispatch(decrement)

  return (
    <div className="flex items-center space-x-2">
      <button
        className="px-5 py-2 border rounded-lg"
        onClick={onIncrement}
      >
        +1
      </button>
      <button
        className="px-5 py-2 border rounded-lg"
        onClick={onDecrement}
      >
        -1
      </button>
    </div>
  )
}
```

**8. Using Redux in the Application**

Once everything is set up, we can integrate Redux into our app and see how the state updates in action!

```tsx
import { DisplayCounter } from './components/DisplayCounter'
import { CounterControls } from './components/CounterControls'

export default function App() {
  return (
    <div className="App">
      <h1>My Redux Counter</h1>
      <DisplayCounter />
      <CounterControls />
    </div>
  )
}
```

## How Does Redux Work?

Let’s break down what happens when a user clicks the **“+1”** button:

1. The user clicks the “+1” button.
1. The `onIncrement` function is triggered, which dispatches an action: `{ type: “INCREMENT” }`
1. Redux sends this action to `counterReducer`, since the action is related to the `counter` state.
1. The `counterReducer` detects the `"INCREMENT"` action type and returns a new state (`state + 1`).
1. Redux updates the state in the store.
1. All components using `useSelector(counterSelector)` receive a notification about the state change.
1. The `DisplayCounter` component re-renders with the updated counter value.

This is the **one-way data flow** in Redux!

![redux](https://cdn.sanity.io/images/z7im7se7/production/4565c5fb72e883ab9624230144d68d8c2794ee46-1000x362.png)

## Redux in Real Life

To better understand Redux, imagine it as an **emergency response system**:

- **Store** → The dispatch center that keeps track of all incidents.
- **Actions** → Emergency calls reporting incidents.
- **Dispatch** → The actual call to 911.
- **Reducers** → The specific emergency responders (firefighters, ambulance, police) that handle only their type of incident.
- **Selectors** → The methods used to retrieve information from the dispatch center.

## Benefits of Redux

1. ✅ **Predictability** — State changes only through actions and reducers, making the flow consistent.
1. ✅ **Centralization** — All application state is stored in a single place.
1. ✅ **Debugging** — Every state change can be tracked.
1. ✅ **Testing** — Reducers are pure functions, making them easy to test.
1. ✅ **Caching** — The state can be saved and restored between sessions.

## When Should You Use Redux?

Redux is especially useful when:

- You have **multiple states** used across different parts of the app.
- State update logic is **complex**.
- **Many developers** are working on the same project.
- You need to **track every state change**.
- You want to **persist state** across page reloads.

## Resources

- [React Context Usage Example](https://github.com/dm3yb/react-context-and-redux-template/tree/main)
- [React Redux Usage Example](https://github.com/dm3yb/react-context-and-redux-template/tree/feat/migrate-to-redux)

## Conclusion

*Now You Know Redux!* As you can see, by breaking it down into parts and gradually implementing it into your application — plus adding some automation — you can make your app more reliable and free from unnecessary complexities!