Key Points
This article was originally published in Codebrahma.
JavaScript is a single-threaded programming language. That is to say, when you write the following code...
…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 获取数据。
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:
For each method, we will execute these two API calls:
userDetails
city from 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 获取数据。
. The response will be an array: /restuarants/:city
userDetails: { … city: 'city', … };
setState
redux-async, redux-promise, redux-async-queue).
PromisesPromise 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 ElliotIn 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', …]
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, // 设置状态 }) }) }) }
Disadvantages of this method
Status Management Using global storage can actually solve half of our problems in these cases. We will use Redux as our global storage.
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.
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
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?
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:
In our example, we can rewrite the action like this:
userId = fetch(userEndPoint); // 从 userEndpoint 获取 userId userDetails = fetch(userEndpoint, userId) // 为此特定 userId 获取数据。
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.
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:
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', … };
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:
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:
As stated in its documentation, "Epic is the core primitive of redux-observable" section:
For our task, we can simply write the following code:
userId = fetch(userEndPoint); // 从 userEndpoint 获取 userId userDetails = fetch(userEndpoint, userId) // 为此特定 userId 获取数据。
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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!