Introduction to Redux Actions and Reducers
Redux is a state management library commonly used in React applications. It helps in managing the application state in a predictable way. Redux actions and reducers are two fundamental concepts in Redux.
Redux Actions
Actions in Redux are plain JavaScript objects that represent an intention to change the state. They describe what happened in the application and include information necessary for the state change. Actions have a type
property that specifies the type of action being performed.
Here's an example of a Redux action that updates a user's name:
1const updateUser = (name) => {
2 return {
3 type: 'UPDATE_USER',
4 payload: name
5 };
6};
In the above example, the UPDATE_USER
action is defined with a payload
property that contains the new name.
Redux Reducers
Reducers are pure functions in Redux that specify how the application's state should change in response to actions. A reducer takes the current state and an action as arguments and returns the new state.
Here's an example of a Redux reducer that handles the UPDATE_USER
action:
1const initialState = {
2 user: {
3 name: ''
4 }
5};
6
7const userReducer = (state = initialState, action) => {
8 switch (action.type) {
9 case 'UPDATE_USER':
10 return {
11 ...state,
12 user: {
13 name: action.payload
14 }
15 };
16 default:
17 return state;
18 }
19};
In the above example, the user
reducer handles the UPDATE_USER
action by updating the name
property of the user object in the state.
Redux actions and reducers work together to manage the state of a Redux application. Actions represent the intention to change the state, while reducers implement the logic to update the state based on the actions.
Understanding Redux actions and reducers is crucial for developing applications using Redux. In the next sections, we'll dive deeper into how to define actions and implement reducers in Redux.
Let's test your knowledge. Is this statement true or false?
Redux actions describe what happened in the application and include information necessary for the state change.
Press true if you believe the statement is correct, or false otherwise.
Defining Actions
In Redux, actions are plain JavaScript objects that describe an intention to change the state. They represent an event or an action that has occurred in the application. Actions have a type
property that indicates the type of action being performed. Additional properties, such as payload
, can be included to provide additional data for the action.
Actions can be defined as functions that return an object. The object contains the type
property and any additional properties required for the action. Here's an example of a Redux action that updates the user's name:
1const updateUser = (name) => {
2 return {
3 type: 'UPDATE_USER',
4 payload: name
5 };
6};
In the above example, the UPDATE_USER
action is defined with a payload
property that contains the new name.
Actions can also be asynchronous by using middleware like Redux Thunk. This allows actions to dispatch other actions or perform asynchronous operations. Here's an example of an asynchronous action that fetches user data from an API:
1const fetchUser = () => {
2 return async (dispatch) => {
3 dispatch({ type: 'FETCH_USER_REQUEST' });
4
5 try {
6 const response = await fetch('/api/user');
7 const data = await response.json();
8
9 dispatch({ type: 'FETCH_USER_SUCCESS', payload: data });
10 } catch (error) {
11 dispatch({ type: 'FETCH_USER_FAILURE', payload: error.message });
12 }
13 };
14};
In the above example, the fetchUser
action is defined as an asynchronous function that dispatches different actions based on the API response.
Defining actions is an important part of Redux as they determine how the state will be updated. Actions should capture the intention of the application and provide the necessary information to perform the state update. The actions can then be dispatched using the Redux store to trigger the corresponding reducers and update the state accordingly.
Think of actions like the instructions for a recipe. They specify the steps to be followed to achieve the desired outcome. In Redux, actions serve a similar purpose by describing the steps needed to update the state.
Now that we understand how to define actions, let's move on to implementing reducers to handle these actions.
xxxxxxxxxx
// Let's define an action to update the user's name
const updateUser = (name) => {
return {
type: 'UPDATE_USER',
payload: name
};
};
// Create an action to fetch user data
const fetchUser = () => {
return async (dispatch) => {
dispatch({ type: 'FETCH_USER_REQUEST' });
try {
const response = await fetch('/api/user');
const data = await response.json();
dispatch({ type: 'FETCH_USER_SUCCESS', payload: data });
} catch (error) {
dispatch({ type: 'FETCH_USER_FAILURE', payload: error.message });
}
};
};
Build your intuition. Is this statement true or false?
Actions in Redux are plain JavaScript objects that describe an intention to change the state.
Press true if you believe the statement is correct, or false otherwise.
Implementing Reducers
Reducers are functions that specify how the state should be updated when an action is dispatched. In Redux, reducers have a very specific signature. They accept two arguments: the current state and the action being dispatched.
Here's an example of a basic reducer that handles the state changes for a TODO application:
1const initialState = {
2 todos: [],
3 filter: 'all'
4};
5
6const todoReducer = (state = initialState, action) => {
7 switch (action.type) {
8 case 'ADD_TODO': {
9 const { id, text } = action.payload;
10 const newTodo = { id, text, completed: false };
11 return {
12 ...state,
13 todos: [...state.todos, newTodo]
14 };
15 }
16 case 'TOGGLE_TODO': {
17 const { id } = action.payload;
18 const updatedTodos = state.todos.map(todo =>
19 todo.id === id ? { ...todo, completed: !todo.completed } : todo
20 );
21 return {
22 ...state,
23 todos: updatedTodos
24 };
25 }
26 case 'SET_FILTER': {
27 const { filter } = action.payload;
28 return {
29 ...state,
30 filter
31 };
32 }
33 default:
34 return state;
35 }
36};
In the example above, the todoReducer
function handles the state updates for a TODO application. It accepts the current state and the action as arguments. Depending on the action type, it performs the necessary state changes and returns a new state object.
Reducers are pure functions, which means they should always return a new state object and not modify the existing state. This ensures that the state changes are predictable and easier to debug.
Feel free to test the above reducer by executing the code snippet in your JavaScript environment.
xxxxxxxxxx
console.log(todoReducer(initialState, { type: 'ADD_TODO', payload: { id: 1, text: 'Buy groceries' } }));
// Replace this code with your implementation of a Redux reducer
const initialState = {
todos: [],
filter: 'all'
};
const todoReducer = (state = initialState, action) => {
switch (action.type) {
case 'ADD_TODO': {
const { id, text } = action.payload;
const newTodo = { id, text, completed: false };
return {
state,
todos: [state.todos, newTodo]
};
}
case 'TOGGLE_TODO': {
const { id } = action.payload;
const updatedTodos = state.todos.map(todo =>
todo.id === id ? { todo, completed: !todo.completed } : todo
);
return {
state,
todos: updatedTodos
};
}
case 'SET_FILTER': {
const { filter } = action.payload;
Let's test your knowledge. Fill in the missing part by typing it in.
Reducers are ___ functions that specify how the state should be updated when an action is dispatched.
Write the missing line below.
Combining Reducers
In larger Redux applications, it's common to have multiple reducers that manage different parts of the application state. For example, you might have a separate reducer for managing the state of a movie collection and another reducer for managing the state of a user profile.
To combine these reducers, Redux provides the combineReducers
function. This function creates a higher-order reducer that internally calls the individual reducers and combines their state into a single state object.
Here's an example of how to use combineReducers
to combine the movieReducer
and userReducer
:
1const movieReducer = (state = [], action) => {
2 // handle movie-related actions
3};
4
5const userReducer = (state = {}, action) => {
6 // handle user-related actions
7};
8
9import { combineReducers } from 'redux';
10
11const rootReducer = combineReducers({
12 movies: movieReducer,
13 user: userReducer
14});
xxxxxxxxxx
// Output: {}
// Let's say we have two separate reducers for managing the state of a movie collection and a user profile
const movieReducer = (state = [], action) => {
// handle movie-related actions
};
const userReducer = (state = {}, action) => {
// handle user-related actions
};
// To combine these two reducers, we can use the `combineReducers` function provided by Redux
import { combineReducers } from 'redux';
const rootReducer = combineReducers({
movies: movieReducer,
user: userReducer
});
// The `combineReducers` function creates a higher-order reducer that internally calls `movieReducer` and `userReducer`
// The created rootReducer can now be used to create the Redux store
import { createStore } from 'redux';
const store = createStore(rootReducer);
console.log(store.getState());
// Output: { movies: [], user: {} }
Build your intuition. Fill in the missing part by typing it in.
In Redux, the combineReducers
function is used to combine multiple ____ into a single reducer.
Write the missing line below.
Middleware and Thunk
Middleware is a crucial concept in Redux that allows you to add extra functionality to the standard dispatch process. It sits between the dispatched action and the reducer, enabling you to modify or intercept actions.
In the Redux ecosystem, thunk middleware is commonly used to handle asynchronous logic. Thunk middleware allows you to write action creators that return functions instead of plain action objects. These functions can then dispatch multiple actions and perform side effects such as making API calls.
To use thunk middleware in your Redux application, you need to install the redux-thunk
package and include it when creating your Redux store.
Here's an example of how to use thunk middleware:
1import { createStore, applyMiddleware } from 'redux';
2import thunk from 'redux-thunk';
3import rootReducer from './reducers';
4
5const store = createStore(rootReducer, applyMiddleware(thunk));
With thunk middleware, you can write action creators that return functions by utilizing the dispatch
function provided as an argument. Inside the function, you can perform asynchronous logic and dispatch additional actions when necessary.
1export const fetchUser = (userId) => {
2 return (dispatch) => {
3 dispatch({ type: 'FETCH_USER_REQUEST' });
4 api.getUser(userId)
5 .then((user) => {
6 dispatch({ type: 'FETCH_USER_SUCCESS', payload: user });
7 })
8 .catch((error) => {
9 dispatch({ type: 'FETCH_USER_FAILURE', payload: error });
10 });
11 };
12};
The fetchUser
action creator returns a function that receives the dispatch
argument. Inside the function, you can make the API call to fetch the user data and dispatch different actions to represent different states of the asynchronous operation.
With the help of thunk middleware, you can handle complex asynchronous logic in a structured and flexible way, keeping your Redux reducers pure and focused on state management. Thunk is just one example of middleware that Redux offers, and there are several other middleware options available to suit different needs.
xxxxxxxxxx
// Example code
const fetchUser = (userId) => {
return (dispatch) => {
dispatch({ type: 'FETCH_USER_REQUEST' });
api.getUser(userId)
.then((user) => {
dispatch({ type: 'FETCH_USER_SUCCESS', payload: user });
})
.catch((error) => {
dispatch({ type: 'FETCH_USER_FAILURE', payload: error });
});
};
};
Try this exercise. Fill in the missing part by typing it in.
Thunk middleware allows you to write action creators that return __ instead of plain action objects.
Write the missing line below.
Working with Async Actions
When working with Redux, it's common to encounter scenarios where you need to handle asynchronous actions, such as making API calls or performing database operations. In such cases, Redux provides a way to deal with async actions and update the state accordingly.
To work with async actions in Redux, you can use middleware like redux-thunk
or redux-saga
. These middleware allow you to write action creators that return functions instead of plain objects.
Here's an example of an action creator that makes an API call to fetch a user's data:
1const fetchUser = async (userId) => {
2 try {
3 const response = await axios.get(`/users/${userId}`);
4 console.log(response.data);
5 } catch (error) {
6 console.error(error);
7 }
8}
9
10fetchUser(123);
In this example, we're using the axios
library to make an HTTP GET request to fetch user data. The await
keyword is used to wait for the API call to complete and the response is logged to the console. If an error occurs, it will be caught and logged as well.
By using async actions, you can handle asynchronous operations in a structured and manageable way, ensuring that the state is updated correctly based on the async results. This allows you to create more interactive and dynamic applications with Redux.
xxxxxxxxxx
`const axios = require('axios');`
`const fetchUser = async (userId) => {`
`try {`
`const response = await axios.get(`/users/${userId}`);`
`console.log(response.data);`
`} catch (error) {`
`console.error(error);`
`}`
`}`
`fetchUser(123);`
Try this exercise. Fill in the missing part by typing it in.
Async actions in Redux can be handled using middleware like _____________
. This middleware allows you to write action creators that return ____ instead of plain objects. By using this middleware, you can perform asynchronous operations and update the state accordingly based on the results. For example, you can make API calls or perform database operations and dispatch actions based on the success or failure of these operations. This allows you to create more interactive and dynamic applications with Redux.
Write the missing line below.
Writing Tests for Redux Actions and Reducers
When working with Redux actions and reducers, it's important to write tests to ensure that they are functioning correctly. Testing your Redux code helps to catch any bugs or issues early on, and provides confidence that your code is working as expected.
Testing Actions
To test Redux actions, you can create test cases that verify the action creators return the expected action objects. You can use assertion libraries like chai
or the built-in assert
module in Node.js to compare the expected and actual actions.
Here's an example of testing an action creator addUser
that adds a user to the state:
1const addUser = (user) => {
2 return { type: 'ADD_USER', payload: user };
3};
4
5const user = { id: 1, name: 'John Doe' };
6const expectedAction = { type: 'ADD_USER', payload: user };
7
8const actualAction = addUser(user);
9
10// Assert that the expected action matches the actual action
11assert.deepEqual(expectedAction, actualAction);
By writing tests for your action creators, you can ensure they are returning the correct action objects with the expected properties and values.
Testing Reducers
To test Redux reducers, you can create test cases that simulate different actions being dispatched and verify that the state is updated correctly. You can also test the initial state of the reducer and verify that it returns the expected default state.
Here's an example of testing a reducer userReducer
that handles the 'ADD_USER' action:
1const userReducer = (state = [], action) => {
2 switch (action.type) {
3 case 'ADD_USER':
4 return [...state, action.payload];
5 default:
6 return state;
7 }
8};
9
10const initialState = [];
11
12const addUserAction = { type: 'ADD_USER', payload: { id: 1, name: 'John Doe' } };
13
14const nextState = userReducer(initialState, addUserAction);
15
16// Assert that the state has been updated correctly
17assert.deepEqual(nextState, [{ id: 1, name: 'John Doe' }]);
xxxxxxxxxx
testAddUser();
const addUser = (user) => {
return { type: 'ADD_USER', payload: user };
};
const userReducer = (state = [], action) => {
switch (action.type) {
case 'ADD_USER':
return [state, action.payload];
default:
return state;
}
};
// Test the addUser action creator
const testAddUser = () => {
const user = { id: 1, name: 'John Doe' };
const expectedAction = { type: 'ADD_USER', payload: user };
const actualAction = addUser(user);
console.log(expectedAction);
console.log(actualAction);
// Assert that the expected action matches the actual action
if (JSON.stringify(expectedAction) === JSON.stringify(actualAction)) {
console.log('Test passed!');
} else {
console.log('Test failed!');
}
Are you sure you're getting this? Click the correct answer from the options.
Which of the following is a best practice for testing Redux actions and reducers?
Click the option that best answers the question.
Best Practices for Working with Redux Actions and Reducers
When working with Redux actions and reducers, it's important to follow best practices to ensure clean and maintainable code. In this section, we will explore some recommended best practices for working with Redux actions and reducers.
1. Organizing Actions
To keep your actions organized and easily manageable, it's a good practice to store them in a separate file or folder. This makes it easier to locate and update actions when needed. You can group related actions together, such as all user-related actions in one file.
Here's an example of organizing actions in a separate file:
1// actions.js
2
3export const ADD_USER = 'ADD_USER';
4export const REMOVE_USER = 'REMOVE_USER';
5export const UPDATE_USER = 'UPDATE_USER';
6
7export function addUser(user) {
8 return {
9 type: ADD_USER,
10 payload: user,
11 };
12}
13
14// ... other action creators
2. Using Constants for Action Types
To avoid typos and improve code readability, it's recommended to use constants for action types instead of hardcoding them. By defining action type constants, you can easily reference them and avoid spelling mistakes.
Here's an example of using constants for action types:
1// actionTypes.js
2
3export const ADD_USER = 'ADD_USER';
4export const REMOVE_USER = 'REMOVE_USER';
5export const UPDATE_USER = 'UPDATE_USER';
3. Handling Asynchronous Actions
When working with asynchronous actions, such as API calls, it's important to handle them properly. Redux provides middleware like redux-thunk
or redux-saga
to handle asynchronous actions. It's recommended to use these middleware libraries to manage async actions and keep the action creators clean and focused on the business logic.
Here's an example of using redux-thunk
middleware to handle asynchronous actions:
1// userActions.js
2
3import { FETCH_USERS_SUCCESS, FETCH_USERS_FAILURE } from './actionTypes';
4
5export function fetchUsers() {
6 return async (dispatch) => {
7 try {
8 const response = await fetch('/api/users');
9 const users = await response.json();
10 dispatch({
11 type: FETCH_USERS_SUCCESS,
12 payload: users,
13 });
14 } catch (error) {
15 dispatch({
16 type: FETCH_USERS_FAILURE,
17 error: error.message,
18 });
19 }
20 };
21}
4. Using Selectors
Selectors are functions that extract specific portions of the state from the Redux store. They help decouple components from the shape of the state and provide a centralized location to access specific portions of the state.
Here's an example of using selectors to extract user-related data from the state:
1// selectors.js
2
3export function getUsers(state) {
4 return state.users;
5}
6
7export function getUserById(state, userId) {
8 return state.users.find((user) => user.id === userId);
9}
5. Testing Actions and Reducers
Writing tests for Redux actions and reducers is important to ensure they are working correctly. Use testing frameworks like Jest
or Mocha
along with assertion libraries like chai
or expect
to write tests for action creators and reducers. Test the expected behavior for different types of actions and validate the resulting state changes.
Here's an example of testing an action creator addUser
using Jest
:
1// userActions.test.js
2
3import { addUser } from './userActions';
4
5describe('userActions', () => {
6 test('addUser should create ADD_USER action', () => {
7 const user = { id: 1, name: 'John Doe' };
8 const expectedAction = {
9 type: ADD_USER,
10 payload: user,
11 };
12
13 const action = addUser(user);
14
15 expect(action).toEqual(expectedAction);
16 });
17});
These are just a few best practices to keep in mind when working with Redux actions and reducers. Following these practices can help improve code organization, maintainability, and testability.
xxxxxxxxxx
// Replace with relevant code for best practices
Build your intuition. Click the correct answer from the options.
Which of the following is NOT a best practice for working with Redux actions and reducers?
Click the option that best answers the question.
- Using constants for action types
- Storing actions in a separate file or folder
- Handling asynchronous actions with middleware
- Mutating the state directly
Generating complete for this lesson!