How redux uses actions
Redux works off of pure functions. This means that side effects or mutations will not work as intended. If impure functions are used, the application will most likely log out errors to the console. Side effects include using ajax to hit an API, reaching out to the browser, and timeouts. In order to make asynchronous requests or side effects inside of Redux, the use of middleware is required. Common middlewares that handle asynchronous actions are Redux Thunk and Redux Saga.
Saga vs. Thunk
How does Redux Saga compare to Redux Thunk? Which one should you use and why? Choosing between the two for handling async actions within your code is up to you. As with all tools, they both have their pros and cons.
What the Thunk?
Redux Thunk intercepts Redux action creators. Instead of action creators returning object actions, they return functions. These returned functions can hold both asynchronous and synchronous actions. This includes using Ajax to hit an endpoint, dispatching other action creators, or getting information from the browser. These returned functions are considered “Thunks” and they flow with the regular action creators to the source of application connection. This probably includes being wired up with the React Redux connect method.
The callback nature of Thunk makes it difficult to handle errors and write tests. Deeply nested errors within Thunks makes it hard to weed through to the source. Tests become brittle and hard to maintain, especially at scale.
Introducing Redux Saga
On the other hand, Redux Saga stands independent of Redux action creators, it does not intertwine with the Redux flow to connect. It has the ability to dispatch other action creators and run synchronous / asynchronous operations. A “Saga” is a generator function. Similar to Thunk, all actions are organized inside of a generator function. It is wired up within the creation of the store the same as Redux Thunk. Once the middleware is connected to the store, a ‘task’ or running thread needs to initialize. Sagas run as a background running process, similar to a daemon. The Saga middleware hooks into the store and watches the flow of actions -> store -> reducers. When Sagas are created, configurations need to be set up in order for the middleware to know when to fire a Saga. These configurations are the glue between written sagas and actually stepping through each iteration. They are high level implementations of lower level saga effects called saga helpers.
On the other hand, try catch blocks are utilized within Sagas. This enables easy error handling and debugging. By gracefully catching errors, one can simply dispatch other actions to show messages to the user. It’s also, easy to replicate different scenarios for testing purposes with the yield keyword.
The hard parts of Redux Saga
The most daunting aspect to using Sagas are writing generator functions. Generators are not used very often and are definitely unique. When called, they don’t return the defined return value but always return a generator object. Learning how to use generator functions and objects are critical to understanding how to test your Sagas. All side effects should be organized into Saga effects with the yield keyword. This is how Redux Saga knows what to do with each side effect as it steps through your defined Saga. Learning the difference between blocking and non-blocking, as well as learning how each command is called can be difficult at first!
The good parts of Redux Saga
Redux Saga offers many out of the box customization options. With the use of Saga commands (also called effects), one can easily configure complex async actions with relative ease. Just as important as understanding generators, is understanding Saga commands/effects. The effects are organized into two categories, blocking and non-blocking. The Redux Saga docs explain it best, “A Blocking call means that the Saga yielded an Effect and will wait for the outcome of its execution before resuming to the next instruction inside the yielding Generator. A Non-blocking call means that the Saga will resume immediately after yielding the Effect.” Almost any side effect could potentially be used with a blocking or non-blocking command. It depends on if the Saga flow is designed to wait for a response or not. For example, as we watch shows on Netflix, it is sending out ajax requests every so often, so that it can keep track of our progress. Since It does not necessarily care for a returned client side response, it would wrap the call into a non-blocking command.
There are simple to complex effects designed to help with any possible side effect need. Sequential and concurrent async requests comes easy with Saga commands as well. Concurrent background running requests can also be canceled and clean up mid flight. Something that is not possible with Thunks out of the box. There is a very detailed API doc that can be referenced for help here. Some popular commands are fork and take. Fork is a non-blocking command, which means that side effects wrapped inside of it will be called and the saga will continue iterating through without waiting for a response. Take is blocking, so the action within it blocks the saga until resolution.
Then there’s escaping callback hell. When asynchronous actions need to happen synchronously in Redux Thunk, chained promises are the answer. Chaining promises introduces callbacks on callbacks. This same effect is accomplished with the yield keyword and a Saga command. There are no callback functions and the ability to write tests becomes more of a reality. Speaking about tests, Sagas are built off of generator functions and Saga commands. Which means that not only is it possible to write reable tests but testing Sagas does not require mocking. With the callback structure of Redux Thunk, mocking out the responses is difficult to manage and can be considered a code smell. If Sagas are created correctly, it’s easy to step through each iteration of the returned generator object and test that the right effect is called at the right time with the right arguments. Saga also ships with some built in testing utils for testing difficult asynchronous requests. Also, the yield keyword has two implementations. Not only is it used inside of generator functions to define iterations but it can receive an optional value from the caller. Which means that when iterating over the returned generator object with the next method, values can be passed through as a parameter. With this in mind, one could pass in different values and test that sagas handle returned async actions properly. A great example of this is when working with if checks from inside of sagas that check data called from the store state. Then performs conditional effects depending on the returned store data. Also, the yield* keyword can flatten generators (or iterable objects) inside of a generator. Which means that the component composition behavior can be replicated with Sagas. For example, let’s say we had two different generator functions that are yielding values. You could bring all of the yielded values of one of the generator functions into the other by using the yield* keyword. This will loop through each yield in the called generator function and pull them out into the one generator function.
Putting it All Together
First off, it is important to understand that there is no silver bullet. Every tool comes with pros and cons. These should be considered before using any tool inside of production code. Redux Saga has its own difficulty and might not be the best option for some applications. As mentioned throughout, it does have plenty of pros that can help change the way you handle effects in your code base. When used correctly with generators and Saga effects, Redux Saga offers the ability to write scalable, testable, and composable complex side effects.
- Sagas are generator functions.
- Generator functions return generator objects. Objects have a set number of iterations defined by the yield keyword used in the called generator function.
- Saga commands are used to tell Saga middleware how to handle provided side effects.
- Redux Saga middleware calls Saga generators and steps through each iteration of the returned generator object.
- Testing is made simple. No mocking, just testing that correct commands are called at the right iteration, under correct situations, and with the right arguments.
More Information https://egghead.io/courses/async-react-with-redux-saga