Async Operations in React Redux Applications
This post was originally posted at Codebrahma.
JavaScript is a single-threaded programming language. That is, when you have code something like this …
… the second line doesn’t get executed till the first one gets completed. Mostly this won’t be a problem, since millions of calculations are performed by the client or server in a second. We notice the effects only when we’re performing a costly calculation (a task that takes noticeable time to complete — a network request which takes some time to return back).
Why did I show only an API call (network request) here? What about other async operations? An API call is a very simple and useful example for describing how to deal with an asynchronous operation. There are other operations, like setTimeout()
, performance-heavy calculations, image loading, and any event-driven operations.
While structuring our application, we need to consider how asynchronous execution impacts structuring. For example, consider fetch()
as a function that performs an API call (network request) from the browser. (Forget if it is an AJAX request. Just think of the behavior as either asynchronous or synchronous in nature.) The time elapsed while the request is processed on the server doesn’t happen on the main thread. So your JS code will keep getting executed, and once the request returns a response it will update the thread.
Consider this code:
userId = fetch(userEndPoint); // Fetch userId from the userEndpoint
userDetails = fetch(userEndpoint, userId) // Fetch for this particular userId.
In this case, since fetch()
is asynchronous, we won’t be having userId
when we try to fetch userDetails
. So we need to structure it in a way that ensures the second line executes only when the first returns a response.
Most modern implementations of network requests are asynchronous. But this doesn’t always help, since we depend on the previous API response data for the subsequent API calls. Let’s look at how particularly we can structure this in ReactJS/Redux applications.
React is a front-end library used for making user interfaces. Redux is a state container that can manage the whole state of the application. With React in combination with Redux, we can make efficient applications that scale well. There are several ways to structure async operations in such a React application. For each method, let’s discuss the pros and cons in relation to these factors:
- code clarity
- scalability
- ease of error handling.
For each method, we’ll perform these two API calls:
1. Fetching city from userDetails (First API response)
Let’s assume the endpoint is /details
. It will have the city in the response. The response will be an object:
userDetails : {
…
city: 'city',
…
};
2. Based on the user city we will fetch all restaurants in the city
Let’s say the endpoint is /restuarants/:city
. The response will be an array:
['restaurant1', 'restaurant2', …]
Remember that we can do the second request only when we finish doing the first (since it’s dependent on the first request). Let’s look at various ways to do this:
- directly using promise or async await with setState
- using Redux Thunk
- using Redux-Saga
- using Redux Observables.
Particularly I have chosen the above methods because they’re the most popularly used for a large-scale project. There are still other methods that can be more specific to particular tasks and that don’t have all the features required for a complex app (redux-async, redux-promise, redux-async-queue to name a few).
Promises
A promise is an object that may produce a single value some time in the future: either a resolved value, or a reason that it’s not resolved (e.g., a network error occurred). — Eric Elliot
In our case, we’ll use the axios library to fetch data, which returns a promise when we make a network request. That promise may resolve and return the response or throw an error. So, once the React Component mounts, we can straight away fetch like this:
componentDidMount() {
axios.get('/details') // Get user details
.then(response => {
const userCity = response.city;
axios.get(`/restaurants/${userCity}`)
.then(restaurantResponse => {
this.setState({
listOfRestaurants: restaurantResponse, // Sets the state
})
})
})
}
This way, when the state changes (due to fetching), Component will automatically re-render and load the list of restaurants.
Async/await
is a new implementation with which we can make async operations. For example, the same thing can be achieved by this:
async componentDidMount() {
const restaurantResponse = await axios.get('/details') // Get user details
.then(response => {
const userCity = response.city;
axios.get(`/restaurants/${userCity}`)
.then(restaurantResponse => restaurantResponse
});
this.setState({
restaurantResponse,
});
}
Both of these are the simplest of all methods. Since the entire logic is inside the component, we can easily fetch all the data once the component loads.
Drawbacks in the method
The problem will be when doing complex interactions based on the data. For example, consider the following cases:
- We don’t want the thread in which JS is being executed to be blocked for network request.
- All the above cases will make the code very complex and difficult to maintain and test.
- Also, scalability will be a big issue, since if we plan to change the flow of the app, we need to remove all the fetches from the component.
- Imagine doing the same if the component is at the top of the parent child tree. Then we need to change all the data dependent presentational components.
- Also to note, the entire business logic is inside the component.
How we can improve from here?
1. State Management
In these cases, using a global store will actually solve half of our problems. We’ll be using Redux as our global store.
2. Moving business logic to correct place
If we think of moving our business logic outside of the component, then where exactly can we do that? In actions? In reducers? Via middleware? The architecture of Redux is such that it’s synchronous in nature. The moment you dispatch an action (JS objects) and it reaches the store, the reducer acts upon it.
3. Ensuring there’s a separate thread where async code is executed and any change to global state can be retrieved through subscription
From this, we can get an idea that if we’re moving all the fetching logic before reducer — that is either action or middleware — then it’s possible to dispatch the correct action at the correct time.
For example, once the fetch starts, we can dispatch({ type: 'FETCH_STARTED' })
, and when it completes, we can dispatch({ type: 'FETCH_SUCCESS' })
.
Want to develop a React JS application?
[affiliate-section title=”Recommended Courses”][affiliate-card title=”The Best Way to Learn React for Beginners” affiliatename=”Wes Bos” text=”A step-by-step training course to get you building real world React.js + Firebase apps and website components in a couple of afternoons. Use coupon code ‘SITEPOINT’ at checkout to get 25% off.” url=”https://ReactForBeginners.com/friend/SITEPOINT” imageurl=”https://dab1nmslvvntp.cloudfront.net/wp-content/uploads/2017/07/1501203893wesbos.jpg”][/affiliate-section]
Using Redux Thunk
Redux Thunk is middleware for Redux. It basically allows us to return function
instead of objects
as an action. This helps by providing dispatch
and getState
as arguments for the function. We use the dispatch effectively by dispatching the necessary actions at the right time. The benefits are:
- allowing multiple dispatches inside the function
- relating of business logic to the fetch will be outside of React components and moved to actions.
In our case, we can rewrite the action like this:
export const getRestaurants = () => {
return (dispatch) => {
dispatch(fetchStarted()); // fetchStarted() returns an action
fetch('/details')
.then((response) => {
dispatch(fetchUserDetailsSuccess()); // fetchUserDetailsSuccess returns an action
return response;
})
.then(details => details.city)
.then(city => fetch('/restaurants/city'))
.then((response) => {
dispatch(fetchRestaurantsSuccess(response)) // fetchRestaurantsSuccess(response) returns an action with the data
})
.catch(() => dispatch(fetchError())); // fetchError() returns an action with error object
};
}
As you can see, we now have a good control of when to dispatch
what type of action. Each function call like fetchStarted()
, fetchUserDetailsSuccess()
, fetchRestaurantsSuccess()
and fetchError()
dispatches a plain JavaScript object of a type and additional details if required. So now it’s the job of the reducers to handle each action and update the view. I haven’t discussed the reducer, since it’s straightforward from here and the implementation might be varying.
For this to work, we need to connect the React component with Redux and bind the action with the component using the Redux library. Once this is done, we can simply call this.props.getRestaurants()
, which in turn will handle all the above tasks and update the view based on the reducer.
In terms of its scalability, Redux Thunk can be used in apps which don’t involve complex controls over async actions. Also, it works seamlessly with other libraries, as discussed in the topics of the next section.
But still, it’s a little difficult to do certain tasks using Redux Thunk. For example, we need to pause the fetch in between, or when there are multiple such calls, and allow only the latest, or if some other API fetches this data and we need to cancel.
We can still implement those, but it will be little complicated to do exactly. Code clarity for complex tasks will be little poor when compared with other libraries, and maintaining it will be difficult.
Continue reading %Async Operations in React Redux Applications%
LEAVE A REPLY
You must be logged in to post a comment.