Redux is a popular state management library that helps you manage the state of your JavaScript applications in a predictable and centralized way. It provides a clear structure for managing state, making data flow through your application easier to understand. Redux is also small, about 2kb so it won’t significantly increase your build size.
In this topic, you'll learn about the core concepts of Redux, including actions, reducers, and store. You'll also discover how to integrate Redux with React, implement advanced techniques, and follow best practices for structuring Redux code.
Redux core concepts
At the heart of Redux are three fundamental concepts: actions, reducers, and store. Let's dive into how these work together to manage application state.
Actions are plain JavaScript objects that describe events in your application. They are the only way to trigger state changes in Redux and must have a type property indicating the type of action being performed.
An action can also have additional fields that describe the event. By convention, these are put under the payload field.
Here is an example of an action:
const addTodo = {
type: 'ADD_TODO',
payload: {
id: 1,
text: 'Learn Redux'
}
}Reducers are pure functions that define how the state changes in response to actions.
A pure function always produces the same output given the same input and does not modify external state or cause side effects. Reducers take the current state and an action as input and return a new state. The action is used to determine how to calculate the new state. Instead of modifying the current state directly, reducers return a new state object.
Here's an example of a reducer that is used to manage state of an array:
const todoReducer = (state = { list: [] }, action) => {
switch (action.type) {
case 'ADD_TODO':
// Adds a new todo item to the list in the state
return { list: [...state.list, action.payload] };
default:
// Returns the current state if no action type matches
return state;
}
};The todoReducer function is a Redux reducer that handles state updates for a list of to-do items. When an action with type 'ADD_TODO' is dispatched, it adds a new item to the list in the state by creating a new state object with the updated list. If the action type does not match, it returns the unchanged state.
The store is the object that holds the application's state and provides methods to access and update it. The store is created by passing a reducer to the createStore function from the Redux library. The function takes a reducer object as a parameter. The store has methods like getState() to retrieve the current state, dispatch(action) to dispatch actions, and subscribe(listener) to register listeners for state changes.
Example:
const store = createStore(reducer)
// To retrieve state
let state = store.getState()
// To dispatch an event
store.dispatch({ type: 'INCREMENT' })The only way to change the state is to call the dispatch method and pass an action to it. The store will run the reducer and save the new state. We can then run a method like getState() to get the new state value.
Integrating Redux with React
To use Redux with React, you need to set up a Redux store and connect it to your React components. The react-redux library provides a convenient way to do this. Here is an example that uses Redux to keep track of a to-do list.
First, we need to install the required packages. For this example, we can use Redux Toolkit since it's built on top of Redux but with a better developer experience. We will also install a React plugin for Redux.
npm i @reduxjs/toolkit react-reduxBefore writing code, it’s a good idea to design your application's state structure and define action types as constants, such as 'ADD_TODO' and 'REMOVE_TODO'.
To create a reducer, use the createSlice function from @reduxjs/toolkit to generate a slice. The slice is an object that contains a reducer.
Example:
import { createSlice } from "@reduxjs/toolkit"
const todoSclice = createSlice({
name: 'todo',
initialState: {
list: []
},
reducers: {
// Increment reducer
addToTodo: (state, item) => {
let newTodo = [...state.list, item]
return newTodo
},
removeFromTodo: (state, item) => {
let newTodo = state.filter(todo => JSON.stringify(item) === JSON.stringify(todo))
return newTodo
}
});
export default todoSlice.reducer;Redux toolkit allows us to write mutating logic in the reducer. It doesn't mutate the state but returns a new immutable state.
increment: (state) => {
state.value += 1
},Next, create a Redux store using the createStore function and pass your root reducer to it. The function also takes two additional optional parameters: initialState and an enhancer. The enhancer is a higher-order function that takes storeCreator and returns a new, enhanced version of it.
To read more about enhancers, visit here.
Next, wrap your React application with the Provider component from react-redux and pass the store as a prop:
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import rootReducer from './reducers';
const store = createStore(rootReducer);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);To connect a React component to the Redux store, use the connect function from react-redux. It allows you to specify which parts of the state your component needs access to (mapStateToProps) and which actions it can dispatch (mapDispatchToProps). The example below connects a component called TodoList to the to-do list state we mentioned before.
import { connect } from 'react-redux';
const TodoList = ({ todos, addTodo }) => {
return (
<div>
<ul>
{todos.map(item => {
// Loop to show data
})}
</ul>
<form>
// Input
<button onClick={addTodo}>Add</button>
</form>
</div>
)
}
const mapStateToProps = (state) => ({
todos: state.todos
});
const mapDispatchToProps = (dispatch) => ({
addTodo: (text) => dispatch({ type: 'ADD_TODO', payload: text })
});
export default connect(mapStateToProps, mapDispatchToProps)(TodoList);Advanced Redux techniques
Redux offers several advanced techniques to enhance state management for your application:
Middleware:
Middleware allows you to intercept, modify, or enhance actions before they reach the reducer. It's useful for logging, handling asynchronous actions, or modifying the action payload. Here's an example of a simple logging middleware:
const loggingMiddleware = (store) => (next) => (action) => {
console.log('Action:', action);
return next(action);
};To use middleware, call the applyMiddleware function from Redux and pass the middleware to it. The function can accept multiple arguments.
const logger = applyMiddleware(loggerMiddleware)Here's an example:
const store = createStore(reducer, applyMiddleware(logger, delay))
// Additional code
store.dispatch({ type: "INCREMENT" })When dispatching an event, middleware functions are executed before the event. In this sense, they function similarly to middleware in Express.
Asynchronous Actions:
Redux Thunk is a popular middleware that enables you to write action creators that return functions instead of objects. This allows you to perform asynchronous operations, such as making API requests, and dispatch actions based on the results.
To do this, you must first install Redux Thunk:
npm i redux-thunkHere's an example of an asynchronous action using Redux Thunk that fetches to-dos from an API:
const fetchTodos = () => {
return (dispatch) => {
dispatch({ type: 'FETCH_TODOS_REQUEST' });
fetch('https://api.example.com/todos')
.then((response) => response.json())
.then((data) => {
dispatch({ type: 'FETCH_TODOS_SUCCESS', payload: data });
})
.catch((error) => {
dispatch({ type: 'FETCH_TODOS_FAILURE', error });
});
};
};Selectors:
Selectors are functions that extract specific pieces of state from the Redux store. They help optimize performance by memoizing results, recomputing them only when relevant parts of the state change. Selectors can be used with libraries like Reselect to create efficient and composable state selectors.
import { createSelector } from 'reselect';
const getTodos = (state) => state.todos;
const getCompletedTodos = createSelector(
getTodos,
(todos) => todos.filter((todo) => todo.completed)
);Redux best practices
When working with Redux, it's important to follow best practices to keep your code maintainable and scalable:
Normalize your state: Store data in a flat, keyed structure instead of nested objects. This helps avoid duplication, makes updates more efficient, and simplifies data access. Libraries like
normalizercan assist with this.Use action constants: Define action types as constants to avoid typos and improve readability.
Keep reducers pure: Ensure reducers don't modify the state directly. Mutating state is one of the leading causes of Redux bugs.
Use selectors: Utilize selectors to compute derived data and keep components efficient.
Organize your files: Structure your Redux files in a logical manner, separating actions, reducers, and selectors.
Conclusion
By understanding the core concepts of Redux—actions, reducers, and store—you can effectively manage the state of your application. Integrating Redux with React allows you to connect components to the Redux store and handle data flow efficiently. Advanced techniques like middleware, asynchronous actions, and selectors further enhance the capabilities of Redux.
By following best practices, keeping your code organized, and using the tools in the Redux ecosystem, you can build scalable and maintainable applications that are easier to understand and debug.
Now, put your Redux knowledge into practice and start building amazing applications!