AlgoInsight

Q&A

How to define constants and reducers in Redux

By AlgoInsight Team2026-06-18functional-programmingstrings

Overview

The Redux data flow is: dispatch an action → a reducer produces new state → UI re-renders. Constants and reducers are the two building blocks you define for each state slice. The classic approach declares them separately; Redux Toolkit's createSlice auto-generates action creators and action types from reducer definitions, dramatically reducing boilerplate.

graph LR
    A["Dispatch action<br/>{type: ADD_TODO, payload}"] --> B["Root reducer routes by action.type"]
    B --> C["Matching case: return new state"]
    C --> D["Store updates → UI re-renders"]
    style C fill:#1e1b4b,color:#fff

Classic Approach: Explicit Constants + Reducer

Step 1: Define action type constants

// actionTypes.js
export const ADD_TODO    = "todos/ADD_TODO";
export const REMOVE_TODO = "todos/REMOVE_TODO";
export const TOGGLE_TODO = "todos/TOGGLE_TODO";

Using namespaced strings ("todos/ADD_TODO") avoids collisions when multiple slices use similar names. Constants prevent typos that would silently produce no action (a plain mistyped string causes no error at all).

Step 2: Define action creators

// actions.js
import { ADD_TODO, REMOVE_TODO, TOGGLE_TODO } from "./actionTypes";

export const addTodo    = (text) => ({ type: ADD_TODO,    payload: { text } });
export const removeTodo = (id)   => ({ type: REMOVE_TODO, payload: { id }   });
export const toggleTodo = (id)   => ({ type: TOGGLE_TODO, payload: { id }   });

Step 3: Write the reducer

A reducer is a pure function - it must not mutate state, must return a new object, and must handle every type it knows about:

// todosReducer.js
import { ADD_TODO, REMOVE_TODO, TOGGLE_TODO } from "./actionTypes";

const initialState = { items: [], nextId: 1 };

export default function todosReducer(state = initialState, action) {
  switch (action.type) {
    case ADD_TODO:
      return {
        ...state,
        items: [...state.items, { id: state.nextId, text: action.payload.text, done: false }],
        nextId: state.nextId + 1,
      };
    case REMOVE_TODO:
      return { ...state, items: state.items.filter(t => t.id !== action.payload.id) };
    case TOGGLE_TODO:
      return {
        ...state,
        items: state.items.map(t =>
          t.id === action.payload.id ? { ...t, done: !t.done } : t
        ),
      };
    default:
      return state;   // MUST return state for unknown actions
  }
}

Key rules: spread ...state to create a new object; spread ...item to create new item objects; never use state.items.push(...) - that mutates.

Modern Approach: Redux Toolkit createSlice (recommended)

createSlice generates action types, action creators, and the reducer from one declaration, and uses Immer internally so you can write "mutating" syntax safely:

import { createSlice } from "@reduxjs/toolkit";

const todosSlice = createSlice({
  name: "todos",             // prefix for generated action types: "todos/addTodo"
  initialState: { items: [], nextId: 1 },
  reducers: {
    addTodo(state, action) {
      state.items.push({ id: state.nextId++, text: action.payload, done: false });
      // Immer makes this mutation-looking code safe - it actually produces a new state
    },
    removeTodo(state, action) {
      state.items = state.items.filter(t => t.id !== action.payload);
    },
    toggleTodo(state, action) {
      const todo = state.items.find(t => t.id === action.payload);
      if (todo) todo.done = !todo.done;
    },
  },
});

export const { addTodo, removeTodo, toggleTodo } = todosSlice.actions;
export default todosSlice.reducer;

No manual constants needed - todosSlice.actions.addTodo.type is "todos/addTodo" automatically. Dispatching is unchanged: dispatch(addTodo("Buy milk")).

Combining Reducers

import { configureStore } from "@reduxjs/toolkit";
import todosReducer from "./todosSlice";
import userReducer  from "./userSlice";

const store = configureStore({
  reducer: {
    todos: todosReducer,
    user:  userReducer,
  },
});

Why This Matters in Interviews

Redux interviews test state management fundamentals. Key points for the classic approach: constants prevent typo bugs, reducers must be pure (no mutations, no side effects), always return a new state object (use spread), and always return state in the default case. For RTK: createSlice generates types/creators, Immer makes mutations safe inside the reducer, and configureStore replaces combineReducers+createStore. Demonstrating RTK shows you work with modern Redux.

Common Pitfalls

References

More answers