Home > Web Front-end > JS Tutorial > Async Operations in React Redux Applications

Async Operations in React Redux Applications

Lisa Kudrow
Release: 2025-02-16 12:02:12
Original
495 people have browsed it

Async Operations in React Redux Applications

Key Points

  • The single-threaded nature of JavaScript means that asynchronous operations, such as API calls, are critical to non-blocking UIs, and efficient handling of these operations is critical in React Redux applications.
  • Redux Thunk, Redux-Saga, and Redux-Observables are popular Redux asynchronous operation management middleware, each providing different advantages based on the complexity and needs of the application.
  • Redux Thunk simplifies asynchronous action distribution by allowing return functions instead of action, enabling sequential API calls and processing related data in these functions.
  • Redux-Saga leverages ES6 generators to provide more powerful solutions for complex scenarios such as handling race conditions, pauses, and cancel operations, making it suitable for large applications.
  • Redux-Observables leverages RxJS to provide a powerful way to manage side effects through action flows, providing excellent scalability and control over asynchronous operations in complex application architectures.

This article was originally published in Codebrahma.

JavaScript is a single-threaded programming language. That is to say, when you write the following code...

Async Operations in React Redux Applications

…The second line will only be executed after the first line is executed. This won't be a problem in most cases, because the client or server performs millions of calculations per second. We only notice these effects when we perform costly calculations (a task that takes quite a while to complete – a network request takes some time to return).

Why am I only showing API calls (network requests) here? What about other asynchronous operations? API calls are a very simple and useful example to describe how to handle asynchronous operations. There are other operations, such as setTimeout(), performance-intensive computing, image loading, and any event-driven operations.

When building an application, we need to consider how asynchronous execution affects the structure. For example, think of fetch() as a function that performs an API call (network request) from the browser. (Ignore whether it is an AJAX request. Just treat its behavior as asynchronous or synchronous.) The time elapsed when the request is processed on the server does not occur on the main thread. Therefore, your JS code will continue to execute and once the request returns a response, it will update the thread.

Consider this code:

userId = fetch(userEndPoint); // 从 userEndpoint 获取 userId
userDetails = fetch(userEndpoint, userId) // 为此特定 userId 获取数据。
Copy after login
Copy after login
Copy after login
Copy after login

In this case, since fetch() is asynchronous, when we try to get userDetails we will not have userId. So we need to build it in a way that ensures that the second row is executed only after the first row returns the response.

Most modern network request implementations are asynchronous. But this doesn't always work because we rely on previous API response data for subsequent API calls. Let's see how to build it specifically in a ReactJS/Redux application.

React is a front-end library for creating user interfaces. Redux is a state container that manages the entire state of an application. Using React with Redux allows us to create efficient and scalable applications. In such a React application, there are several ways to build asynchronous operations. For each method, we will discuss its advantages and disadvantages in terms of the following factors:

  • Code clarity
  • Scalability
  • Easy to handle errors

For each method, we will execute these two API calls:

1. Get userDetailscity from

(first API response)

Suppose the endpoint is /details. It will include the city in the response. The response will be an object:

userId = fetch(userEndPoint); // 从 userEndpoint 获取 userId
userDetails = fetch(userEndpoint, userId) // 为此特定 userId 获取数据。
Copy after login
Copy after login
Copy after login
Copy after login

2. Based on the user city, we will get all restaurants in city

Suppose the endpoint is

. The response will be an array: /restuarants/:city

userDetails: {
  …
  city: 'city',
  …
};
Copy after login
Copy after login
Remember that we can only make the second request after the first request is completed (because it depends on the first request). Let's take a look at various methods:

    Use Promise or async/await directly with
  • setState
  • Using Redux Thunk
  • Using Redux-Saga
  • Using Redux Observables
I specifically chose the above methods because they are the most commonly used methods in large projects. There are still other methods that may be more specific to a specific task and do not have all the features required by a complex application (e.g.

redux-async, redux-promise, redux-async-queue).

Promises

Promise is an object that may produce a single value at some time in the future: a parsed value or an unresolved cause (for example, a network error occurred). — Eric Elliot

In our example, we will use the axios library to get the data, which returns a promise when we make a network request. The Promise may parse and return a response or throw an error. So once the

React component is mounted, we can get it directly like this:

['restaurant1', 'restaurant2', …]
Copy after login
In this way, when the state changes (due to the acquisition), the

component will automatically re-render and load the restaurant list.

Async/await is a new implementation that we can use to perform asynchronous operations. For example, the same function can be achieved by:

componentDidMount() {
  axios.get('/details') // 获取用户详细信息
    .then(response => {
      const userCity = response.city;
      axios.get(`/restaurants/${userCity}`)
        .then(restaurantResponse => {
          this.setState({
            listOfRestaurants: restaurantResponse, // 设置状态
          })
        })
    })
}
Copy after login
Both methods are the easiest. Since the whole logic is inside the component, we can easily get all the data at once after the component is loaded.

Disadvantages of this method

The problem arises when complex data-based interactions occur. For example, consider the following situation:

Async Operations in React Redux Applications

  • We do not want the thread executing JS to be blocked by network requests.
  • All the above situations make the code very complex and difficult to maintain and test.
  • Also, scalability will be a big problem because if we plan to change the flow of the application, we need to remove all the fetch operations from the component.
  • Imagine what we would do if the component was on top of the parent-child tree. Then we need to change all the representation components that depend on the data.
  • Also note that the entire business logic is inside the component.

How do we improve?

  1. Status Management Using global storage can actually solve half of our problems in these cases. We will use Redux as our global storage.

  2. Move business logic to the right place If we consider moving business logic out of the component, where exactly can we do this? In actions? In reducers? Through middleware? Redux's architecture is synchronous. Once you distribute an action (JS object) and it reaches the storage, the reducer will operate on it.

  3. Make sure there is a separate thread that executes the asynchronous code and that any changes to the global state can be retrieved by subscription

Async Operations in React Redux Applications

From this we can learn that if we move all the acquisition logic before the reducer—i.e., action or middleware—then we can distribute the correct action at the right time. For example, once the fetch starts, we can distribute dispatch({ type: 'FETCH_STARTED' }), and when it is done, we can distribute dispatch({ type: 'FETCH_SUCCESS' }).

Want to develop a React JS application?

Using Redux Thunk

Redux Thunk is a middleware for Redux. It basically allows us to return functions instead of objects as action. This helps by providing dispatch and getState as parameters of the function. We use dispatch to distribute necessary actions at the right time. The benefits are:

  • Allow multiple distributions within the function
  • The association of business logic with the acquisition will be moved from the React component to the actions.

In our example, we can rewrite the action like this:

userId = fetch(userEndPoint); // 从 userEndpoint 获取 userId
userDetails = fetch(userEndpoint, userId) // 为此特定 userId 获取数据。
Copy after login
Copy after login
Copy after login
Copy after login

As you can see, we now have a good control over what type of action is distributed. Each function call (such as fetchStarted(), fetchUserDetailsSuccess(), fetchRestaurantsSuccess(), and fetchError()) will distribute an action of type normal JavaScript object, and additional details can be added if needed. So now the task of reducer is to process each action and update the view. I didn't discuss reducer because it's simple from here and the implementation may be different.

To make this work, we need to connect the React component to Redux and bind the action to the component using the Redux library. Once done, we can simply call this.props.getRestaurants(), which in turn will handle all the above tasks and update the view according to the reducer.

For its scalability, Redux Thunk can be used for applications that do not involve complex control of asynchronous actions. Additionally, it works seamlessly with other libraries, as described in the topic in the following section.

However, it is still a bit difficult to perform certain tasks using Redux Thunk. For example, we need to pause the intermediate fetch operation, or only allow the latest calls when there are multiple such calls, or if other APIs get this data and we need to cancel.

We can still implement these, but it will be more complicated to execute accurately. Compared with other libraries, the code clarity of complex tasks will be slightly worse and will be more difficult to maintain.

Using Redux-Saga

With the Redux-Saga middleware, we can gain the additional advantage to address most of the above features. Redux-Saga is developed based on the ES6 generator.

Redux-Saga provides an API that helps achieve the following goals:

  • Blocking events, blocking threads on the same line until some operations are completed
  • Non-blocking events, making the code asynchronous
  • Handle competition between multiple asynchronous requests
  • Pause/throttling/de-bounce any action

How does Saga work?

Saga uses a combination of ES6 generator and async/await API to simplify asynchronous operations. It basically does its work on its separate threads where we can make multiple API calls. We can use their API to make each call synchronous or asynchronous, depending on the use case. The API provides the ability to make a thread wait on the same line until the request returns a response. Apart from that, this library provides many other APIs, which make API requests very easy to handle.

Consider our previous example: If we initialize a saga and configure it with Redux based on what is mentioned in its documentation, we can do the following:

userDetails: {
  …
  city: 'city',
  …
};
Copy after login
Copy after login

So if we distribute it using a simple action of type FETCH_RESTAURANTS , the Saga middleware will listen and respond. In fact, no action is used by middleware. It just listens and performs some other tasks, and distributes new actions if needed. By using this architecture, we can distribute multiple requests, each of which describes:

  • When will the first request start?
  • When will the first request be completed
  • When will the second request start?

and so on.

In addition, you can see the advantages of fetchRestaurantSaga(). We are currently using the call API to implement blocking calls. Saga provides other APIs, such as fork(), which implements non-blocking calls. We can combine blocking and non-blocking calls to maintain a structure that is suitable for our application.

With scalability, using saga is beneficial:

  • We can build and group saga based on any specific task. We can trigger a saga from another saga by simply distributing an action.
  • Since it is a middleware, the action we wrote will be a normal JS object, unlike thunk.
  • Since we put the business logic inside saga (which is a middleware), it would be much easier to understand the React part of it if we know what saga is capable of.
  • Errors can be easily monitored and distributed to storage through try/catch mode.

Using Redux-Observables

As stated in its documentation, "Epic is the core primitive of redux-observable" section:

  1. Epic is a function that receives the action stream and returns the action stream. That is, Epic runs in parallel with the normal Redux distribution channel, after the reducer has received them.
  2. action is always run through a reducer before epic receives them. Epic receives and outputs only another action stream. This is similar to Redux-Saga, because none of the actions are used by middleware. It just listens and performs some other tasks.

For our task, we can simply write the following code:

userId = fetch(userEndPoint); // 从 userEndpoint 获取 userId
userDetails = fetch(userEndpoint, userId) // 为此特定 userId 获取数据。
Copy after login
Copy after login
Copy after login
Copy after login

At first, this may seem a bit confusing. However, the more you understand RxJS, the easier it is to create an Epic.

Like saga, we can distribute multiple actions, each of which describes which part of the API request chain the thread is currently in.

In terms of scalability, we can split or combine Epic based on a specific task. Therefore, this library can help build scalable applications. If we understand the observable pattern of writing code, the code clarity is good.

My preferences

How to determine which library to use? It depends on how complex our API requests are.

How to choose between Redux-Saga and Redux-Observable? It depends on the learning generator or RxJS. Both are different concepts, but equally good enough. I suggest trying both to see which one is better for you.

Where is putting the business logic for processing APIs? It is best to put it before the reducer, but not in the component. The best way is in the middleware (using saga or observable).

You can read more React development articles at Codebrahma.

FAQs about asynchronous operations in React-Redux applications

What is the role of middleware in handling asynchronous operations in Redux?

Middleware in Redux plays a crucial role in handling asynchronous operations. It provides a third-party extension point between the distribution action and the action arrives at the reducer. Middleware can be used to record, modify, and even cancel actions, as well as to distribute other actions. In the context of asynchronous operations, middleware like Redux Thunk or Redux Saga allows you to write action creators that return functions instead of action . This function can then be used to delay the distribution of an action or to only distribute an action when a specific condition is met.

How does Redux Thunk help manage asynchronous operations?

Redux Thunk is a middleware that allows you to write action creators that return functions instead of action. thunk can be used to delay the distribution of actions or to distribute actions only when certain conditions are met. This feature makes it an excellent tool for handling asynchronous operations in Redux. For example, you can distribute an action to indicate the start of an API call, and then distribute another action when the call returns data or error message.

What is the difference between Redux Thunk and Redux Saga?

Redux Thunk and Redux Saga are both middleware for managing side effects in Redux, including asynchronous operations. The main difference between the two is their approach. Redux Thunk uses callback functions to handle asynchronous operations, while Redux Saga uses generator functions and more declarative methods. This makes Redux Saga more powerful and flexible, but also more complex. If your application has simple asynchronous operations, Redux Thunk may suffice. However, for more complex scenarios involving race conditions, cancellation, and if-else logic, Redux Saga may be a better choice.

How to handle errors in asynchronous operations in Redux?

Action can be distributed when an error occurs during an asynchronous operation to handle error handling in asynchronous operation in Redux. This action can take its error message as its payload. You can then handle this action in the reducer to update the status with the error message. This way, an error message can be displayed to the user or recorded for debugging.

How to test asynchronous action in Redux?

Asynchronous actions in Redux can be tested by mocking Redux storage and API calls. For Redux storage, you can use libraries like redux-mock-store. For API calls, you can use libraries like fetch-mock or nock. In your tests, you can distribute an asynchronous action and then assert that the expected action has been distributed with the correct payload.

How to cancel asynchronous operations in Redux?

Middleware like Redux Saga can be used to cancel asynchronous operations in Redux. Redux Saga uses generator functions, which can be cancelled using cancel effect. When cancel effect is yielded, saga will be cancelled from its startup point until the current effect is cancelled.

How to handle race conditions in asynchronous operations in Redux?

Middleware like Redux Saga can be used to handle race conditions in asynchronous operations in Redux. Redux Saga provides effects like takeLatest and takeEvery that can be used to handle concurrent actions. For example, if the previously started saga task is still running when a new action is distributed, takeLatest cancels the task.

How to use async/await with Redux Thunk?

Redux Thunk natively supports async/await. In your action creator, you can return asynchronous functions instead of regular functions. Inside this asynchronous function, you can use async/await to handle asynchronous operations. When the asynchronous operation is completed, the dispatch function can be called using the action object.

How to handle loading state in asynchronous operations in Redux?

The loading state in asynchronous operations in Redux can be handled by distributing actions before and after asynchronous operations. The action distributed before the operation can set the load status to true, and the action distributed after the operation can set it to false. In your reducer, you can handle these actions to update the load state in the storage.

How to deal with side effects in Redux?

Side effects in Redux can be handled using middleware like Redux Thunk or Redux Saga. These middleware allows you to write action creators that return functions instead of action . This function can be used to perform side effects such as asynchronous operations, logging, and conditionally distributing actions.

The above is the detailed content of Async Operations in React Redux Applications. For more information, please follow other related articles on the PHP Chinese website!

Statement of this Website
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn
Latest Articles by Author
Popular Tutorials
More>
Latest Downloads
More>
Web Effects
Website Source Code
Website Materials
Front End Template