Redux Fundamentals: Core Concepts

Module 25: Frontend Frameworks & State Management

Introduction to State Management

As React applications grow, managing state becomes increasingly complex. Component state works well for isolated data, but presents challenges when state needs to be:

Think of state management like managing inventory in a large store. When the store is small with few products (components), a simple notepad (local state) works fine. But as the store grows to multiple departments with thousands of products, you need a centralized inventory system (global state management) to track everything efficiently.

Types of State Management Solutions

graph TD A[Application State] A --> B[Local Component State] A --> C[Global/Shared State] C --> D[Context API] C --> E[Redux] C --> F[Other Libraries] style A fill:#f96 style B fill:#9af style C fill:#9af style D fill:#bd6 style E fill:#bd6 style F fill:#bd6 classDef default stroke:#333,stroke-width:2px;

What is Redux?

Redux is a predictable state container for JavaScript applications. It helps you write applications that behave consistently across different environments and are easy to test.

Redux was inspired by Flux architecture and functional programming concepts. It was created by Dan Abramov and Andrew Clark in 2015 and quickly became the most popular state management solution for React applications.

When Do You Need Redux?

Redux is most beneficial when:

Real-world examples of applications that benefit from Redux:

The Three Principles of Redux

Redux is built on three fundamental principles:

1. Single Source of Truth

The entire state of your application is stored in a single JavaScript object called the store.

Analogy: Think of the Redux store like a bank's central database. All your account information is stored in one secure location, rather than scattered across multiple systems. This makes it easier to track, audit, and manage your financial data.

// Example of a Redux store state object
{
  user: {
    id: "u123",
    name: "Jane Doe",
    email: "jane@example.com",
    preferences: {
      theme: "dark",
      notifications: true
    }
  },
  products: {
    items: [...],
    isLoading: false,
    error: null
  },
  cart: {
    items: [...],
    totalAmount: 129.99
  }
}

2. State is Read-Only

The only way to change state is to emit an action, which is a plain JavaScript object describing what happened.

Analogy: Imagine a library where books (state) can't be modified directly. Instead, you submit a change request form (action) to the librarian, who then makes the appropriate modifications according to the library's rules.

// Examples of Redux actions
// Action to add item to cart
{
  type: 'ADD_TO_CART',
  payload: {
    productId: 'p123',
    name: 'Wireless Headphones',
    price: 79.99,
    quantity: 1
  }
}

// Action to toggle theme
{
  type: 'TOGGLE_THEME'
}

// Action to update user profile
{
  type: 'UPDATE_USER_PROFILE',
  payload: {
    name: 'Jane Smith',
    email: 'jane.smith@example.com'
  }
}

3. Changes are Made with Pure Functions

To specify how the state tree is transformed by actions, you write pure reducers - functions that take the previous state and an action, and return the next state.

Analogy: Reducers are like pastry chefs following a strict recipe. Given the same ingredients (previous state and action) and recipe (reducer logic), they will always produce the same pastry (next state) without affecting anything else in the kitchen.

// Example of a Redux reducer
function cartReducer(state = { items: [], totalAmount: 0 }, action) {
  switch (action.type) {
    case 'ADD_TO_CART':
      const newItem = action.payload;
      const existingItemIndex = state.items.findIndex(
        item => item.productId === newItem.productId
      );
      
      if (existingItemIndex >= 0) {
        // Item already exists, update quantity
        const updatedItems = [...state.items];
        updatedItems[existingItemIndex] = {
          ...updatedItems[existingItemIndex],
          quantity: updatedItems[existingItemIndex].quantity + newItem.quantity
        };
        
        return {
          ...state,
          items: updatedItems,
          totalAmount: state.totalAmount + (newItem.price * newItem.quantity)
        };
      } else {
        // Add new item
        return {
          ...state,
          items: [...state.items, newItem],
          totalAmount: state.totalAmount + (newItem.price * newItem.quantity)
        };
      }
      
    case 'REMOVE_FROM_CART':
      const productId = action.payload;
      const itemToRemove = state.items.find(item => item.productId === productId);
      
      if (!itemToRemove) return state;
      
      return {
        ...state,
        items: state.items.filter(item => item.productId !== productId),
        totalAmount: state.totalAmount - (itemToRemove.price * itemToRemove.quantity)
      };
      
    default:
      return state;
  }
}

Redux Core Concepts

graph LR A[User Interface] -->|Dispatches| B[Actions] B -->|Processed by| C[Reducers] C -->|Updates| D[Store] D -->|Notifies| A style A fill:#9af style B fill:#f96 style C fill:#bd6 style D fill:#9cf classDef default stroke:#333,stroke-width:2px;

Actions

Actions are payloads of information that send data from your application to your store. They are the only source of information for the store.

Key characteristics of actions:

Action Creators

Action creators are functions that create and return action objects. They encapsulate the action creation logic and make dispatching actions more convenient.

// Action creator
function addToCart(product, quantity = 1) {
  return {
    type: 'ADD_TO_CART',
    payload: {
      productId: product.id,
      name: product.name,
      price: product.price,
      quantity
    }
  };
}

// Usage
dispatch(addToCart(product, 2));

Reducers

Reducers specify how the application's state changes in response to actions. A reducer is a pure function that takes the previous state and an action, and returns the next state.

Key characteristics of reducers:

Store

The Redux store is the object that brings actions and reducers together. The store has the following responsibilities:

import { createStore } from 'redux';
import rootReducer from './reducers';

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

// Log the initial state
console.log(store.getState());

// Subscribe to state changes
const unsubscribe = store.subscribe(() => {
  console.log('State updated:', store.getState());
});

// Dispatch actions
store.dispatch(addToCart(product));
store.dispatch(updateUserProfile(userInfo));

// Unsubscribe to stop listening for state updates
unsubscribe();

Redux Data Flow

Redux follows a strict unidirectional data flow, making state changes predictable and traceable:

  1. Event occurs in the UI (button click, form submission, API response)
  2. Action is dispatched to describe what happened
  3. Reducer processes the action and creates a new state
  4. Store updates the state and notifies all listeners
  5. UI updates to reflect the new state
sequenceDiagram participant UI as User Interface participant AC as Action Creator participant D as Dispatch participant R as Reducer participant S as Store UI->>AC: Event occurs (click, input, etc.) AC->>D: Create and dispatch action D->>R: Pass action to reducer R->>S: Compute and return new state S->>UI: Notify UI of state update UI->>UI: Re-render with new state

This unidirectional flow has several advantages:

Real-world analogy: Think of Redux's data flow like an efficient restaurant. The customer (UI) places an order (action), the waiter (dispatcher) takes it to the kitchen, the chef (reducer) prepares the meal according to the recipe, the completed dish (new state) is delivered back to the customer, and the customer enjoys their meal (UI updates).

Setting Up a Basic Redux Store

Let's walk through setting up a basic Redux store for a counter application:

Step 1: Define Action Types

// actionTypes.js
export const INCREMENT = 'INCREMENT';
export const DECREMENT = 'DECREMENT';
export const RESET = 'RESET';
export const SET_VALUE = 'SET_VALUE';

Step 2: Create Action Creators

// actions.js
import { INCREMENT, DECREMENT, RESET, SET_VALUE } from './actionTypes';

export const increment = () => ({
  type: INCREMENT
});

export const decrement = () => ({
  type: DECREMENT
});

export const reset = () => ({
  type: RESET
});

export const setValue = (value) => ({
  type: SET_VALUE,
  payload: value
});

Step 3: Create Reducer

// reducer.js
import { INCREMENT, DECREMENT, RESET, SET_VALUE } from './actionTypes';

const initialState = {
  count: 0
};

function counterReducer(state = initialState, action) {
  switch (action.type) {
    case INCREMENT:
      return {
        ...state,
        count: state.count + 1
      };
      
    case DECREMENT:
      return {
        ...state,
        count: state.count - 1
      };
      
    case RESET:
      return {
        ...state,
        count: 0
      };
      
    case SET_VALUE:
      return {
        ...state,
        count: action.payload
      };
      
    default:
      return state;
  }
}

export default counterReducer;

Step 4: Create and Configure the Store

// store.js
import { createStore } from 'redux';
import counterReducer from './reducer';

const store = createStore(
  counterReducer,
  // Optional: Add Redux DevTools Extension support
  window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);

export default store;

Step 5: Use the Store

// index.js
import store from './store';
import { increment, decrement, reset, setValue } from './actions';

// Log initial state
console.log('Initial state:', store.getState());

// Subscribe to state changes
const unsubscribe = store.subscribe(() => {
  console.log('Updated state:', store.getState());
});

// Dispatch actions
store.dispatch(increment());
// => { count: 1 }

store.dispatch(increment());
// => { count: 2 }

store.dispatch(decrement());
// => { count: 1 }

store.dispatch(setValue(100));
// => { count: 100 }

store.dispatch(reset());
// => { count: 0 }

// Stop listening to state updates
unsubscribe();

Practice Activity

Build a Todo List with Redux

Create a simple todo list application using Redux. The application should support:

  1. Adding new todos
  2. Toggling todo completion status
  3. Filtering todos (All, Active, Completed)
  4. Deleting todos

Follow these steps:

  1. Define action types (ADD_TODO, TOGGLE_TODO, DELETE_TODO, SET_FILTER)
  2. Create action creators for each action type
  3. Create reducers for todos and filter state
  4. Combine reducers using combineReducers
  5. Set up the Redux store
  6. Test your store by dispatching actions and logging the state

Start with this skeleton code:

// actionTypes.js
export const ADD_TODO = 'ADD_TODO';
export const TOGGLE_TODO = 'TOGGLE_TODO';
export const DELETE_TODO = 'DELETE_TODO';
export const SET_FILTER = 'SET_FILTER';

// Define filter constants
export const FILTERS = {
  ALL: 'ALL',
  ACTIVE: 'ACTIVE',
  COMPLETED: 'COMPLETED'
};

// actions.js
// TODO: Create action creators

// reducers.js
// TODO: Create and combine reducers

// store.js
// TODO: Create and configure the store

Key Takeaways

Additional Resources