Redux is usually taught with synchronous actions – these are easier to understand but as developers of web apps, we tend to deal with asynchronous actions a lot more often. In this talk, Christopher McDonald compares 3 libraries to assist you with asynchronous Redux actions: redux-thunk, redux-promise, and redux-saga. Chris also compares the different approaches used by these libraries with real-world code examples.
Watch the video on YouTube below and enjoy ?:
[00:01:58] An action creator is just a function that basically returns this object. That’s how’d dispatch an action through a store. Async actions, Redux out of the box will only support synchronous actions, you’re going to have to use some middleware if you want to do any asynchronous. What I’m going to talk about in this talk is Redux-thunk, Redux-promise and Redux-saga. There are some other ones but I’m just going to talk about those three. Redux has the apply middleware function, which is what you use to setup your Redux store with the Middleware. There’s an example of setting up with thunk. Okay, so now we’re going to go through the individual examples. Redux-thunk, this is the easiest one to understand and get your head around because there’s not that much new to learn about. This is recommended and referenced quite a lot in the Redux docs, I think it was made by Dan Abramov, as well. You create action creators that return a function instead of an object. Your action creator function returns a function that gets injected with the dispatch function, plus gets date and then you’re to do whatever you want within that function. Within this example, in the code, my first thunk, we’ll return the function and gets the dispatch and the get state functions and then we’re free to do multiple dispatch calls within there. These actions are treated just like any other Redux action.
[00:03:43] An more real example of thunk, here we’re got an action creator called “submit comment” which is passed in message and user ID. Return our function which is injected with dispatch and get state functions. We dispatch an immediate function to say comment form submitting with the message. We do our asynchronous fetch, which is using the new fetch API which returns a promise. When that promise resolves, we check the responses, if the response is okay then we’ll go and try and get a JSON response of that and if that works, then we’ll dispatch out add comment function. This is the result comment which has a message, user ID and the comment ID. Otherwise, we can dispatch failures and then it’s always good to catch errors in case of a network failure, because a lot of people miss this out. Testing thunk, typically you need to mock a lot of things. You need to mock the Redux store; you need to mock the middleware. You can mock the store using Redux mock store. Then for your Async requests or any libraries you want to mock, you can use Sinon.js, or if you want to mock specifically HTTP calls, you can use Nock. There’s actually quite a lot of effort involved in testing Redux-thunk and then you can see this link if you want to see some more examples of that.
[00:05:22] Now, I move onto Redux-thunk demo. I put together a small application. Let me find it. Yes. Okay. I basically recreated a little Twitter search thing, you can go in and search for Tweets. Let’s say, I don’t know, React. Okay. Then you can switch between the searches. Then, if I were to search for React again, this would increase the number of Tweets that searched. I can keep running more searches. I could add a check box to do polling on this same search and we’d end up getting more Tweets and more Tweets and they’re all cached in-session. If I reloaded this, it would start off from scratch. I have a plugin called, “The Redux debugger” plugin. Now, I don’t know if I can make this bigger. All right, that’s probably not that visible. On the left hand side, I know, I’ll make that over there, okay. Can you see that? On the left side you can see all of the actions that have come through. You can see I’ve got an action called, “Search for Tweets started”, this is the thing, if I click the search button, it’s going to show that there’s an active search running and it’ll disable a button. Then we have an event for success, which the action for this success event gives me my Tweets. Let’s just have a look at that in the code.
[00:07:28] All right. Here we are in the editor. Now, for Redux-thunk, what you’re doing is you’re creating your Asynchronous action creators. This is the one that we’re interested in here, it’s called “Fetch Tweets”. We’re only interested in the dispatch function. We dispatch the search for Tweets has started with the search text and then we call API search and then API is just some little library I wrote that just wraps a lot of the fetch logic. This internally is using a Twitter proxy, which just means it’s something that runs in the background at this URL, which means I can make requests to this URL which is local host at this port, without have to go through all the stuff you need to do with Twitter. Back to actions, okay. When this API search returns a promise which when resolves we’re given the Tweets and then we dispatch search for Tweets success. The reducers for these. There are three reducers but you can see at the route level, we’ve got Tweets and we have searches. Here’s where we handle all of the started success, error, and all of that. Then when we’ve actually got some Tweets, the Tweets part of the state, we’re just interested in this event and creating an object. This is not an array of Tweets; we’re creating an object so we can index by the Tweet IDs because that’s useful later on. Especially in big apps. That’s the Redux-thunk demo.
[00:09:21] Pros for Redux-thunk: it’s easy to understand, easiest to understand from my opinion out of all the ones that were on offer. There is plenty of documentation, loads of tutorials and you code it all so there’s no magic involved, which is why it’s easy to understand. Cons: it can get quite complicated quickly, you have to resolve promises and handle those yourselves. It can be difficult to test because you’re going to have to mock a lot of things. Right, now we move on to Redux-promise. This is what they call a Flux standard action compliant promise middleware for Redux, which is very complicated. Basically, it’s just saying your actions have to have these type of things, so you need the type that is common for all of Redux actions. You specify a payload field. A payload field can either be a promise, it can be the data that’s resolved from a promise or the error from a promise. Error which is an optional bullion flag and then meta which is where you stick everything else. There’s also Redux-promise middleware, which is a better version than Redux-promise. It has the ability to have pending actions and allows you to give post fixes to your actions. For instance, with Redux-promise, I want to go and fetch something, I’d set my payload to API fetch something. Then what the reducers would receive, eventually, is an action that has the payload with the fetch data or if something went wrong, a payload with the error and the error flag set to true.
[00:11:06] This is basically just what I said, you set your action payload to be a promise, the middleware is going to intercept your action. Check for your promise as a payload and then wait for that to resolve of fail before it passes it on to the reducers. If it’s successful, payload will be the result of the promise, if it fails then it will be the error. All right, here’s a more complete example if we were hosting comments. Here I go to do a post on my API comments endpoint and that’s the body. If the response is okay, then we’ll try to resolve and return the actual comment that’s returned from the network request. Otherwise, we throw these errors and then here’s the action create out that responds to that API request. In Redux-promise, let’s go to here. Right, this is just the same this, just in code again. We’re got our fetch Tweets action but this time the API, the payload is in the API search function that’s called. You can see it resolves the same way in here. If I open the dev tools again and put that on the bottom, search for React, where is it gone? Okay, we get search for Tweets started and then search for Tweets, but now this time search for Tweets has the payload as being the actual data and there is no error field. If I was to go and stop this Titter proxy thing from running and then do another search, you’d get failed to fetch. Here, we’ve got the same search for Tweets started again action, then the same search for Tweets action that’s passed through but this time there’s a bullion that says there’s an error and the meta contains the error. Sorry, the payload contains the error; although, you can’t see it.
[00:13:32] Then, finally, okay, pros and cons of Redux-promise. This is quite easy to understand; it can result in less boiler plate code. You get simplistic action creators; however, this has the least amount of documentation to it and it also assumes that you’re going to use some other library like Redux actions and you may even still require Redux-thunk. In my opinion, it’s not really worth pursuing. If you were going to go down Redux-promise route, you’re better off with Redux-promise middleware that does things slightly better. As I said, you can have the actions with the suffix that tells you whether something is an error or it succeeded. You also get pending actions for free. Redux-saga, this is the one that I prefer. The only thing to really understand about this is you need to know generators. It uses generators to handle your asynchronous flows, but the advantage is you get code that’s really easy to read, you can understand what’s going on, what’s happening in an error case and what’s happening in failure. Best way to think of them is they’re like background demons to handle some asynchronous process. You can have a saga that’s watching for something in the background and then once you get an action, it can go and start doing all the work in the background and dispatching actions as usual. You can compose sagas as well; sagas can call other sagas. You normally trigger saga on some action that’s dispatched by the store. The middleware controls the flow of the sagas. Generator functions yield objects to the Redux-saga middleware.
[00:15:42] This is probably not going to make much sense unless you know how generators work. It’s definitely worth doing some tutorial videos if that’s not familiar for you. If you yield a promise in one of your sagas, the Redux middleware will wait for that to resolve and then it will pass back in the resolve state of the promise or a failure. There are lots of helper functions that can control what the middleware is going to do next, so you can tell it to dispatch an action, wait for a dispatch action, you can call the function and it can allow for pretty complicated flows. This is one of the biggest advantages is it’s quite easy to write tests for the flows with little to no mocking. In most cases, if we’re testing our asynchronous flows, we don’t really want to test an API library that’s just checking if our API library works. That should be a separate unit test. If you want to test an asynchronous flow, you just want to check that you flow is dispatching the right actions at the end. This is an example saga, so imagine I have an API which is the same API as before. We’re using some help functions here like, “Take every call and put…” I’ll describe what those do in a minute. This is our main saga, to go and fetch products. In this case, we’re wrapping this in the tri-catch block, we’re yielding a call, this is the call function that’s up here. We’re saying, “Redux-saga, please call this function with these arguments.” This API fetch is going to go do the fetch for the products, Redux-saga will wait for the promise to resolve and it will inject the results of that back into this line.
[00:17:42] This yield call eventually gets replaced with a value which is the result of this call. Then after we’ve got our products, we can dispatch products received and in case there’s an error, then we dispatch as failure. This is watcher saga where we used to take every helper function. This is saying to Redux-saga, “If we yield this and this action is dispatched via the store, then run this saga.” Every time this event dispatches, restart this saga and then it runs. Then the same thing, we can have that for create product and then watch create product and then what you have is your route saga which is where you start off all the watchers, basically. Here in the fort call, we’re just basically saying, “Start these two watchers, which is watching for this action and that one is watching for something else”. Now, this is the example from the documentation about how you’d write a test for these sagas. Here we’re using call and put, so call is the function that goes and says, “Redux-saga, call this function for me”. Then put is a helper function that says, “Redux-saga, go and dispatch an action for me, put this action through the store”. If we were writing a test for this saga, we create an instance of the saga. That fetch product, for example, from before, we’re just basically calling it, and remember this is a generator function. Generator functions return an iterator that you can use to step through all of your yield statements.
[00:19:30] Our first test is saying, “Iterate to next.value” which is the first yielded statement, which is supposed to equal to this. Look at this line, this is a call statement, the same call statement that we have from the Redux helper functions, which is saying, “Call API fetch products”. The reason you can do this is because this “call API fetch products” is just like an action creator itself, it returns an object. It’s not really doing anything, it’s an instruction that is being sent to Redux-saga to say, “Go and do this”. From the first step in this function, we can assert that it’s an instruction to go and do this. Then what we can do is we can create a fake response. You tell it to step through the next one, passing in the products and then you assert that your asynchronous flow as dispatched the correct action, which in this case is supposed to be a put statement. Remember put statements are saying, “Dispatch an action”. Here we’re asserting dispatch that we have received the products and these are the products that we’re received. That’s why it’s so easy to test sagas. Last step, which is kind of redundant, but here it is. Search for React again and I get fail to fetch because I stopped the thing running. Okay, we’ve got something. Then, finally, here is all of the requests. Now, there are a lot of helper functions that you can use in Redux-saga. Let me just open one of them.
[00:21:40] Okay, now you’ll notice that our actions file has only the synchronous actions, we have no funny actions here going on. These are all pure functions. What we have now instead is a file called, “Sagas” which is where we put all of our asynchronous sagas. In this example, we’ve got fetch Tweets as our worker saga, again, we’re doing the call to search, we dispatch that it’s successful with the Tweets or if there’s an error we dispatch an error. Then we have a watcher for what’s searching for Tweets. If you’ll notice on this line, I’m using take every, which is saying, “Listen for every type of this event”. If I just clear out the events here and click this like three times and go back, see, we got three requested? Then three successes, so it went and ran all three of them. There’s an alternative to that which are coming out of this line. I’ll put that one on, this is take latest which is saying, “Instead of doing take every, just take the last action and process that”. The previous call, every time one of these actions gets dispatched, it’s going to start your worker saga and wait for them to finish. In this case, take latest will do the same thing but it’s going to cancel any previously running sagas. The handler for this one will be cancelled and it should only do the last one.
[00:23:15] If I do the same and do React with three searches, see now we’ve got the three requested actions and then only one success, which is what you want. They have other things like that which I won’t go into too much detail. Right, so the pros for Redux-saga is that it has lots of documentation, you actually end up writing a lot less code as long as you’ve wrapped your API calls into some kind of promise library. The Async flows are very declarative, easy to read, you can handle pretty complicated scenarios and they’re easy to test. Cons is you need to understand generators and it has a steeper learning curve. Each of these approaches have got their own pros and cons, stick with what you know. Some of them, especially Redux-promise, you might still have to rely on Redux-thunk, but I would recommend spending the time to learn generators and give Redux-saga a go. If you use RX.js, you might want to try Redux-observable. I’ll admit that I have not looked at this at all, but from the docs it says it’s pretty similar to Redux-saga except you work with an observable. One final tip, isolate your API test from your Async flow tests. That’s what Redux-saga is really good for. Here if you want to learn more and the GIF isn’t playing but it was a cat who goes behind the laptop. That’s it. I’ll put a link to the slides on the meetup page.