Introduction

Redux keeps all your app state in one store and updates it through a strict action → reducer → new state flow. That makes state changes predictable and easy to debug with time-travel tools.

It shines when:

  • Many components at different tree levels read the same data.
  • You need an audit trail of every state change.
  • Complex async workflows (API calls, optimistic updates) require clear orchestration.

The core principle of Redux is a single source of truth—all state is stored in a centralized store. Changes follow a strict flow:

  1. Dispatch an Action – Defines what should happen.
  2. Reducer Updates State – A pure function modifies state without side effects.
  3. New State is Available – Components re-render with the updated state.

Redux is often used with React via react-redux, but it can work with any framework or even standalone.

Getting Started with Redux

Installing Redux and React-Redux

npm install redux react-redux
  • redux: Core library.
  • react-redux: Integration tools for React applications.

Creating a Redux Store

The store is the centralized location where application state is maintained.

js
import { createStore } from 'redux';

import { createStore } from 'redux';

// Initial state
const initialState = {
  counter: 0,
};

// Reducer function
function counterReducer(state = initialState, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { ...state, counter: state.counter + 1 };
    case 'DECREMENT':
      return { ...state, counter: state.counter - 1 };
    default:
      return state;
  }
}

// Creating store
const store = createStore(counterReducer);
console.log(store.getState()); // { counter: 0 }

Actions: Defining Intentions

Actions describe events in the application and have a required type property.

js
const incrementAction = { type: 'INCREMENT' };
const decrementAction = { type: 'DECREMENT' };

Connecting Redux with React

Use Provider from react-redux to make the store available to all components.

js
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';

import App from './App';
import counterReducer from './reducers';
import { createStore } from 'redux';

import App from './App';
import counterReducer from './reducers';
import { createStore } from 'redux';

const store = createStore(counterReducer);

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

Using Redux in Components

Use useSelector to access the store and useDispatch to send actions.

jsx
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';

function Counter() {
  const counter = useSelector(state => state.counter);
  const dispatch = useDispatch();

  return (
    <div>
      <h1>Counter: {counter}</h1>
      <button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button>
      <button onClick={() => dispatch({ type: 'DECREMENT' })}>-</button>
    </div>
  );
}

export default Counter;

Simplifying Redux with Redux Toolkit

Redux Toolkit (RTK) reduces boilerplate code and improves efficiency.

Installing Redux Toolkit

npm install @reduxjs/toolkit

Using Redux Toolkit

RTK provides createSlice to define reducers and actions in one place.

js
import { configureStore, createSlice } from '@reduxjs/toolkit';

import { configureStore, createSlice } from '@reduxjs/toolkit';

const counterSlice = createSlice({
  name: 'counter',
  initialState: { value: 0 },
  reducers: {
    increment: state => {
      state.value += 1;
    },
    decrement: state => {
      state.value -= 1;
    },
    incrementByAmount: (state, action) => {
      state.value += action.payload;
    },
  },
});

export const { increment, decrement, incrementByAmount } = counterSlice.actions;

const store = configureStore({
  reducer: {
    counter: counterSlice.reducer,
  },
});

export default store;

Middleware in Redux

Middleware enhances Redux functionality by handling side effects like logging and async operations.

Example: Logging Middleware

js
import { applyMiddleware, createStore } from 'redux';
import logger from 'redux-logger';

import { applyMiddleware, createStore } from 'redux';
import logger from 'redux-logger';

const store = createStore(rootReducer, applyMiddleware(logger));

Handling Asynchronous Operations with Redux Thunk

Redux is synchronous by default. To handle API calls, use redux-thunk.

Installing Redux Thunk

Async Action Using Thunk

js
import { applyMiddleware, createStore } from 'redux';
import thunk from 'redux-thunk';

import { applyMiddleware, createStore } from 'redux';
import thunk from 'redux-thunk';

const store = createStore(rootReducer, applyMiddleware(thunk));

export const fetchUsers = () => async dispatch => {
  dispatch({ type: 'FETCH_USERS_REQUEST' });
  try {
    const response = await fetch('https://jsonplaceholder.typicode.com/users');
    const data = await response.json();
    dispatch({ type: 'FETCH_USERS_SUCCESS', payload: data });
  } catch (error) {
    dispatch({ type: 'FETCH_USERS_FAILURE', payload: error.message });
  }
};

Real-World Example: E-Commerce Store

Consider a store selling pet products. The app needs to handle:

  • Product listings: Fetching and displaying products from an API.
  • Shopping cart management: Adding/removing products and updating totals.
  • User authentication: Managing user sessions for order processing.
  • Order processing: Handling payment and shipping details.

Each of these is managed as a separate Redux slice for modularity and scalability.

Product Slice

js
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';

import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';

export const fetchProducts = createAsyncThunk(
  'products/fetchProducts',
  async () => {
    const response = await fetch('https://api.example.com/cat-products');
    return response.json();
  }
);

const productsSlice = createSlice({
  name: 'products',
  initialState: { items: [], status: 'idle', error: null },
  extraReducers: builder => {
    builder
      .addCase(fetchProducts.pending, state => {
        state.status = 'loading';
      })
      .addCase(fetchProducts.fulfilled, (state, action) => {
        state.status = 'succeeded';
        state.items = action.payload;
      })
      .addCase(fetchProducts.rejected, (state, action) => {
        state.status = 'failed';
        state.error = action.error.message;
      });
  },
});

export default productsSlice.reducer;

This modular approach makes the application scalable and easy to maintain.

Conclusion

Don’t reach for Redux by default — Context or Zustand cover most small-to-medium apps. Pick Redux when you need middleware pipelines, DevTools time-travel, or a strict event-log architecture. If you do choose it, use Redux Toolkit from the start — vanilla Redux boilerplate isn’t worth writing anymore.

Further Reading