Q&A
How to define constants and reducers in Redux
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
- Mutating state directly (
state.items.push(...)in classic Redux without Immer) - breaks immutability and change detection. - Forgetting the
default: return statecase - causes the state to becomeundefined. - Performing side effects (API calls,
console.log) inside a reducer - violates purity. - Not namespacing action type strings - collisions across slices.
- Using
createSlicemutation syntax in classic Redux (no Immer) - that would mutate the real state.
References
- Redux fundamentals: https://redux.js.org/tutorials/fundamentals/part-2-concepts-data-flow
- Redux Toolkit
createSlice: https://redux-toolkit.js.org/api/createslice