Introduction

Redux is a powerful JavaScript library for managing application state. It is particularly useful when:

  • You have a large application requiring centralized state management.
  • Data needs to be shared between components at different hierarchy levels.
  • You need a clear and predictable way to handle complex state updates.

Benefits of Redux

Redux helps:

  • Organize and structure application data.
  • Provide easy access to data from anywhere in the application.
  • Standardize how data is updated and managed.

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
123456789101112131415161718192021222324
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
12
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
1234567891011121314151617181920
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
1234567891011121314151617
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
1234567891011121314151617181920212223242526272829
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
1234567
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
123456789101112131415161718
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
1234567891011121314151617181920212223242526272829303132
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

Redux is a powerful tool for managing complex application state, but it is not always necessary. For simpler apps, React Context API may suffice. However, when dealing with large applications requiring predictable state management, Redux (especially with Redux Toolkit) is invaluable.

Further Reading

Redux Documentation

Redux Toolkit

React-Redux

Redux DevTools

With Redux and Redux Toolkit, you can build scalable, maintainable applications with clear data flow and predictable state management.