Redux in React

Redux is a predictable state container for JavaScript applications, commonly used with React. It helps you write applications that behave consistently and run in different environments.

Core Concepts

Redux is built on three fundamental principles:

  1. Single Source of Truth: The state of your whole application is stored in an object tree within a single store.
  2. State is Read-only: The only way to change the state is to emit an action, an object describing what happened.
  3. Changes are made with pure functions: To specify how the state tree is transformed by actions, you write pure reducers.

Basic Redux Setup

// Action Types
const ADD_TODO = "ADD_TODO";
const TOGGLE_TODO = "TOGGLE_TODO";
// Action Creators
function addTodo(text) {
return { type: ADD_TODO, text };
}
function toggleTodo(index) {
return { type: TOGGLE_TODO, index };
}
// Reducer
function todos(state = [], action) {
switch (action.type) {
case ADD_TODO:
return [
...state,
{
text: action.text,
completed: false,
},
];
case TOGGLE_TODO:
return state.map((todo, index) => {
if (index === action.index) {
return { ...todo, completed: !todo.completed };
}
return todo;
});
default:
return state;
}
}
// Store
const store = createStore(todos);

Using Redux with React

// Provider
import { Provider } from "react-redux";
function App() {
return (
<Provider store={store}>
<TodoApp />
</Provider>
);
}
// Connecting Components
import { connect } from "react-redux";
function TodoList({ todos, toggleTodo }) {
return (
<ul>
{todos.map((todo, index) => (
<li
key={index}
onClick={() => toggleTodo(index)}
style={{ textDecoration: todo.completed ? "line-through" : "none" }}
>
{todo.text}
</li>
))}
</ul>
);
}
function mapStateToProps(state) {
return { todos: state };
}
function mapDispatchToProps(dispatch) {
return {
toggleTodo: (index) => dispatch(toggleTodo(index)),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(TodoList);

Redux Toolkit

Redux Toolkit is the official, opinionated, batteries-included toolset for efficient Redux development.

import { createSlice, configureStore } from "@reduxjs/toolkit";
// Slice
const todosSlice = createSlice({
name: "todos",
initialState: [],
reducers: {
addTodo: (state, action) => {
state.push({ text: action.payload, completed: false });
},
toggleTodo: (state, action) => {
const todo = state[action.payload];
if (todo) {
todo.completed = !todo.completed;
}
},
},
});
export const { addTodo, toggleTodo } = todosSlice.actions;
// Store
const store = configureStore({
reducer: {
todos: todosSlice.reducer,
},
});

Using Redux Toolkit with React Hooks

import { useSelector, useDispatch } from "react-redux";
import { addTodo, toggleTodo } from "./todosSlice";
function TodoApp() {
const todos = useSelector((state) => state.todos);
const dispatch = useDispatch();
const [text, setText] = useState("");
const handleSubmit = (e) => {
e.preventDefault();
if (text.trim()) {
dispatch(addTodo(text));
setText("");
}
};
return (
<div>
<form onSubmit={handleSubmit}>
<input
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="Add a todo"
/>
<button type="submit">Add Todo</button>
</form>
<ul>
{todos.map((todo, index) => (
<li
key={index}
onClick={() => dispatch(toggleTodo(index))}
style={{ textDecoration: todo.completed ? "line-through" : "none" }}
>
{todo.text}
</li>
))}
</ul>
</div>
);
}

Middleware

Redux middleware provides a third-party extension point between dispatching an action and the moment it reaches the reducer.

// Logger Middleware
const logger = (store) => (next) => (action) => {
console.group(action.type);
console.info("dispatching", action);
const result = next(action);
console.log("next state", store.getState());
console.groupEnd();
return result;
};
// Apply Middleware
const store = createStore(rootReducer, applyMiddleware(logger, thunk));

Best Practices

  1. Normalize State Shape: Keep your state normalized to avoid redundancy and simplify updates.
  2. Use Selectors: Use selectors to compute derived data and encapsulate state structure.
  3. Keep Actions Atomic: Each action should represent a single change to the state.
  4. Use Redux Toolkit: Redux Toolkit simplifies Redux setup and reduces boilerplate.
  5. Avoid Deeply Nested State: Flatten your state structure when possible.
  6. Use Immutable Update Patterns: Always update state immutably.

When to Use Redux

Redux is most useful when:

  • You have large amounts of application state that many components need to access
  • The state updates frequently
  • The logic to update the state is complex
  • You need to track state changes for debugging

For simpler applications, consider using React’s built-in useState and useContext hooks.

Conclusion

Redux provides a robust solution for managing complex state in React applications. While it has a learning curve, its predictable state updates and powerful developer tools make it a valuable tool for large-scale applications.

Share & Connect