Redux

Predictable state container with actions, reducers, and a single store

Redux - Predictable State Container

Redux is a predictable state container for JavaScript apps. It helps you write applications that behave consistently, run in different environments, and are easy to test. Redux is based on three core principles: single source of truth, state is read-only, and changes are made with pure functions.

Core Concepts

  • Store — Single object holding all application state
  • Actions — Plain objects describing what happened
  • Reducers — Pure functions that specify how state changes
  • Dispatch — Method to send actions to the store
  • Selectors — Functions to extract data from state

The Redux Flow

UI Event → dispatch(action) → Reducer → New State → UI Update

┌─────────┐     ┌─────────┐     ┌─────────┐     ┌─────────┐
│   UI    │────▶│ Action  │────▶│ Reducer │────▶│  Store  │
│ (View)  │     │ Creator │     │         │     │ (State) │
└─────────┘     └─────────┘     └─────────┘     └────┬────┘
     ▲                                               │
     └───────────────────────────────────────────────┘
                    Subscribe & Re-render

Actions

// Actions are plain objects with a type property
const addTodo = {
  type: 'todos/add',
  payload: {
    id: 1,
    text: 'Learn Redux',
    completed: false
  }
};

// Action creators - functions that return actions
function addTodo(text) {
  return {
    type: 'todos/add',
    payload: {
      id: Date.now(),
      text,
      completed: false
    }
  };
}

function toggleTodo(id) {
  return {
    type: 'todos/toggle',
    payload: { id }
  };
}

function deleteTodo(id) {
  return {
    type: 'todos/delete',
    payload: { id }
  };
}

// Action type constants (prevents typos)
const ADD_TODO = 'todos/add';
const TOGGLE_TODO = 'todos/toggle';
const DELETE_TODO = 'todos/delete';

Reducers

// Reducer is a pure function: (state, action) => newState
const initialState = {
  todos: [],
  filter: 'all'
};

function todoReducer(state = initialState, action) {
  switch (action.type) {
    case 'todos/add':
      return {
        ...state,
        todos: [...state.todos, action.payload]
      };
      
    case 'todos/toggle':
      return {
        ...state,
        todos: state.todos.map(todo =>
          todo.id === action.payload.id
            ? { ...todo, completed: !todo.completed }
            : todo
        )
      };
      
    case 'todos/delete':
      return {
        ...state,
        todos: state.todos.filter(todo => todo.id !== action.payload.id)
      };
      
    case 'filter/set':
      return {
        ...state,
        filter: action.payload
      };
      
    default:
      return state;
  }
}

// Combine multiple reducers
import { combineReducers } from 'redux';

const rootReducer = combineReducers({
  todos: todoReducer,
  user: userReducer,
  settings: settingsReducer
});

// State shape will be:
// { todos: {...}, user: {...}, settings: {...} }

Store

import { createStore } from 'redux';

// Create the store with the root reducer
const store = createStore(rootReducer);

// Get current state
console.log(store.getState());

// Dispatch actions to update state
store.dispatch(addTodo('Learn Redux'));
store.dispatch(toggleTodo(1));

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

// Later, stop listening
unsubscribe();

React-Redux Integration

import { Provider, useSelector, useDispatch } from 'react-redux';
import { createStore } from 'redux';

// 1. Create store
const store = createStore(rootReducer);

// 2. Wrap app with Provider
function App() {
  return (
    <Provider store={store}>
      <TodoApp />
    </Provider>
  );
}

// 3. Use hooks to access state and dispatch
function TodoList() {
  // Select data from the store
  const todos = useSelector(state => state.todos);
  const filter = useSelector(state => state.filter);
  
  // Get dispatch function
  const dispatch = useDispatch();
  
  const filteredTodos = todos.filter(todo => {
    if (filter === 'active') return !todo.completed;
    if (filter === 'completed') return todo.completed;
    return true;
  });
  
  return (
    <ul>
      {filteredTodos.map(todo => (
        <li 
          key={todo.id}
          onClick={() => dispatch(toggleTodo(todo.id))}
          style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
        >
          {todo.text}
          <button onClick={() => dispatch(deleteTodo(todo.id))}>
            Delete
          </button>
        </li>
      ))}
    </ul>
  );
}

function AddTodo() {
  const [text, setText] = useState('');
  const dispatch = useDispatch();
  
  const handleSubmit = (e) => {
    e.preventDefault();
    if (text.trim()) {
      dispatch(addTodo(text));
      setText('');
    }
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input 
        value={text} 
        onChange={(e) => setText(e.target.value)}
        placeholder="Add todo"
      />
      <button type="submit">Add</button>
    </form>
  );
}

Selectors

// Basic selectors
const selectTodos = state => state.todos;
const selectFilter = state => state.filter;

// Derived data selectors
const selectCompletedTodos = state => 
  state.todos.filter(todo => todo.completed);

const selectActiveTodos = state => 
  state.todos.filter(todo => !todo.completed);

const selectTodoCount = state => state.todos.length;

// Parameterized selector
const selectTodoById = (state, todoId) =>
  state.todos.find(todo => todo.id === todoId);

// Usage in components
function TodoStats() {
  const total = useSelector(selectTodoCount);
  const completed = useSelector(state => selectCompletedTodos(state).length);
  const active = useSelector(state => selectActiveTodos(state).length);
  
  return (
    <div>
      Total: {total} | Active: {active} | Completed: {completed}
    </div>
  );
}

Redux Pros & Cons

Pros Cons
Predictable state changes Lots of boilerplate code
Excellent DevTools Steep learning curve
Time-travel debugging Can be overkill for small apps
Large ecosystem & community Verbose action/reducer setup

💡 Key Takeaways

  • • Use Redux for complex apps with lots of shared state
  • • Keep reducers pure - no side effects, no mutations
  • • Use selectors to encapsulate state access
  • • Consider Redux Toolkit for modern Redux development
  • • Install Redux DevTools browser extension for debugging