Chapter 7- React - Redux

We started React with the issue of real-time updates — if a variable is changing, then I want to render it in the UI instantly. Then we went to props, but there came our first stepping stone: prop drilling.

Solving that, we came across Context, and it seemed all done — no extra unnecessary states or props. But when building a large application, we happened to come across another stepping stone: multiple stores and multiple providers.

And there came the most powerful line:

“Single source of truth” — and hence, REDUX!

Redux comes in—a powerful library that helps developers write predictable, testable, and scalable applications. But before diving deep into how Redux works, let’s explore what it is, where it came from, and the specific challenges it solves in modern web development.

What is Redux?

Redux is a friendly tool that acts as a predictable state container for JavaScript applications. It's most often used with libraries like React, but you can also use it with Angular, Vue, or even plain JavaScript.

At its heart, Redux helps you keep track of your app's state in one place, called the store. It gives you a clear and organized way to update that state using actions and reducers, making everything consistent and easy to follow.

Instead of having state scattered all over different components, which can lead to confusion or bugs, Redux brings it all together in one spot—providing a single source of truth. This makes understanding and fixing data flow much simpler.

Redux works on three core principles:

  1. Single source of truth – The entire state of the app is stored in one place.

  2. State is read-only – The only way to change the state is to emit an action.

  3. Changes are made with pure functions – Reducers take the previous state and an action to return a new state.

History and Origin of Redux

Redux was created in 2015 by Dan Abramov and Andrew Clark, taking a lot of inspiration from Facebook’s Flux architecture. Back then, developers were having a tough time managing state across different parts of large applications.

Flux introduced a unidirectional data flow and separated tasks using actions, dispatchers, and stores. But it was a bit complicated—there were multiple stores, multiple dispatcher functions, and lots of boilerplate code.

Dan Abramov wanted to simplify Flux by cutting out the unnecessary parts and using functional programming principles. He was inspired by Elm, a functional language for front-end development. Redux combined Flux’s unidirectional flow with Elm’s ideas of immutable state and pure functions, creating a simpler and highly testable model.

His talk, "Hot Reloading with Time Travel", showed off Redux's cool feature of tracking every change in the app's state over time, which got developers really excited. Since then, Redux has become one of the go-to libraries for state management, especially in React applications.

Why Redux Was Introduced

Let's dive into why Redux became a must-have tool.

Modern front-end applications, especially single-page applications (SPAs), are super dynamic. They handle:

  • User input across various components

  • Asynchronous data from APIs

  • Navigation state

  • Authentication

  • UI states like loading indicators, errors, modals, and more

Managing all this with React’s local state (useState, useReducer) can get tricky when:

  • Components are deeply nested

  • Multiple components need the same data

  • It's hard to trace or debug state changes

  • Updates lead to unnecessary re-renders or prop drilling headaches

Redux came to the rescue by centralizing the state and offering a reliable, consistent, and predictable way to update it. It helps keep things organized by separating logic (reducers), triggers (actions), and data (store).

Real-World Problems

1. State Management Across Components

In big applications, lots of components might need to access or change the same data. Without Redux, this can cause prop drilling or too much use of context. Redux makes it easy for any component to get any state it needs from the centralized store, using useSelector and useDispatch in React.

2. Predictability

Redux’s strict one-way flow and use of pure functions (reducers) make sure that state changes are always predictable. Every action that updates the state passes through a reducer, which makes it simple to track, debug, and test.

3. Debugging and Time Travel

With tools like the Redux DevTools, developers can:

  • Check out every action and see the resulting state.

  • Travel back in time to look at previous states.

  • Replay actions to track down bugs.
    This is super handy in big applications where finding errors can be tough.

4. Consistency Across the App

Since the state is all in one place and updates follow the same rules, apps become more consistent. Even if lots of developers are working on a project, Redux makes sure everyone follows the same pattern for managing data.

5. Improved Testability

Reducers are pure functions with no side effects, which makes them really easy to unit test. You can give them a previous state and an action, and check that the new state is what you expect.

6. Decoupled Logic

Business logic doesn’t have to be in components anymore. It can be neatly handled in reducers, action creators, or middleware like Redux Thunk or Redux Saga, keeping your components focused on UI rendering.

When to Use Redux

While Redux offers powerful benefits, it’s not always necessary—especially in small projects. You might want to consider Redux if:

  • Your app has complex state logic.

  • Multiple components rely on shared state.

  • You want advanced debugging and state tracking.

  • You need a consistent structure as the team grows.

Redux vs Context API

FeatureContext APIRedux
PurposePrimarily for prop drilling avoidance (simpler state sharing).Designed for managing complex and large-scale application state.
Boilerplate CodeMinimal setup and code.Requires more setup: actions, reducers, store, etc.
ScalabilityWorks well for small to medium apps.Better suited for large applications with complex state logic.
PerformanceMay cause unnecessary re-renders if not optimized.Optimized updates with selective rendering via reducers.
State ManagementNot centralized (shared through nested providers).Centralized with a single source of truth (global store).
Middleware SupportNo built-in support.Supports middleware (like redux-thunk, redux-saga) for async logic.
DevToolsLimited debugging capabilities.Powerful Redux DevTools for time-travel debugging, inspection, etc.
Learning CurveEasy to learn, part of React itself.Steeper learning curve due to additional concepts.
Community & EcosystemLimited tooling.Rich ecosystem and widespread community support.

Now let's start with Redux.

Here's how we will learn:

  1. First, we will focus on pure React in JavaScript only. Once you understand the logic and how it actually works, using RTK or Thunk will be easier.

  2. Next, we will learn about Redux DevTools.

  3. Then, we will move on to Redux Toolkit.

  4. After that, we will cover Redux Thunk.

  5. Finally, we will explore RTK.

While learning each topic, we will begin with the theory and definitions. We will build applications, mainly focusing on CRUD, to-do, or cart functionality. These are basic enough to help us learn everything without getting too confused.

So let's start digging in.

Vanilla Redux (JS Only)

Imagine you’re building a toy store. You have:

  • A shelf where all the toys (data) are kept — this is like the Redux store.

  • A note saying "Add a teddy bear to the shelf" — this is an action.

  • A manager who reads the note and updates the shelf accordingly — this is the reducer.

  • And whenever the shelf changes, the shop gets updated — this is like the subscribe function updating your UI.

Redux helps you store data (like your app’s state) in one place, and update it in a predictable way.

1. Store — "The Shelf"

This is where all your app’s data lives.

  • It's like a big box that holds everything your app needs to remember.

  • The store is created using a special function.

  • You can ask the store: “Hey, what’s in you?” using getState().

  • You can tell it: “Hey, update yourself” using dispatch().

In code:

// Import Redux (assuming you've installed it)
const Redux = require('redux');  

// 1 Create a reducer function (more on this later)
const reducerFunction = (state = { items: [] }, action) => {
  // For now, just return the state as-is
  return state;
};

// 2 Create the Store (the "big box")  
//    - Pass in the reducer function (tells the store how to update)
const store = Redux.createStore(reducerFunction);

// 3 Check what's inside the store (like peeking into the box)
console.log(store.getState()); // { items: [] }

//  Later, we'll use store.dispatch() to update it

2. Actions — "The Notes"

Actions are just plain JavaScript objects.

  • You write a note to the store: “Please add this todo.”

  • This note must have a type (like a label) that tells the store what kind of change to make.

  • You can also send extra data using payload.

Example:

//  Example 1: A simple action (just a type)
const incrementAction = {
  type: 'INCREMENT_COUNTER' // No payload needed here
};

//  Example 2: Action with payload (sending data)
const addTodoAction = {
  type: 'ADD_TODO',          // Required "label" for the Store
  payload: 'Buy groceries'   // Extra data (the actual todo text)
};

//  Sending the action to the Store (we'll cover dispatch later)
// store.dispatch(addTodoAction);

3. Reducers — "The Manager Who Reads Notes"

A reducer is a function that knows how to change the state based on the action it receives.

  • It takes the current state and the action.

  • It returns a new state (not the same one!).

  • It never touches the original — it just makes a new one based on instructions.

Example:

//  Example: A todo list reducer
function todoReducer(state = [], action) {
  // Check the action type (like reading a note's label)
  switch (action.type) {
    case 'ADD_TODO':
      // 📝 Return NEW state (old todos + new one)
      return [...state, action.payload]; // Never modify original array!

    case 'DELETE_TODO':
      //  Filter out the todo to delete
      return state.filter(todo => todo.id !== action.payload.id);

    default:
      //  Unknown action? Just return state unchanged
      return state;
  }
}

// Usage with the store:
// const store = Redux.createStore(todoReducer);

4. Dispatch — "Send the Note to the Manager"

You use dispatch() to send the action to the store.

//  First, create an action (the "note")
const addTodoAction = {
  type: 'ADD_TODO',
  payload: 'Study Redux'  // The actual data
};

//  Then dispatch it to the store
store.dispatch(addTodoAction);

//  Or do it in one line:
store.dispatch({
  type: 'ADD_TODO',
  payload: 'Master Redux'
});

This tells the store: “Here’s a request to update the state.”

What Happens Under the Hood:

Redux calls your reducer with:

  • Current state

  • The action you dispatched
    Reducer returns new state
    Store updates → Notifies all listeners

  • Key Notes:
    1. Dispatch is the only way to trigger state changes.
    2. The action flows: dispatch → reducer → new state.

5. Subscribe — "Let Me Know When the Shelf Changes"

When the store updates, you want your UI (like a list on the screen) to update too.

  • subscribe() lets you listen to changes.

  • You give it a function — and Redux will call it every time something changes.

//  Set up the listener
const unsubscribe = store.subscribe(() => {
  // This runs EVERY TIME the state changes
  console.log(" State updated!", store.getState());

  // In a real app, you'd update your UI here:
  // renderTodos(store.getState().todos);
});

// Later, if you want to stop listening:
unsubscribe(); // Rings the doorbell!

// Simple counter logger
store.subscribe(() => {
  console.log(`Current count: ${store.getState().count}`);
});

store.dispatch({ type: 'INCREMENT' }); // Logs: "Current count: 1"
store.dispatch({ type: 'INCREMENT' }); // Logs: "Current count: 2"
  1. The callback runs after every dispatch

  2. Always unsubscribe when you don't need updates anymore (prevents memory leaks)

  3. In React, you'll use useSelector instead (we'll cover this later)

6. getState() — "What’s on the Shelf Right Now?"

This lets you peek into the store and see what’s inside.

//  Peek at the current state
const currentState = store.getState();
console.log(" Shelf contents:", currentState);

//  Practical example (Todo app):
console.log("My todos:", currentState.todos);
// Might log: ["Buy milk", "Learn Redux"]

Let’s Build It: ToDo App with Plain JavaScript and Redux

Now that you understand the theory, let’s build a ToDo App from scratch.

Folder Setup

todo-app/
├── index.html
└── app.js

Step 1: Create index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Redux ToDo App</title>
</head>
<body>
  <h1>My ToDo List</h1>
  <input id="todo-input" placeholder="What do you need to do?" />
  <button id="add-btn">Add</button>
  <ul id="todo-list"></ul>

  <!-- Redux Library -->
  <script src="https://unpkg.com/redux@4.2.1/dist/redux.min.js"></script>
  <script src="app.js"></script>
</body>
</html>

Step 2: app.js – Start Writing Code Step-by-Step

1. Define Action Types (Just text labels to avoid typos)

/* ========== 1. ACTION TYPES ========== */
// Constants to prevent typos in action.type strings
const ADD_TODO = 'ADD_TODO';
const DELETE_TODO = 'DELETE_TODO';

2. Create Action Creators (Functions that return action objects)

/* ========== 2. ACTION CREATORS ========== */
// Factory functions that create action objects
function addTodo(text) {
  return {
    type: ADD_TODO,
    payload: text  // The todo text to add
  };
}

function deleteTodo(index) {
  return {
    type: DELETE_TODO,
    payload: index  // Array index of todo to remove
  };
}

3. Write the Reducer (How state should change)

/* ========== 3. REDUCER ========== */
// Handles state updates based on action types
function todoReducer(state = [], action) {
  switch (action.type) {
    case ADD_TODO:
      // Return new array with added todo (never modify existing state!)
      return [...state, action.payload];

    case DELETE_TODO:
      // Return filtered array without the deleted todo
      return state.filter((_, i) => i !== action.payload);

    default:
      // Always return current state for unknown actions
      return state;
  }
}

4. Create Store

/* ========== 4. STORE CREATION ========== */
// Create store with our reducer
const store = Redux.createStore(todoReducer);

5. Render ToDos on Screen (Whenever state changes)

/* ========== 5. RENDER FUNCTION ========== */
// Updates DOM to match current state
function render() {
  const todos = store.getState(); // Get current todos
  const list = document.getElementById('todo-list');

  // Clear previous list items
  list.innerHTML = ''; 

  // Create list item for each todo
  todos.forEach((todo, index) => {
    const li = document.createElement('li');
    li.textContent = todo;

    // Add delete button for each todo
    const delBtn = document.createElement('button');
    delBtn.textContent = '❌';
    delBtn.onclick = () => store.dispatch(deleteTodo(index));

    li.appendChild(delBtn);
    list.appendChild(li);
  });
}

6. Listen to Changes

/* ========== 6. STATE SUBSCRIPTION ========== */
// Re-render whenever state changes
store.subscribe(render);

7. Add Event Listener to Button

/* ========== 7. EVENT LISTENERS ========== */
// Handle new todo submissions
document.getElementById('add-btn').addEventListener('click', () => {
  const input = document.getElementById('todo-input');
  const text = input.value.trim();

  if (text) {
    store.dispatch(addTodo(text)); // Add to store
    input.value = ''; // Clear input
  }
});

// Initial render
render();

That’s It! Your Redux ToDo App is Ready

Now you:

  • Added new todos to the store.

  • Deleted them from the store.

  • Watched your UI update itself automatically when the store changed.

Final Tips

  • Redux has only 3 moving parts: store, actions, reducers.

  • Always return new state, never mutate the old one.

  • Learn Redux without React first – it will make using it with React way easier.

Application URL - https://redux-playground-zxewrswt.stackblitz.io

Editor URL - https://stackblitz.com/edit/redux-playground-zxewrswt?file=index.js

Absolutely! Let’s explore Redux DevTools in a beginner-friendly and in-depth way — just like you’re watching or reading your first detailed vlog on it.

Redux DevTools

Imagine being able to see your entire app’s brain, travel back in time, pause it, and understand every move it makes — that’s Redux DevTools.

What Are Redux DevTools?

Redux DevTools is a browser extension (or library) that lets you visualize, inspect, and debug your Redux store.

Think of it like this:

  • You send an action: “Add to cart”

  • Redux updates the store

  • DevTools records that action and the change it made

  • You can see that action, the state before and after, and even go back in time

It’s like a black box for your app's brain — every thought, every memory, and every decision is saved and visible.

Why Redux DevTools Are Helpful (Real Benefits)

  1. Visual Debugging
    No more guessing what changed — see it.

  2. Time Travel
    Pause, play, rewind your app’s state like a movie.

  3. Bug Tracking
    Find out what went wrong and when.

  4. Learning Tool
    Beginners can watch Redux work in real time.

  5. Performance Optimizing
    Spot unnecessary actions or large state changes.

How to Install Redux DevTools

1. Install the Browser Extension

Click "Add to browser" → Done!

2. Add Support in Your Code

If you’re using plain Redux (not Redux Toolkit):

const store = Redux.createStore(
  rootReducer,
  window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);

If using Redux Toolkit (recommended for React apps):

const store = configureStore({
  reducer: rootReducer,
  devTools: true // it's enabled by default
});

1. State Viewer

This shows the entire Redux state at any given moment.

  • Want to know what’s inside the store right now?

  • Click on "State" and see it as a tree view.

Tip: Click through nested objects to explore deep structures.

This is how it looks initially.

2. Action Logger

Every time you dispatch an action (like "ADD_TODO" or "DELETE_ITEM"), Redux DevTools:

  • Logs it in a timeline

  • Shows you the type and payload

  • Lets you click each one and see how the state changed

This helps you debug issues like: “Why didn’t my todo show up?”

3. Time Travel Debugging (Jump, Play, Pause)

This is like having a remote control for your app.

Buttons:

  • Jump to the first action

  • Pause recording actions

  • Replay actions one by one

  • Rollback to previous states

This lets you test your app at different stages without refreshing or clicking again.

4. Diff Comparison

Every time the state updates, DevTools shows you a diff:

  • What changed?

  • What stayed the same?

It highlights:

  • Added values

  • Removed ones

  • Modified values

Useful for finding small changes in large state trees.

5. Chart Visualizations

The “Chart” tab helps you see your app’s state like a map.

  • You can explore:

    • Actions

    • State changes

    • Dependencies between slices

This is especially powerful in large apps where:

  • You manage many reducers

  • You want to understand the flow

Now after all this lets understand multiple reducers

What is combineReducers?

combineReducers is a helper function from Redux that lets you combine multiple reducer functions into one main reducer.

Imagine your application state as a big object. Instead of having one function handle all the different parts of that object, combineReducers allows you to divide the logic into separate functions, each taking care of its own piece of the state.

The Problem Without combineReducers

Let’s say you have two different features in your app:

  • A counter

  • A user profile

You could manage both like this:

/**
 * Initial state object for the application.
 * Contains:
 * - count: A number representing a counter value
 * - user: An object containing user information (name and email)
 */
const initialState = {
  count: 0,
  user: {
    name: '',
    email: ''
  }
};

/**
 * Root reducer function that manages the application state.
 * 
 * @param {Object} state - Current state of the application. Defaults to initialState if not provided.
 * @param {Object} action - Action object containing type and optional payload.
 * @returns {Object} - New state after applying the action.
 */
function rootReducer(state = initialState, action) {
  // Switch statement determines how state should change based on action type
  switch (action.type) {
    // Case for incrementing the counter
    case 'INCREMENT':
      return {
        // Spread operator copies all existing state properties
        ...state,
        // Override the count property with incremented value
        count: state.count + 1
      };

    // Case for setting user data
    case 'SET_USER':
      return {
        // Spread operator copies all existing state properties
        ...state,
        // Override the user property with new user data from action payload
        user: action.payload
      };

    // Default case returns current state for unknown actions
    default:
      return state;
  }
}

As you add more features, this rootReducer will become long and hard to read.

Enter combineReducers

Instead of one giant reducer, you can break it down:

/**
 * Reducer for managing count state
 * 
 * @param {number} state - Current count value, defaults to 0 if not provided
 * @param {Object} action - Action object containing type and optional payload
 * @returns {number} - New count value after applying the action
 */
function countReducer(state = 0, action) {
  switch (action.type) {
    // Handles INCREMENT action
    case 'INCREMENT':
      // Returns current state + 1 (pure function - doesn't mutate state)
      return state + 1;

    // Default case returns current state for any unhandled actions
    default:
      return state;
  }
}

/**
 * Reducer for managing user state
 * 
 * @param {Object} state - Current user object, defaults to {name: '', email: ''} if not provided
 * @param {string} state.name - User's name
 * @param {string} state.email - User's email
 * @param {Object} action - Action object containing type and payload
 * @returns {Object} - New user object after applying the action
 */
function userReducer(state = { name: '', email: '' }, action) {
  switch (action.type) {
    // Handles SET_USER action
    case 'SET_USER':
      // Replaces entire user state with action payload
      // Note: In real apps, you might want to validate payload structure
      return action.payload;

    // Default case returns current state for any unhandled actions
    default:
      return state;
  }
}

Then combine them:

import { combineReducers } from 'redux';

/**
 * Combines multiple reducers into a single reducer function.
 * The resulting reducer manages a state object where each key corresponds
 * to the state managed by its associated reducer.
 * 
 * @type {import('redux').Reducer}
 * @property {number} count - State managed by countReducer
 * @property {Object} user - State managed by userReducer
 * @property {string} user.name - User's name from userReducer
 * @property {string} user.email - User's email from userReducer
 */
const rootReducer = combineReducers({
  /**
   * Reducer managing the count state slice
   * @see countReducer
   */
  count: countReducer,

  /**
   * Reducer managing the user state slice
   * @see userReducer
   */
  user: userReducer
});

// Alternative JSDoc syntax for more detailed typing:
/**
 * @typedef {Object} RootState
 * @property {number} count - The counter value from countReducer
 * @property {Object} user - The user object from userReducer
 * @property {string} user.name - User's name
 * @property {string} user.email - User's email
 */

/**
 * @type {import('redux').Reducer<RootState>}
 */
const rootReducer = combineReducers({
  count: countReducer,
  user: userReducer
});

Now, rootReducer will automatically create a state structure like this:

{
  count: 0,
  user: {
    name: '',
    email: ''
  }
}

And each reducer will only be responsible for its own part of the state.

How combineReducers Works Internally

Under the hood, combineReducers creates a function like this:

function rootReducer(state = {}, action) {
  return {
    count: countReducer(state.count, action),
    user: userReducer(state.user, action)
  };
}

It calls each reducer with its part of the state and the same action.

Using it in a Redux Store

Here’s how everything fits together:

import { createStore, combineReducers } from 'redux';

/**
 * Counter reducer - handles numeric count state
 * @param {number} [state=0] Initial count value (defaults to 0)
 * @param {Object} action Redux action object
 * @returns {number} New count value
 */
function countReducer(state = 0, action) {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1;
    default:
      return state;
  }
}

/**
 * User reducer - handles user profile state
 * @param {Object} [state={name: '', email: ''}] Initial user state
 * @param {string} state.name User's name
 * @param {string} state.email User's email
 * @param {Object} action Redux action object
 * @returns {Object} New user state
 */
function userReducer(state = { name: '', email: '' }, action) {
  switch (action.type) {
    case 'SET_USER':
      return {
        ...action.payload,
        // You could add validation or transformations here
      };
    default:
      return state;
  }
}

/**
 * Combined root reducer that manages the complete application state
 * @type {import('redux').Reducer}
 */
const rootReducer = combineReducers({
  count: countReducer,    // Manages state.count
  user: userReducer      // Manages state.user
});

/**
 * Redux store instance that holds the complete application state
 * @type {import('redux').Store}
 */
const store = createStore(
  rootReducer,
  // Optional: You could add middleware or enhancers here
  // window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);

// Example action dispatches
store.dispatch({ type: 'INCREMENT' });  // Increments count by 1
store.dispatch({ type: 'INCREMENT' });  // Increments count again

store.dispatch({
  type: 'SET_USER',
  payload: {
    name: 'John',
    email: 'john@example.com'
  }
});

/**
 * Gets and logs the current complete Redux state
 * @returns {void}
 */
function logCurrentState() {
  console.log('Current Redux state:', store.getState());
  // Example output:
  // {
  //   count: 2,
  //   user: { name: 'John', email: 'john@example.com' }
  // }
}

logCurrentState();

// Optional: Subscribe to store changes
const unsubscribe = store.subscribe(logCurrentState);

// Later you can unsubscribe when needed
// unsubscribe();

Nested combineReducers

/**
 * Combined reducer for application-specific state (auth and settings)
 * @type {import('redux').Reducer}
 * @property {Object} auth - Authentication state managed by authReducer
 * @property {Object} settings - Application settings managed by settingsReducer
 */
const appReducer = combineReducers({
  /**
   * Handles authentication state
   * @see authReducer
   */
  auth: authReducer,

  /**
   * Handles application configuration/settings
   * @see settingsReducer
   */
  settings: settingsReducer
});

/**
 * Root reducer combining all state slices in the application
 * @type {import('redux').Reducer}
 * @property {Object} app - Contains auth and settings from appReducer
 * @property {Object} ui - UI state managed by uiReducer
 */
const rootReducer = combineReducers({
  /**
   * Main application state (nested reducer)
   * @see appReducer
   */
  app: appReducer,

  /**
   * UI-related state (loading states, modals, etc.)
   * @see uiReducer
   */
  ui: uiReducer
});

// TypeScript-style alternative with full state shape documentation:
/**
 * @typedef {Object} AppState
 * @property {import('./authReducer').AuthState} auth
 * @property {import('./settingsReducer').SettingsState} settings
 */

/**
 * @typedef {Object} RootState
 * @property {AppState} app
 * @property {import('./uiReducer').UIState} ui
 */

/**
 * @type {import('redux').Reducer<RootState>}
 */
const rootReducer = combineReducers({
  app: appReducer,
  ui: uiReducer
});

The Need for Combine Reducers

As applications grow, managing all state with a single reducer becomes unwieldy. Different parts of your state might need to handle different actions, and having everything in one massive switch statement is hard to maintain.

This is where the concept of combining reducers comes in. It allows you to:

  • Split your state management into smaller, more focused reducers

  • Keep each reducer responsible for a specific slice of the state

  • Combine them back into a single reducer function

Now time has come for react-redux

Installing Redux and react-redux

npm install redux react-redux

Or if you're using yarn:

yarn add redux react-redux

These packages provide:

  • redux: The core Redux library that creates the store and handles actions/reducers

  • react-redux: Official React bindings for Redux that provide components like Provider and hooks like useSelector and useDispatch

You might also want to install Redux DevTools for debugging:

npm install --save-dev redux-devtools-extension

1. Provider – The Connection Point

What it does:

The Provider is like a big delivery hub that makes your Redux store accessible to your entire React app. Imagine your Redux store as a giant warehouse of data (like user info, cart items, etc.). The Provider shares this data with any component that asks for it.

Key Concept:

// Import necessary libraries and components
import React from 'react'; // Main React library
import ReactDOM from 'react-dom'; // React DOM rendering utilities
import { Provider } from 'react-redux'; // Provides Redux store to React components
import { createStore } from 'redux'; // Redux function to create the application store
import rootReducer from './reducers'; // Combined reducers for the application
import App from './App'; // Main application component

/**
 * Create the Redux store that holds the complete state tree of the app.
 * The store is created by passing the root reducer which combines all
 * individual reducers of the application.
 */
const store = createStore(rootReducer);

/**
 * Render the application to the DOM.
 * 
 * The <Provider> component makes the Redux store available to all
 * child components that are wrapped in connect().
 * 
 * The main <App> component is rendered inside the Provider,
 * which allows all components in the app to access the Redux store.
 * 
 * The application is mounted to the DOM element with id 'root'.
 */
ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

You're telling React: “Hey, here’s a global data store. Any component inside <App /> can now tap into it.”

Think of it like:

A school has a library (Redux store). By placing the library key (Provider) at the entrance of the school, every student (component) can enter and borrow books (data), as long as they know what they’re looking for.

Key points about Provider:

  • It uses React's Context API behind the scenes

  • Only one store should be passed to it

  • Any component in the component tree can access the store if needed

  • It doesn't interfere with component's local state - you can use both together

2. useSelector – Reading From the Store

What it does:

The useSelector hook lets your component read data from the Redux store. Think of it like a library card – you use it to get access to specific books from the library.

const cartItems = useSelector(state => state.cart.items);

You're saying: “Give me the list of items inside the cart part of the store.”

Reactivity:

Every time state.cart.items changes, your component automatically re-renders to show the latest data. This is like a notification system: if a book you borrowed gets updated, the library tells you.

// Import the useSelector hook from react-redux to access the Redux store state
import { useSelector } from 'react-redux';

/**
 * Cart component that displays all items in the shopping cart.
 * 
 * This component connects to the Redux store to access the cart items
 * and renders them in a list format.
 */
function Cart() {
  // Select cart items from the Redux store state
  // The useSelector hook takes a selector function that extracts
  // the specific part of state we need (cart.items in this case)
  const cartItems = useSelector(state => state.cart.items);

  return (
    <div>
      {/* 
        Render each cart item as a separate div 
        Using item.id as the key for optimal React rendering performance
      */}
      {cartItems.map(item => (
        <div key={item.id}>
          {/* Display the name of each cart item */}
          {item.name}
        </div>
      ))}
    </div>
  );
}

export default Cart;

Important notes about useSelector:

  • It takes a selector function that receives the entire Redux state

  • It will cause a re-render whenever the selected value changes

  • For performance, you can use multiple useSelector calls instead of returning a large object

  • For derived data, consider using memoized selectors with libraries like Reselect

3. useDispatch – Sending Actions to the Store

What it does:

useDispatch is your way of sending instructions (actions) to the Redux store. It's like placing an order at a counter.

dispatch(addToCart(product))

You’re saying: “Hey Redux, please add this product to the cart.”

Real-world Analogy:

You're in a restaurant. dispatch is like telling the waiter (Redux) to perform a task (like placing your food order). The action (e.g. addToCart) tells Redux what you want to do, and the reducers will update the store accordingly.

// Import the useDispatch hook from react-redux to dispatch actions to the Redux store
import { useDispatch } from 'react-redux';
// Import the addToCart action creator from our actions file
import { addToCart } from './actions';

/**
 * Product component that displays a single product and allows adding it to cart.
 * 
 * @param {Object} props - Component props
 * @param {Object} props.product - The product object containing product details
 * @param {string} props.product.name - The name of the product
 * @param {number|string} props.product.id - Unique identifier for the product
 * @param {number} props.product.price - Price of the product
 */
function Product({ product }) {
  // Initialize the dispatch function from Redux
  // This will be used to send actions to the Redux store
  const dispatch = useDispatch();

  return (
    <div className="product">
      {/* Display the product name */}
      <h3>{product.name}</h3>

      {/*
        Button to add the product to cart
        When clicked, it dispatches the addToCart action with the product as payload
      */}
      <button 
        onClick={() => dispatch(addToCart(product))}
        aria-label={`Add ${product.name} to cart`}
      >
        Add to Cart
      </button>
    </div>
  );
}

export default Product;

Best practices with useDispatch:

  • Consider creating action dispatcher functions to avoid dispatching directly in components

  • For async operations, use middleware like redux-thunk or redux-saga

  • Memoize dispatch functions with useCallback if passing them as props to optimized components

Project Time!!!

Features we will implement -

  1. Add to Cart:

    • Products can be added to the cart

    • Duplicate items increment quantity instead of creating new entries

  2. Remove from Cart:

    • Individual items can be removed

    • Entire cart can be cleared

  3. Update Quantity:

    • Quantity can be increased/decreased with buttons

    • Direct quantity input could be added easily

  4. Calculations:

    • Automatic calculation of total quantity

    • Automatic calculation of total amount

    • Per-item subtotal calculation

Step 1: Set Up Redux Store Structure

// types/cartTypes.ts

// Define the shape of a single cart item
export interface CartItem {
  id: string | number;
  name: string;
  price: number;
  quantity: number;
  image?: string; // Optional product image
}

// Define the shape of the entire cart state
export interface CartState {
  items: CartItem[];
  totalQuantity: number;
  totalAmount: number;
}

Step 2: Create Cart Actions

// actions/cartActions.ts
import { CartItem } from '../types/cartTypes';

// Action types
export const ADD_TO_CART = 'ADD_TO_CART';
export const REMOVE_FROM_CART = 'REMOVE_FROM_CART';
export const UPDATE_QUANTITY = 'UPDATE_QUANTITY';
export const CLEAR_CART = 'CLEAR_CART';

// Action creators
export const addToCart = (item: Omit<CartItem, 'quantity'>) => ({
  type: ADD_TO_CART,
  payload: { ...item, quantity: 1 }, // Default quantity to 1 when adding
});

export const removeFromCart = (id: string | number) => ({
  type: REMOVE_FROM_CART,
  payload: id,
});

export const updateQuantity = (id: string | number, quantity: number) => ({
  type: UPDATE_QUANTITY,
  payload: { id, quantity },
});

export const clearCart = () => ({
  type: CLEAR_CART,
});

Step 3: Create Cart Reducer

// reducers/cartReducer.ts

// Importing required types
import { CartState, CartItem } from '../types/cartTypes';

// Importing action types
import {
  ADD_TO_CART,
  REMOVE_FROM_CART,
  UPDATE_QUANTITY,
  CLEAR_CART,
} from '../actions/cartActions';

// Initial state of the cart
const initialState: CartState = {
  items: [],
  totalQuantity: 0,
  totalAmount: 0,
};

// Utility function to calculate total quantity and total amount from items array
const calculateTotals = (items: CartItem[]) => {
  const totalQuantity = items.reduce((sum, item) => sum + item.quantity, 0);
  const totalAmount = items.reduce(
    (sum, item) => sum + item.price * item.quantity,
    0
  );
  return { totalQuantity, totalAmount };
};

// Reducer function to handle cart-related actions
export const cartReducer = (
  state = initialState,
  action: any
): CartState => {
  switch (action.type) {
    // Add item to cart
    case ADD_TO_CART: {
      // Check if the item already exists in the cart
      const existingItem = state.items.find(
        (item) => item.id === action.payload.id
      );

      let updatedItems;
      if (existingItem) {
        // If item exists, increase its quantity
        updatedItems = state.items.map((item) =>
          item.id === action.payload.id
            ? { ...item, quantity: item.quantity + 1 }
            : item
        );
      } else {
        // If item doesn't exist, add it to the cart
        updatedItems = [...state.items, action.payload];
      }

      // Recalculate totals after updating the items
      const { totalQuantity, totalAmount } = calculateTotals(updatedItems);

      return {
        ...state,
        items: updatedItems,
        totalQuantity,
        totalAmount,
      };
    }

    // Remove item from cart using its ID
    case REMOVE_FROM_CART: {
      const updatedItems = state.items.filter(
        (item) => item.id !== action.payload
      );

      // Recalculate totals after removal
      const { totalQuantity, totalAmount } = calculateTotals(updatedItems);

      return {
        ...state,
        items: updatedItems,
        totalQuantity,
        totalAmount,
      };
    }

    // Update the quantity of a specific item
    case UPDATE_QUANTITY: {
      const { id, quantity } = action.payload;

      // If quantity is zero or negative, return current state (or optionally remove item)
      if (quantity <= 0) {
        return state;
      }

      // Update the quantity for the matched item
      const updatedItems = state.items.map((item) =>
        item.id === id ? { ...item, quantity } : item
      );

      // Recalculate totals after quantity update
      const { totalQuantity, totalAmount } = calculateTotals(updatedItems);

      return {
        ...state,
        items: updatedItems,
        totalQuantity,
        totalAmount,
      };
    }

    // Clear the entire cart and reset to initial state
    case CLEAR_CART:
      return initialState;

    // For any unknown action, return current state
    default:
      return state;
  }
};

Step 4: Create Product Component

// components/Product.tsx
import React from 'react';
import { useDispatch } from 'react-redux';
import { addToCart } from '../actions/cartActions';

interface ProductProps {
  product: {
    id: string | number;
    name: string;
    price: number;
    image?: string;
    description?: string;
  };
}

/**
 * Product component displays a single product with add to cart functionality
 * @param {ProductProps} props - Component props containing product information
 */
const Product: React.FC<ProductProps> = ({ product }) => {
  const dispatch = useDispatch();

  const handleAddToCart = () => {
    dispatch(addToCart(product));
  };

  return (
    <div className="product-card">
      {product.image && (
        <img 
          src={product.image} 
          alt={product.name} 
          className="product-image"
        />
      )}
      <h3 className="product-name">{product.name}</h3>
      <p className="product-price">${product.price.toFixed(2)}</p>
      {product.description && (
        <p className="product-description">{product.description}</p>
      )}
      <button 
        onClick={handleAddToCart}
        className="add-to-cart-btn"
        aria-label={`Add ${product.name} to cart`}
      >
        Add to Cart
      </button>
    </div>
  );
};

export default Product;

Step 5: Create Cart Component

// components/Cart.tsx
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { 
  removeFromCart, 
  updateQuantity,
  clearCart 
} from '../actions/cartActions';
import { CartState } from '../types/cartTypes';

/**
 * Cart component displays all items in the cart with options to modify quantities,
 * remove items, or clear the entire cart
 */
const Cart: React.FC = () => {
  const dispatch = useDispatch();
  const { items, totalQuantity, totalAmount } = useSelector(
    (state: { cart: CartState }) => state.cart
  );

  const handleRemoveItem = (id: string | number) => {
    dispatch(removeFromCart(id));
  };

  const handleQuantityChange = (id: string | number, newQuantity: number) => {
    dispatch(updateQuantity(id, newQuantity));
  };

  const handleClearCart = () => {
    dispatch(clearCart());
  };

  if (items.length === 0) {
    return (
      <div className="cart-empty">
        <p>Your cart is empty</p>
      </div>
    );
  }

  return (
    <div className="cart-container">
      <h2 className="cart-title">Your Cart ({totalQuantity} items)</h2>

      <div className="cart-items">
        {items.map((item) => (
          <div key={item.id} className="cart-item">
            <div className="item-info">
              <h4 className="item-name">{item.name}</h4>
              <p className="item-price">${item.price.toFixed(2)}</p>
            </div>

            <div className="item-quantity">
              <button
                onClick={() => handleQuantityChange(item.id, item.quantity - 1)}
                disabled={item.quantity <= 1}
                aria-label="Decrease quantity"
              >
                -
              </button>

              <span className="quantity-value">{item.quantity}</span>

              <button
                onClick={() => handleQuantityChange(item.id, item.quantity + 1)}
                aria-label="Increase quantity"
              >
                +
              </button>
            </div>

            <div className="item-subtotal">
              ${(item.price * item.quantity).toFixed(2)}
            </div>

            <button
              onClick={() => handleRemoveItem(item.id)}
              className="remove-item"
              aria-label={`Remove ${item.name} from cart`}
            >
              ×
            </button>
          </div>
        ))}
      </div>

      <div className="cart-summary">
        <div className="total-amount">
          <span>Total:</span>
          <span>${totalAmount.toFixed(2)}</span>
        </div>

        <button 
          onClick={handleClearCart}
          className="clear-cart-btn"
        >
          Clear Cart
        </button>

        <button className="checkout-btn">Proceed to Checkout</button>
      </div>
    </div>
  );
};

export default Cart;

Step 6: Combine Reducers and Create Store

// store.ts
import { createStore, combineReducers } from 'redux';
import { cartReducer } from './reducers/cartReducer';

// Combine all reducers
const rootReducer = combineReducers({
  cart: cartReducer,
  // Add other reducers here if needed
});

// Define root state type based on the reducers
export type RootState = ReturnType<typeof rootReducer>;

// Create Redux store
export const store = createStore(rootReducer);

Step 7: Implement the Main App Component

// App.tsx
import React from 'react';
import { Provider } from 'react-redux';
import { store } from './store';
import Product from './components/Product';
import Cart from './components/Cart';
import './App.css';

// Sample products data
const sampleProducts = [
  {
    id: 1,
    name: 'Wireless Headphones',
    price: 99.99,
    image: 'https://example.com/headphones.jpg',
    description: 'High-quality wireless headphones with noise cancellation',
  },
  {
    id: 2,
    name: 'Smart Watch',
    price: 199.99,
    image: 'https://example.com/smartwatch.jpg',
    description: 'Latest model with health tracking features',
  },
  // Add more products as needed
];

const App: React.FC = () => {
  return (
    <Provider store={store}>
      <div className="app">
        <header className="app-header">
          <h1>E-Commerce Store</h1>
        </header>

        <main className="app-main">
          <section className="products-section">
            <h2>Products</h2>
            <div className="products-grid">
              {sampleProducts.map((product) => (
                <Product key={product.id} product={product} />
              ))}
            </div>
          </section>

          <aside className="cart-section">
            <Cart />
          </aside>
        </main>
      </div>
    </Provider>
  );
};

export default App;

Now, as we approach the more commonly used redux format , we have reached redux-toolkit.

So, let's begin with redux-toolkit.

What is Redux Toolkit?

Redux Toolkit is a library that wraps around the original Redux library and provides powerful utilities to simplify state management.

Why use it?

  • Reduces boilerplate

  • Comes with Immer for immutability

  • Includes Redux Thunk by default

  • Simplifies store setup

  • Encourages best practices

To install:

npm install @reduxjs/toolkit react-redux

What Are Slices?

In Redux Toolkit, Slices combine the reducer logic and action creators in one place. Think of it as a “slice” of your global state + the actions that can change it.

Creating a slice:

// Import the createSlice function from Redux Toolkit
// createSlice simplifies the process of creating Redux slices (reducer + actions)
import { createSlice } from '@reduxjs/toolkit';

// Create a counter slice using createSlice
// A slice contains the reducer logic and actions for a specific feature
const counterSlice = createSlice({
  // Name of the slice, used to generate action types
  name: 'counter',

  // Initial state for this slice of the Redux store
  initialState: { value: 0 },

  // Reducer functions that handle state updates
  reducers: {
    // Increment reducer: increases the counter value by 1
    // Uses immer under the hood for safe state mutation
    increment: state => {
      state.value += 1;
    },

    // Decrement reducer: decreases the counter value by 1
    decrement: state => {
      state.value -= 1;
    },

    // IncrementByAmount reducer: adds specified value to the counter
    // Uses action payload to determine increment amount
    incrementByAmount: (state, action) => {
      state.value += action.payload;
    }
  }
});

// Export the generated action creators for use in components
// These functions create actions that we can dispatch
export const { increment, decrement, incrementByAmount } = counterSlice.actions;

// Export the reducer function for use in the Redux store configuration
// This handles all actions defined in the slice's reducers field
export default counterSlice.reducer;

Note: You directly mutate state inside reducers, but thanks to Immer, it’s actually immutable behind the scenes.

Immer: Mutability Made Simple

Normally in Redux, you must return a new object to update state, which can be error-prone. Redux Toolkit uses Immer internally, which lets you "mutate" state directly while keeping it immutable.

Without Immer (vanilla Redux):

return { ...state, value: state.value + 1 };

With Immer (RTK):

state.value += 1;

Behind the scenes, Immer tracks your changes and produces a new immutable state object.

Todo App with Redux Toolkit

Let’s see Redux Toolkit in action with a simple Todo example.

1. Create todoSlice.js

// Import createSlice from Redux Toolkit
// createSlice provides an efficient way to create Redux state slices with associated reducers and actions
import { createSlice } from '@reduxjs/toolkit';

// Create a todo slice to manage todo items in the Redux store
const todoSlice = createSlice({
  // Slice name used to generate action types
  name: 'todo',

  // Initial state is an empty array to store todo items
  initialState: [],

  // Reducer functions to handle different todo operations
  reducers: {
    // Add a new todo item to the state
    // Uses immer to allow safe state mutation
    addTodo: (state, action) => {
      // Push new todo object with unique ID, text from payload, and default completed status
      state.push({ 
        id: Date.now(), // Generate unique ID using current timestamp
        text: action.payload, // Todo text from action payload
        completed: false // Initial completed status
      });
    },

    // Toggle the completed status of a todo item
    toggleTodo: (state, action) => {
      // Find the todo item by ID from action payload
      const todo = state.find(t => t.id === action.payload);
      if (todo) {
        // Safely mutate the state with immer
        todo.completed = !todo.completed;
      }
    },

    // Delete a todo item from the state
    deleteTodo: (state, action) => {
      // Return new array filtered to exclude todo with matching ID
      // Note: This uses a return statement instead of direct mutation
      return state.filter(t => t.id !== action.payload);
    }
  }
});

// Export generated action creators for use in components
// These functions create actions that can be dispatched
export const { addTodo, toggleTodo, deleteTodo } = todoSlice.actions;

// Export the reducer function for Redux store configuration
// Handles all todo-related state updates
export default todoSlice.reducer;

2. Configure store

// Import configureStore from Redux Toolkit
// configureStore simplifies Redux store creation with good defaults
import { configureStore } from '@reduxjs/toolkit';

// Import the todo reducer from the todo slice file
import todoReducer from './todoSlice';

// Create the Redux store using configureStore
const store = configureStore({
  // Combine multiple reducers into a single root reducer
  reducer: {
    // Define the 'todo' state branch managed by todoReducer
    // This matches the slice name defined in todoSlice
    todo: todoReducer
  }
  // Note: configureStore automatically enables:
  // - Redux DevTools Extension
  // - Thunk middleware
  // - Development checks for common mistakes
});

// Export the configured store as the default export
// This store should be provided to the React app using the Provider component
export default store;

3. Connect in your React component

// Import React and necessary hooks
import React, { useState } from 'react';
// Import Redux hooks to interact with the store
import { useSelector, useDispatch } from 'react-redux';
// Import todo-related action creators
import { addTodo, toggleTodo, deleteTodo } from './todoSlice';

function TodoApp() {
  // Local state for managing the input field
  const [input, setInput] = useState('');

  // Get todos from Redux store state using useSelector
  // 'state.todo' corresponds to the reducer name in store configuration
  const todos = useSelector(state => state.todo);

  // Get dispatch function to send actions to Redux store
  const dispatch = useDispatch();

  // Handle adding new todo
  const handleAdd = () => {
    if (input) {
      // Dispatch addTodo action with input text as payload
      dispatch(addTodo(input));
      // Clear input field after submission
      setInput('');
    }
  };

  return (
    <div>
      {/* Todo input section */}
      <input 
        value={input} 
        onChange={e => setInput(e.target.value)}
        placeholder="Enter todo..."
      />
      <button onClick={handleAdd}>Add</button>

      {/* Todo list display */}
      <ul>
        {todos.map(todo => (
          <li 
            key={todo.id} 
            // Toggle todo completion on click
            onClick={() => dispatch(toggleTodo(todo.id))}
          >
            {/* Show text with strike-through if completed */}
            <span style={{ 
              textDecoration: todo.completed ? 'line-through' : 'none' 
            }}>
              {todo.text}
            </span>
            {/* Delete button with stopPropagation to prevent toggle */}
            <button 
              onClick={(e) => {
                e.stopPropagation();
                dispatch(deleteTodo(todo.id))
              }}
            >
              ❌
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default TodoApp;

What is Middleware in Redux?

Middleware sits between dispatching an action and the moment it reaches the reducer. It lets you intercept actions to:

  • Log actions

  • Delay actions

  • Handle side effects like API calls

Example: Logger middleware

// Redux middleware that logs actions and state changes
// Follows the middleware signature: store => next => action => fn
const logger = store => next => action => {
  // Log the action before it propagates through the middleware chain
  console.log('Dispatching:', action);

  // Pass the action to the next middleware/reducer and get the result
  // This is crucial for proper middleware chain execution
  const result = next(action);

  // Log the updated state after the action has been processed
  // Note: store.getState() returns the entire state tree
  console.log('Next State:', store.getState());

  // Return the result to maintain the middleware chain
  // This preserves compatibility with other middlewares
  return result;
};

// Usage example: 
// Apply this middleware when creating the store:
// const store = configureStore({
//   middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(logger)
// });

// Key features:
// 1. Logs action type and payload before processing
// 2. Shows state changes after reducers have handled the action
// 3. Maintains proper middleware chain by returning next() result
// 4. Works with both synchronous and asynchronous actions

// Important considerations:
// - Should be added as the first middleware for accurate action logging
// - Might expose sensitive data in state/actions - use carefully in production
// - Consider conditional logging based on environment (dev vs prod)
// - For production use, consider using redux-logger package instead

In RTK, add custom middleware like this:

// Create Redux store using Redux Toolkit's configureStore
const store = configureStore({
  // Combine all reducers into a single root reducer
  // rootReducer should be created using combineReducers if multiple reducers exist
  reducer: rootReducer,

  // Configure middleware chain
  middleware: (getDefaultMiddleware) =>
    // Start with default middleware provided by Redux Toolkit
    // Includes: thunk, immutable check, serializable check
    getDefaultMiddleware()
      // Add custom logger middleware to the chain
      .concat(logger)
});

// Key Configuration Details:
// 1. Default middleware includes:
//    - Redux Thunk (async actions)
//    - Immutability checks in development
//    - Serializability checks in development
// 2. Logger middleware added last to capture complete state changes
// 3. Store automatically configures:
//    - Redux DevTools Extension integration
//    - Development checks for common errors

// Production Considerations:
// - Remove logger middleware in production using env check:
//   .concat(process.env.NODE_ENV !== 'production' ? logger : [])
// - Consider removing development checks:
//   getDefaultMiddleware({
//     immutableCheck: false,
//     serializableCheck: false
//   })

API Calls in Redux Toolkit

API calls are side effects and not supposed to happen in reducers. We use middleware (like Redux Thunk) for this.

RTK also has createAsyncThunk for handling async actions.

Example: Fetching posts

Create async thunk

// Import Redux Toolkit async utilities
// createAsyncThunk handles asynchronous action creators
// createSlice creates Redux slice with async reducer support
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';

// Create async thunk for fetching posts from API
// First argument: action type prefix (sliceName/actionName)
// Second argument: async payload creator callback
export const fetchPosts = createAsyncThunk(
  'posts/fetchPosts', // Action type prefix for generated action types:
                     // - pending: 'posts/fetchPosts/pending'
                     // - fulfilled: 'posts/fetchPosts/fulfilled'
                     // - rejected: 'posts/fetchPosts/rejected'
  async () => {
    // Perform actual API call
    const response = await fetch('https://jsonplaceholder.typicode.com/posts');

    // Return parsed JSON as payload for fulfilled action
    // Note: errors are automatically caught and handled
    return response.json();

    // For error handling, you could add try/catch:
    // try {
    //   const res = await fetch(...);
    //   return await res.json();
    // } catch (error) {
    //   return thunkAPI.rejectWithValue(error.message);
    // }
  }
);

// Typical next steps (not shown here):
// 1. Create slice with extraReducers to handle async states
// 2. Export reducer and actions
// 3. Use in component with dispatch(fetchPosts())

// Best practices notes:
// - Keep async logic in thunks separate from reducers
// - Handle loading/error states in slice
// - Consider adding cancellation with AbortController
// - Use TypeScript interfaces for API response types
// - Add error handling for production use
// - Consider rate-limiting for API protection

postsSlice.js

// Create posts slice to manage API fetched posts data
const postsSlice = createSlice({
  // Slice name used to generate action types
  name: 'posts',

  // Initial state shape for posts data management:
  // - data: array to store fetched posts
  // - loading: boolean for request status tracking
  // - error: string/null to store error messages
  initialState: { 
    data: [], 
    loading: false, 
    error: null 
  },

  // Handle async thunk actions using extraReducers
  extraReducers: (builder) => {
    builder
      // Handle pending state when fetchPosts is initiated
      .addCase(fetchPosts.pending, state => {
        state.loading = true;
        state.error = null; // Clear previous errors
      })

      // Handle successful data fetch
      .addCase(fetchPosts.fulfilled, (state, action) => {
        state.loading = false;
        // Store fetched data in state
        // Note: This replaces existing data, consider merging for pagination
        state.data = action.payload;
      })

      // Handle API request failure
      .addCase(fetchPosts.rejected, (state, action) => {
        state.loading = false;
        // Store error message from rejected promise
        // For better error handling, use action.payload with rejectWithValue
        state.error = action.error.message;
      });
  }
});

// Export the generated reducer for store configuration
export default postsSlice.reducer;

// Typical usage patterns:
// - Dispatch(fetchPosts()) in components
// - Access state with useSelector: 
//   const { data, loading, error } = useSelector(state => state.posts)

// Best practices notes:
// 1. Consider adding data normalization for large datasets
// 2. Add error reset functionality in separate reducer
// 3. Implement caching strategies for repeated requests
// 4. Add cancellation support with AbortController
// 5. Use selectors for state access abstraction
// 6. Consider adding timestamp for data freshness checks

Redux Thunk (Async Logic)

Redux Thunk is a middleware that lets you return a function instead of an action. This is great for:

  • Delaying dispatch

  • Running async logic

  • Making API calls

RTK includes it by default!

Custom thunk example:

// Traditional Redux async action creator using thunk middleware
// Note: Consider using createAsyncThunk from Redux Toolkit for modern approach
export const fetchUsers = () => async (dispatch) => {
  // Dispatch loading state action
  dispatch(startLoading());

  try {
    // Initiate API request to fetch users
    const res = await fetch('/api/users');

    // Check for HTTP errors (fetch doesn't reject on HTTP error status)
    if (!res.ok) {
      throw new Error(`HTTP error! status: ${res.status}`);
    }

    // Parse JSON response
    const data = await res.json();

    // Dispatch success action with formatted data
    dispatch(usersLoaded(data));

  } catch (err) {
    // Dispatch error action with error message
    // Consider adding error formatting/handling logic here
    dispatch(setError(err.message));

    // For better error handling, you might want to:
    // - Distinguish between network errors and server errors
    // - Log errors to monitoring service
    // - Dispatch specific error types
  }
};

// Typical action creators needed (not shown here):
// - startLoading(): { type: 'users/startLoading' }
// - usersLoaded(payload): { type: 'users/loaded', payload }
// - setError(message): { type: 'users/setError', payload: message }

// Modern Redux Toolkit alternative:
// Use createAsyncThunk + createSlice extraReducers for better TypeScript support
// and automatic action type generation

// Best practices notes:
// 1. Add request cancellation support with AbortController
// 2. Consider adding timeout handling
// 3. Add data validation/normalization for API responses
// 4. Use proper API service layer abstraction
// 5. Add retry logic for transient errors
// 6. Consider adding caching mechanisms

Here is the repo of todo application in redux toolkit -

https://github.com/vaishdwivedi1/redux-toolkit-todo

Finally, we come to the last and one of the most important topics of React: REDUX TOOLKIT QUERY.

What is RTK Query?

RTK Query is a zero-config, opinionated data fetching and caching layer for Redux apps. It handles the tough stuff of managing server-state (data from APIs) by automatically setting up Redux logic like actions, reducers, and selectors. Designed to fit seamlessly with Redux Toolkit, it saves you the hassle of writing thunks, middleware, or caching logic on your own.

Key Features:

  1. Auto-Generated React Hooks: Create query and mutation hooks with a single function.

  2. Smart Caching: Automatic deduplication, revalidation, and garbage collection.

  3. Declarative API: Define endpoints with endpoints, and RTK Query handles the rest.

  4. Optimistic Updates: Update the UI instantly while syncing with the server in the background.

  5. TypeScript Support: First-class TypeScript integration for type-safe APIs.

Why Use RTK Query?

The Problem with Traditional Redux

In classic Redux, fetching data requires:

  • Writing async thunks or sagas.

  • Managing loading, error, and success states.

  • Caching logic to avoid redundant network requests.

  • Normalizing data for performance.

This leads to verbose code, maintenance overhead, and potential bugs.

How RTK Query Solves It

RTK Query automates these tasks:

  • Automatic Caching: Queries are cached by default, and identical requests are deduped.

  • Auto-Generated Hooks: Use useQuery or useMutation hooks for data fetching.

  • Built-in Loading & Error States: Access isLoading, isError, and more directly.

  • Simplified Mutations: Update server data and automatically re-fetch related queries.

Setting Up RTK Query

Step 1: Install Redux Toolkit

npm install @reduxjs/toolkit react-redux

Step 2: Create an API Service

Define your API endpoints in a file like apiSlice.js:

// Import core RTK Query functions for creating API service
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';

/**
 * Create a centralized API service instance using RTK Query
 * This will be used to define endpoints and generate React hooks
 */
export const api = createApi({
  /**
   * Configure base query with default settings for all requests
   * Uses fetchBaseQuery (wrapper around fetch) with base API URL
   */
  baseQuery: fetchBaseQuery({
    baseUrl: '/api', // Base URL for all API endpoints
    // (Optional: Add default headers/authentication here)
  }),

  /**
   * Define API endpoints for CRUD operations
   * Endpoints can be queries (GET) or mutations (POST/PUT/DELETE)
   * Each endpoint will automatically generate a React hook
   */
  endpoints: (builder) => ({
    /**
     * GET /posts endpoint configuration
     * Query endpoint for fetching list of posts
     * Generated hook: useGetPostsQuery()
     */
    getPosts: builder.query({
      query: () => ({
        url: '/posts', // Endpoint URL (appended to baseUrl)
        method: 'GET', // Default method, can be omitted
        // (Optional: Add params/headers here)
      }),
      // (Optional: Add transformResponse/transformError here)
      // (Optional: Add providesTags for caching)
    }),

    /**
     * POST /posts endpoint configuration
     * Mutation endpoint for creating new post
     * Generated hook: useAddPostMutation()
     */
    addPost: builder.mutation({
      query: (newPost) => ({
        url: '/posts', // Endpoint URL
        method: 'POST',
        body: newPost, // Request payload
        // (Optional: Add headers/params here)
      }),
      // (Optional: Add invalidatesTags for cache invalidation)
      // (Optional: Add onQueryStarted for optimistic updates)
    }),
  }),
});

/**
 * Auto-generated React hooks from defined endpoints
 * Hooks follow naming convention:
 * - use + EndpointName + Query/Mutation
 * Example: getPosts(query) -> useGetPostsQuery
 *          addPost(mutation) -> useAddPostMutation
 * 
 * Hook features:
 * - Automatic data fetching/caching
 * - Loading/error states management
 * - Cache lifecycle control
 */
export const { 
  useGetPostsQuery, // Hook for fetching posts
  useAddPostMutation // Hook for creating posts
} = api;

Explanation:

  1. createApi Configuration:

    • Core setup for API service

    • Combines base configuration with endpoints

    • Manages caching, hook generation, and Redux integration

  2. fetchBaseQuery:

    • Lightweight fetch wrapper

    • Handles request/response serialization

    • Base URL for all subsequent endpoints

  3. Endpoint Builder:

    • builder.query for read operations (GET)

    • builder.mutation for write operations (POST/PUT/DELETE)

    • Each endpoint configuration includes:

      • URL path

      • HTTP method

      • Request configuration (body, headers, params)

  4. Auto-Generated Hooks:

    • Return object with { data, error, isLoading, refetch }

    • Mutation hooks return [triggerFn, { data, error, status }]

    • Handle full request lifecycle management

Step 3: Configure the Store

Add the API service to your Redux store:

// Import core Redux Toolkit store configuration utility
import { configureStore } from '@reduxjs/toolkit';
// Import API service instance from our slice definition
import { api } from './apiSlice';

/**
 * Create and configure Redux store instance
 * This serves as the central state management container
 * Integrates with RTK Query for API state management
 */
export const store = configureStore({
  /**
   * Combine reducers from different slices
   * RTK Query automatically generates its reducer under api.reducerPath
   */
  reducer: {
    // Dynamic namespace for RTK Query's internal state
    // Uses the reducerPath defined in the API service (default: 'api')
    [api.reducerPath]: api.reducer,

    // (Optional: Add other reducers here for client-side state management)
    // Example: user: userReducer
  },

  /**
   * Configure middleware chain
   * Essential for RTK Query functionality including:
   * - Caching
   * - Polling
   * - Request lifecycle management
   */
  middleware: (getDefaultMiddleware) =>
    // Start with default middleware including:
    // - redux-thunk
    // - Immutability check (development)
    // - Serializability check (development)
    getDefaultMiddleware()
      // Add RTK Query's middleware for automatic caching and polling
      .concat(api.middleware),
});

/**
 * Store Configuration Summary:
 * - Dedicated slice for RTK Query state (managed automatically)
 * - Integrated middleware for handling API request lifecycle
 * - Ready for expansion with additional reducers/middleware
 * 
 * TypeScript Note: Store type is inferred automatically
 * Export store type for potential type-safe access patterns
 */

// Optional: Export RootState type for type-safe selectors
// export type RootState = ReturnType<typeof store.getState>;

Explanation:

  1. configureStore Purpose:

    • Creates the Redux store instance

    • Combines reducers and middleware

    • Enables DevTools integration automatically

  2. Reducer Configuration:

    • [api.reducerPath] uses the auto-generated reducer path from createApi

    • Maintains RTK Query's internal state (cache, subscriptions, etc)

    • Shows where to add additional reducers for client state

  3. Middleware Configuration:

    • Preserves default middleware (thunk, immutability checks)

    • Adds RTK Query's custom middleware for:

      • Automatic caching/revalidation

      • Request deduplication

      • Polling management

      • Error handling

  4. TypeScript Support:

    • Automatic type inference for store state

    • Optional RootState type export for type-safe selectors

Step 4: Use Hooks in Components

Fetch data in your React components:

import { useGetPostsQuery, useAddPostMutation } from './apiSlice';

/**
 * PostsList Component
 * Demonstrates RTK Query integration for data fetching and mutations
 * Features:
 * - Automatic query execution on mount
 * - Loading/error states handling
 * - Optimistic UI updates (when configured)
 * - Cache management through API tags
 */
function PostsList() {
  // Query Hook: Fetch posts data
  // Automatically triggers on component mount
  // Returns { data, isLoading, isError, refetch, ... }
  const { 
    data: posts,       // Renamed for clarity
    isLoading,         // Loading state indicator
    isError            // Error state indicator
  } = useGetPostsQuery(
    undefined,         // Query argument (none required in this case)
    {
      // Optional: Add polling/refetch options
      // pollingInterval: 5000,
      // refetchOnMountOrArgChange: true
    }
  );

  // Mutation Hook: Post creation
  // Returns [trigger function, { isLoading, error, data }]
  const [addPost] = useAddPostMutation({
    // Optional: Add fixed cache tags for invalidation
    // fixedCacheTag: 'Posts'
  });

  // Loading state UI
  if (isLoading) return <div>Loading...</div>;

  // Error state UI
  if (isError) return <div>Error fetching posts!</div>;

  // Main render
  return (
    <div className="posts-container">
      {/* Post list rendering */}
      {posts?.map(post => (
        <div key={post.id} className="post-item">
          {post.title}
        </div>
      ))}

      {/* Post creation button */}
      <button 
        onClick={() => 
          // Trigger mutation with new post payload
          addPost({ 
            title: 'New Post',
            // In real usage: Get values from form state
            body: 'Sample content',
            userId: 1
          })
          // Optional: Handle promise for error tracking
          // .unwrap()
          // .then(...)
          // .catch(...)
        }
        className="add-post-btn"
      >
        Add Post
      </button>
    </div>
  );
}

/**
 * Component Behavior Details:
 * - On mount: Automatically fetches posts via useGetPostsQuery
 * - On addPost click: 
 *   1. Sends POST request
 *   2. If invalidatesTags configured in API slice:
 *      - Automatically refetches posts after mutation
 *   3. Without cache invalidation: Requires manual refetch
 * 
 * Best Practice Notes:
 * - Consider adding error boundaries for production
 * - For forms: Use controlled components with local state
 * - Add loading state for mutations (isLoading from mutation result)
 * - Implement optimistic updates via onQueryStarted in API slice
 * - Consider adding debouncing for rapid user interactions
 */

Explanation:

  1. Query Hook (useGetPostsQuery):

    • Auto-fetches on component mount

    • Manages loading/error states internally

    • Returns destructured response with renamed data

  2. Mutation Hook (useAddPostMutation):

    • Returns tuple with trigger function and status object

    • Trigger function accepts payload for POST request

    • Demonstrates optional promise handling

  3. State Handling:

    • Clear loading/error states before rendering content

    • Optional loading state for mutation (not shown)

  4. UI Structure:

    • Basic list rendering with unique keys

    • Action button with inline handler

    • Styling class placeholders

  5. Performance Considerations:

    • Cache behavior depends on API slice configuration

    • Optional refetching strategies

Component Lifecycle Flow:

Mount → Fetch Posts → Render
  ↑
Add Post → Mutation → (Optional Cache Invalidation → Auto-refetch)

Advanced Features

1. Cache Management

  • Re-fetching on Demand: Use refetch from the query hook or invalidate tags to trigger re-fetches.

  • Tags for Cache Invalidation: Tag endpoints to invalidate cached data after mutations:

      endpoints: (builder) => ({
        /**
         * GET /posts - Query Endpoint Configuration
         * Fetches all posts and manages caching
         */
        getPosts: builder.query<Post[], void>({
          query: () => '/posts',  // Endpoint path
    
          /**
           * providesTags - Cache Management
           * Registers this data with 'Posts' tag
           * When other operations invalidate this tag:
           * - Automatic re-fetch occurs if mounted components are using this data
           * - Updates all dependent components with fresh data
           */
          providesTags: ['Posts'],  // Can also use result-based function:
          // providesTags: (result) => 
          //   result ? result.map(({ id }) => ({ type: 'Post', id })) : ['Post']
    
          // Optional: Add transform for response normalization
          // transformResponse: (response: ApiResponse<Post[]>) => response.data,
    
          // Optional: Set caching time (seconds)
          // keepUnusedDataFor: 60,
        }),
    
        /**
         * POST /posts - Mutation Endpoint Configuration
         * Creates new post and invalidates cached data
         */
        addPost: builder.mutation<Post, CreatePostDto>({
          query: (newPost) => ({
            url: '/posts',
            method: 'POST',
            body: newPost,  // Request payload
            // headers: { 'Content-Type': 'application/json' },  // Default
          }),
    
          /**
           * invalidatesTags - Cache Invalidation
           * Invalidates all components using 'Posts' tag
           * Triggers automatic re-fetch of getPosts query
           * Can target specific cache entries:
           * invalidatesTags: (result, error, arg) => [{ type: 'Post', id: arg.id }]
           */
          invalidatesTags: ['Posts'],  // Full cache invalidation
    
          // Optional: Optimistic update configuration
          // onQueryStarted: (newPost, { dispatch, queryFulfilled }) => {
          //   // Implement optimistic update logic here
          // }
        }),
    
        // Additional endpoint pattern example:
        // getPostById: builder.query<Post, string>({
        //   query: (id) => `/posts/${id}`,
        //   providesTags: (result, error, id) => [{ type: 'Post', id }],
        // }),
      }),
    

    Explanation:

    1. Cache Tag System:

      • providesTags: Declares what data a query provides

      • invalidatesTags: Specifies which cached data to mark as stale

      • Tags can be:

        • String identifiers ('Posts')

        • Type+ID objects ({ type: 'Post', id: '123' })

    2. Query Lifecycle:

       graph LR
       A[Component Mount] --> B[getPosts Query]
       B --> C{Cache Valid?}
       C -->|Yes| D[Return Cached Data]
       C -->|No| E[Fetch Fresh Data]
       F[addPost Mutation] --> G[Invalidate 'Posts' Tag]
       G --> H[Trigger getPosts Re-fetch]
      
    3. Best Practices:

      • Granular Tagging: Prefer specific tags over broad invalidation

          // Better than ['Posts'] for large datasets
          providesTags: (result) =>
            result ? result.map(({ id }) => ({ type: 'Post', id })) : ['Post']
        
      • Optimistic Updates: Use onQueryStarted for instant UI feedback

      • Error Handling: Add transformErrorResponse for consistent error formats

    4. Performance Considerations:

      • Broad tags (['Posts']) simplify code but may cause over-fetching

      • Specific tags optimize updates but require careful ID management

      • Balance between cache freshness and network requests

Full Integration Flow:

  1. Component mounts and calls useGetPostsQuery()

  2. RTK Query checks cache for 'Posts' tagged data

  3. Either returns cached data or fetches fresh

  4. User triggers addPost mutation

  5. On success:

    • Invalidates 'Posts' tag

    • All active useGetPostsQuery() hooks re-fetch

    • UI automatically updates with fresh data

This configuration eliminates manual cache management while maintaining:

  • Data consistency

  • Automatic updates

  • Network optimization

  • Type safety (when using TypeScript)

2. Optimistic Updates

Update the UI before the server responds using onQueryStarted:

addPost: builder.mutation<Post, CreatePostDto>({
  query: (newPost) => ({
    url: '/posts',
    method: 'POST',
    body: newPost,
  }),

  /**
   * onQueryStarted - Lifecycle hook for mutation side effects
   * Enables optimistic/pessimistic updates and rollback logic
   * @param newPost - Mutation argument (new post data)
   * @param context - Contains { dispatch, queryFulfilled, getState }
   */
  async onQueryStarted(newPost, { dispatch, queryFulfilled }) {
    try {
      // Pessimistic Update Pattern (default)
      // 1. Wait for server confirmation
      const { data: createdPost } = await queryFulfilled;

      // 2. Update local cache manually
      dispatch(
        api.util.updateQueryData(
          'getPosts',       // Target endpoint name
          undefined,        // Original query argument (none in this case)
          (draftPosts) => { // Immer-powered draft modifier
            // Add new post to existing cache
            draftPosts.push(createdPost);

            // Alternative: Prepend to list
            // draftPosts.unshift(createdPost);
          }
        )
      );

      // 3. Can dispatch additional actions here
      // dispatch(otherSlice.actions.notifyPostAdded());

    } catch (error) {
      // Error Handling Strategies:
      // 1. Automatic rollback not needed here (pessimistic update)
      // 2. Log error to monitoring service
      // 3. Dispatch error notification
      console.error('Post creation failed:', error);

      // Example error handling:
      // dispatch(notificationsSlice.actions.showError(
      //   'Failed to create post'
      // ));
    }
  },

  // Optional: For optimistic updates instead:
  // async onQueryStarted(newPost, { dispatch, queryFulfilled }) {
  //   const patchResult = dispatch(
  //     api.util.updateQueryData('getPosts', undefined, draft => {
  //       draft.push({ ...newPost, id: Date.now() }); // Temp ID
  //     })
  //   );
  //   try {
  //     await queryFulfilled;
  //   } catch {
  //     patchResult.undo(); // Rollback on error
  //   }
  // },
}),

Key Concepts Explained:

  1. Pessimistic vs Optimistic Updates:

     graph TD
     A[Mutation Trigger] --> B{Pessimistic?}
     B -->|Yes| C[Wait for Server Response]
     C --> D[Update Cache on Success]
     B -->|No| E[Immediately Update Cache]
     E --> F[Handle Success/Error]
    
  2. Cache Update Mechanics:

    • api.util.updateQueryData uses Immer.js for immutable updates

    • Operates on normalized RTK Query cache

    • More efficient than full re-fetch when:

      • Server returns complete created object

      • UI needs instant feedback

  3. Error Handling Considerations:

    • Pessimistic updates don't require rollbacks

    • Maintain consistent state with server

    • Better for data integrity, worse for perceived performance

  4. When to Use This Pattern:

    • When server returns complete created entity

    • When UI doesn't need instant feedback

    • When data consistency is critical

Performance Comparison Table:

MetricPessimisticOptimistic
UI ResponseSlowerInstant
Data ConsistencyHighRisky
Error HandlingSimplerComplex
Network UsageSameSame

Here's a comparison table highlighting the key differences between pessimistic and optimistic updates in data fetching:

CriteriaPessimistic UpdatesOptimistic Updates
Data ConsistencyWaits for server confirmation before UI updateUpdates UI immediately, then syncs with server
User ExperienceSlower feedback (shows loading states)Instant feedback (feels faster)
Error HandlingNo rollback needed (UI only updates on success)Requires explicit rollback logic on failure
Implementation ComplexitySimple (no undo logic)Complex (requires undo/rollback handling)
Use CasesCritical operations (e.g., financial transactions)Non-critical actions (e.g., likes, comments)
Rollback MechanismNot requiredManual undo via patchResult.undo()
Server DependencyHigh (UI depends on server response)Low (UI assumes success)
Network PerceptionFeels slower (waits for network)Feels instant (UI updates immediately)
Cache ManagementUpdates cache after server confirmationUpdates cache first, reverts on error
Data Integrity RiskLow (data always matches server)Higher (temporary mismatch possible)
Example ScenarioBank transfer, medical records updateSocial media likes, draft autosave

When to Use Which?

  1. Pessimistic:

    • Critical data where accuracy is paramount

    • When server response is needed for UI updates

    • Simple error handling preferred

  2. Optimistic:

    • Actions where speed > perfect accuracy

    • Highly interactive UIs (e.g., drag-and-drop)

    • When server usually succeeds (low error rate)

Code Pattern Comparison

Pessimistic Update (RTK Query):

async onQueryStarted(arg, { dispatch, queryFulfilled }) {
  try {
    const { data } = await queryFulfilled; // Wait for server
    dispatch(updateCache(data)); // Update UI after success
  } catch (error) {
    // No rollback needed
  }
}

Optimistic Update (RTK Query):

async onQueryStarted(arg, { dispatch, queryFulfilled }) {
  const patchResult = dispatch(updateCache(arg)); // Update UI first
  try {
    await queryFulfilled; // Sync with server
  } catch {
    patchResult.undo(); // Rollback on failure
  }
}
  • Pessimistic = "Trust but verify"

  • Optimistic = "Assume the best, prepare for the worst"

Choose based on your app's needs: data criticality vs. user experience priorities.

3. Polling & Realtime Updates

// Execute query with polling configuration
const { data } = useGetPostsQuery(
  // Query arguments (none required in this case)
  undefined, 

  // Query options object
  {
    /**
     * pollingInterval: 5000 - Auto-re-fetching configuration
     * 
     * Features:
     * - Automatically re-fetches data every 5 seconds
     * - Continues polling while components are mounted
     * - Smart refetching that respects cache tags
     * - Network requests are deduplicated (no duplicate calls)
     * 
     * Behavior:
     * 1. Initial fetch on component mount
     * 2. Subsequent fetches every 5000ms
     * 3. Stops when component unmounts
     * 4. Pauses when window loses focus (default behavior)
     * 
     * Use cases:
     * - Real-time dashboards
     * - Live score updates
     * - Any frequently changing data
     */
    pollingInterval: 5000, // 5000ms = 5 seconds

    // Optional additional configurations:
    // skip: false, // Bypass polling conditionally
    // refetchOnMountOrArgChange: true, // Force initial fetch
    // refetchOnFocus: true, // Re-fetch when window regains focus
    // refetchOnReconnect: true // Re-fetch when network reconnects
  }
);

Explanation:

  1. Polling Mechanism:

    • Automatic recurring data fetching

    • Background updates without user interaction

    • Built-in cache invalidation and deduplication

  2. Performance Considerations:

     graph LR
     A[Polling Start] --> B[Network Request]
     B --> C{Data Changed?}
     C -->|Yes| D[Update UI]
     C -->|No| E[No UI Changes]
    
  3. Best Practices:

    • Use sparingly - balance freshness vs network load

    • Consider server load when choosing interval

    • Combine with skip option for conditional polling

    • For true real-time needs, consider WebSockets + RTK Query cache updates

  4. Alternate Pattern (Dynamic Polling):

     const [pollingInterval, setPollingInterval] = useState(5000);
     const { data } = useGetPostsQuery(undefined, {
       pollingInterval,
     });
     // Can dynamically adjust interval based on app state
    

This implementation pattern is ideal for scenarios where:

  • WebSocket integration isn't available

  • You need simple real-time-like behavior

  • Data changes frequently but predictably

  • You want built-in network optimization

We've covered a lot of theory, so let's put it into practice. This blog is already quite long, so I will attach a to-do application with the RTK GitHub repo below.

https://github.com/vaishdwivedi1/rtk-query

Conclusion

The article dives into Redux, a tool for managing state in JavaScript apps, especially popular with React. It kicks off by talking about the challenges of handling state and data flow in big apps and introduces Redux as the fix. The main ideas of Redux are having one source of truth, keeping state unchanged, and using pure functions. It covers the history and perks of Redux, then walks you through setting it up, like creating a store, sending actions, and using reducer functions. The article also breaks down Redux Toolkit, which makes Redux easier by cutting down on repetitive code and adding handy tools like Immer and Redux Thunk. It includes RTK Query, a feature for handling server-state with automatic caching and ready-made React hooks. Examples, like a to-do app and cart management, show how to use Redux and its new features in real life.

Do share your Thoughts!!!

0
Subscribe to my newsletter

Read articles from Vaishnavi Dwivedi directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Vaishnavi Dwivedi
Vaishnavi Dwivedi