Lizhooh

Using redux-observable to handle asynchronous logic in Redux

Lizhooh · 2017-11-10推荐 · 839阅读 CET/4 204 CET/6 18 原文链接

Using redux-observable to handle asynchronous logic in Redux

Looking for a way to simplify your Redux app?

I’m sure you had a similar experience to what I had when I first started using Redux. You checked it out a bit, got excited by it’s elegance, and then asked:

Where do I put async logic like talking to a server?

In this article, we’ll compare two options for handling async logic in Redux: redux-thunk and redux-observable. Redux-saga is another option that shares a lot of similarities with redux-observable, but it’s not included in this article.

A high-level look at asynchrony in Redux

Let’s explore the scenario of talking to a server in the context of a Redux app.

Usually, when you talk to a server, you don’t do it in a fire and forget manner. When you want to send data to the server to save it, you also want to know whether it was successful or not in order to give the user the opportunity to resubmit or take another action. When you want to retrieve data from the server, your request might be successful or result in several different types of failures (expired session, authorization failed, invalid request, broken connection, etc).

For all these different scenarios, you want your Redux store to be updated with the results, allowing your views to… ahem… react to the changes.

But Redux in itself does not give you a good place to put your async logic. With Redux, you dispatch actions from your component and these actions get reduced to make changes to state. If you want to execute asynchronous logic, should it live in the component? Somewhere else with access to the dispatcher?

Fortunately, Redux caters very well for middleware: functions that you can slot into the dispatch pipeline, where your functions have access to the actions being dispatched and potentially dispatch new actions of their own.

This makes it possible for components to stay simple, to still just dispatch actions, but with middleware that intercepts the actions, where it can perform async logic and dispatch further actions to get the results of the async logic reflected on state.

Redux-thunk

The de facto middleware for executing async logic is redux-thunk, a middleware library created by Dan Abramov, who also created Redux.

With redux-thunk, instead of only publishing plain objects as actions, you can also dispatch functions. These functions are called thunks, and they will be called by the middleware, which will pass dispatch on the 1st parameter and getState on the 2nd parameter of the thunk. That way, you can execute async logic at your leisure and use the dispatcher to dispatch downstream actions when you need to.

Redux thunk basics

Redux-thunk in a simple scenario

Have a look at the following simple example of an action creator that returns a thunk. As you can see, it gets the dispatcher through on its parameters so that you can dispatch downstream actions based on the result of calling fetch to get some data from a server. The thunk also gets access to the state on the store, but we’ll explore that a bit later.

The actions that you dispatch can then be reduced state to allow views to display the results to the user.

A simple async call to the server with redux-thunk

Redux-thunk is a simple go-to solution for the problem of where to put async logic in a Redux app.

Redux-thunk in a more complex scenario

Often we come across more complex scenarios of interaction, where our app needs to handle a bunch of different events from different sources to derive values on state.

Even though scenarios like this can get really complex, let’s go over a scenario that is still simple enough to digest (hopefully) in an article like this.

Imagine you are working on a web-based collaborative drawing app. In this app, you have access to very granular drawing events, and by drawing a simple curve you end up with about 100 events. This is exaggerated, but I’m making it a high number to make our example a bit clearer.

Rapid firing actions from a drawing component

Let’s say that you want to batch those events up to only send the data up to the server between when the user starts drawing a shape and when the user stops drawing a shape (finger up/ mouse up). Essentially, you want to batch up the events into meaningful units.

When you’re working with redux-thunk, your only construct for handling async logic is a thunk. And thunks are meant to be pure, meaning only depend on the input of the function.

A thunk has access to the following inputs:

  • The action that was dispatched, retrieved through a closure from the surrounding action creator function

  • The current state of the store, retrieved by the getState function passed through the thunk’s parameter

Now, in this drawing app of yours, an action gets dispatched for each drawing event with from and to coordinates. Keep in mind that in our example, drawing a simple curve will result in about 100 events, resulting in 100 actions getting dispatched.

So how will we do the batching using redux-thunk? Let’s first start naively by coding up a thunk that we want to execute for each action. First, we just log out to the console, so that you can see what type of data will be coming through. Note that this time I’ve got a getState parameter on the thunk, which we can use to access the store‘s current state.

Ok, so the deal is, we want to store these line events somewhere until we get another action to indicate that the user has stopped drawing (finger up/mouse up).

You might be tempted to think you should just declare a lines array outside of the thunk, so that you can keep on adding to that until you need to send it through to the server.

Sharing a variable between two thunks. Not a good idea.

There’s a problem with that approach though: a different action (and resulting thunk) will be called to signal the end of the batch when the user lifts her finger/mouse. This means that you will need to share data between these 2 different thunks. You don’t want to share a variable between these 2 actions because it would mean that your thunks aren’t pure anymore. This isn’t good because it would return different values with the same input, due to its dependency on other variables.

Ok, so how about dispatching actions for each line action so that we roll these values onto state? You’ll have a lines array on state, where you’ll maintain all the lines that you want to batch into one call to send it to the server. Once a mouse up/finger up action gets dispatched, you can make a call to the server with all the values from the array and clear out the array on state again.

Storing lines on state to make it available for subsequent async logic

With this approach, you won’t need to have a thunk for the line drawing action, because you’re just going to reduce it to your state.

So let’s change the code from above to make it a plain-old action object which we reduce to state:

As you can see, now the action that’ll get fired for each line segment (100 per simple curve in our example), will just get reduced into an array, called lines, to state.

Now you can focus on getting the lines sent to the server when the user lifts her finger or mouse.

This is where you’ll be executing async logic again, so you need to add a thunk this time around, have a look at the drawingMouseUp action creator below. It returns the thunk that contains the async logic.

The code above now has a new thunk, drawingMouseUp, which has access to the state through the getState function (provided by the redux-thunk middleware). It uses that to get access to the lines array that it sends to the server.

After sending it to the server, it dispatches an action based on the result of the fetch call.

When it was successful, it dispatches drawingSyncSuccess , which results in the lines array being cleared out and a drawingSuccess variable to state to be set to true. It also ensures that drawingSyncFailed is set to false.

Inversely, when it failed to sync to the server, it dispatches drawingSyncFailed, which results in the drawingSyncFailed value to be set to true on state, and the drawingSuccess value to be set to false. In this scenario, we leave the lines variable alone, because we might want to try sending it to the server again.

The disadvantages of asynchronous composition using redux-thunk

Redux-thunk is quite simple and easy to get started with, but that comes at a cost. Using the approach from above to manage your async composition in Redux has some disadvantages:

  1. You have to store the lines value in your Redux state store, because you‘re collecting it using one action and sending it up to the server using another. Why is this a problem? We like to think of React components as being functions of state. And in this scenario, we did not need the lines array on state, we just put it there, because we wanted to avoid a variable dependency between 2 different thunks. But with the above approach, we are storing stuff on state, which would call components to be re-rendered, even though they won’t need to. You could use shouldComponentUpdate overrides in your components to avoid this particular side effect of storing extra values on state. In any large application, you probably would use shouldComponentUpdate, but it’s still a smell that your state changes and none of the components are interested in the change.

  2. Very related to the above issue, it complicates your reducer. When you are storing the lines array on state, you need to cater for it in your reducer. As your app grows, your reducers would get more and more complicated as you keep on adding logic for scenarios like this. In a small application this would not be a problem, but would soon overwhelm you when your application starts getting bigger.

  3. Kicking off the async logic to the server happens in a weird place. You might want to trigger other things from happening when a user lifts her finger/mouse from the drawing component. When you start adding other concerns into this action, possibly more async concerns, your logic becomes difficult to untangle. You want a better way of decoupling concerns that need to be kicked off on a mouse up event from the actual event. Meaning, you would want to embrace an event sourcing over a command pattern. The one approach is reactive, the other is imperative.

Redux-observable

Redux-observable is another middleware option that allows you to handle asynchronous logic.

Although it solves the same problem and has some similarity, it works quite differently to redux-thunk.

It embraces the notion that all actions dispatched by your application should be available in an Observable stream and that down-stream actions caused by async logic should be available on an Observable stream.

By default you use RxJS operators on your Observables, but it’s possible to use redux-observable with other Observable libraries.

At a high level, when you use Redux-observable in your application, you need to provide a function with an Observable input and an Observable output. This function is called an epic. The Observable input represents all actions dispatched in the application and the Observable output represents all down-stream actions triggered by your async logic. More succinctly, it calls for actions in, actions out.

This is an Epic

Epics are very similar to reducer functions in Redux in that you have one big function and that you can compose it from other more specific functions. That said, where a reducer’s output is recalculated state, the epic’s output is always downstream actions (similar to a thunk).

Let’s look at the simplest example of an epic function using redux-observable. The following is directly from the redux-observable documentation and contains the type annotated signature of an epic.

As you can see, it’s first parameter is an Observable containing all the actions fired in the application and the second is the store, which can be used to get state directly from the store. Thunks also allow you to call getState to get the latest state from the store, but mostly it’s advised that you try and rely as little as possible on the store when working with epics and try to rely only on the actions being triggered.

Let’s look at an example epic that conforms to this signature. We’re going to take the loadData thunk that we had earlier and change it to an epic.

Notice that this epic abides to this rule: actions in, actions out. The ofType operator on the `action

If you look closely at the code above, you’ll notice that it solves the 3 problems identified when we used redux-thunk to solve the same problem:

  1. We do not need to store the lines on state anymore, so state just reflects what we want components to use.

  2. The reducer is simpler now; it does not need to be aware of the lines being batched up anymore.

  3. We do not have the batching and sending coupled to the DRAWING_MOUSE_UP action anymore. It’s only used as a signal to stop batching. We’ve effectively gone from an imperative relationship between the action and batching to a reactive relationship where it becomes a signal.

Conclusion

While redux-observable might look a bit weird to you, it really starts to shine when you plug it into applications with relatively complex async logic.

Keep in mind that you can combine redux-thunk and redux-observable, so you don’t need to fully abandon the one to use the other.

Overall I love the elegance that it brings into your redux app. It makes it much clearer what’s going on and allows you to use a multitude of powerful operators to derive meaning from actions being fired throughout your app.

Want to learn more on RxJS, React, Redux, and React Native?

I’ve created multiple React and Redux related courses on Pluralsight, which you can view on a free trial.

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.

4585* BlockedUnblockFollowFollowing

Go to the profile of Hendrik Swanepoel

Hendrik Swanepoel

Full-stack JS Engineer. @invisionapp @pluralsight @tagtreetv.

相关文章