Editor’s note: This post was updated 18 March 2022 to convert the images of code to CodePens and written snippets for easier interaction, address the popularity and preference for Redux Toolkit to handle asynchronous actions, and revalidate that the concepts elaborated on in this post are still up-to-date.
As any Redux developer could tell you, the hardest part of building an app is figuring out how to handle asynchronous calls — how do you manage network requests, timeouts, and other callbacks without complicating the Redux actions and reducers?
To manage this complexity, I’ll cover and describe a few different approaches for handling asynchronous tasks in your app, ranging from:
We are going to use React and Redux, so this post assumes you have at least a passing familiarity with how they work.
Typically, for every interaction a user makes with your application, there is usually a change in the state of the application. These interactions could range from clicking a button to hovering over a component on the interface; the results of these interactions determine what is rendered on the UI. These interactions also trigger an action, a plain object that describes what happened and is responsible for changing the state of the application.
Tracking these actions and their respective types in order to determine which effect or response should be carried out can sometimes be cumbersome when using plain JavaScript. Redux solves this using specialized functions that are referred to as action creators. Through action creators, you can perform specific operations based on the type of action dispatched to the reducer.
Calling an API is a common requirement in many apps. Let’s look at an example — imagine we need to show a random picture of a dog when we click a button:
We can use the Dog CEO API and something as simple as a fetch()
call inside of an action creator:
See the Pen
redux-saga-1 by Olu (@olu-damilare)
on CodePen.
There is nothing wrong with this approach. All things being equal, we should go for the simplest approach.
However, using bare Redux won’t give us much flexibility. At its core, Redux is only a state container that supports synchronous data flows: every time an action is sent to the store, a reducer is called and the state is updated immediately.
But in an asynchronous flow, you have to wait for the response first; then, if there’s no error, you can update the state. And what if your application has a complex logic/workflow?
Redux uses middleware to solve this problem. A middleware is a piece of code that is executed after an action is dispatched, but before it reaches the reducer. Its core function is to intercept the action sent to the reducer, perform any asynchronous operation that may be present in the action, and present an object to the reducer.
Many middleware can be arranged into a chain of execution to process the action in different ways, but the middleware has to interpret anything you pass to it. It must also make sure to dispatch a plain object (an action) at the end of the chain.
For asynchronous operations, Redux offers the thunk middleware, which is part of the popular Redux Toolkit.
Redux Thunk is the standard way of performing asynchronous operations in Redux. For our purposes, a thunk represents a function that is only called when needed. Take the example from Redux Thunk’s documentation:
let x = 1 + 2;
The value 3
is assigned immediately to x
. However, when we have something like the following statement:
let foo = () => 1 + 2;
The sum
operation is not executed immediately, only when you call foo()
. This makes foo
a thunk.
Redux Thunk allows an action creator to dispatch a function in addition to a plain object, converting the action creator into a thunk.
This is what our demo app looks like using the Redux Thunk approach:
See the Pen
redux-saga-2 by Olu (@olu-damilare)
on CodePen.
At first, this might not seem too different from the previous approach.
Without Redux Thunk:
// Action creator const fetchDog = async (dispatch) => { try{ dispatch(requestDog()); var response = await fetch('https://dog.ceo/api/breeds/image/random'); var data = response.json; return dispatch(showDog(data)); }catch(error){ return dispatch(requestDogError()); } }; // Invoking the action creator <button onClick={() => fetchDog(this.props.dispatch)}>Show Dog</button>
With Redux Thunk:
// Action creator const fetchDog = async (dispatch) => { try{ dispatch(requestDog()); var response = await fetch('https://dog.ceo/api/breeds/image/random'); var data = response.json; return dispatch(showDog(data)); }catch(error){ return dispatch(requestDogError()); } }; // Invoking the action creator <button onClick={() => this.props.dispatch(fetchDog())}>Show Dog</button>
However, the advantage of using Redux Thunk is that the component doesn’t know that it is executing an asynchronous action. Since the middleware automatically passes the dispatch
function to the function that the action creator returns, there is no difference between asking the component to perform a synchronous action, followed by an asynchronous action (and they don’t have to care anyway).
By using middleware, we have added a layer of indirection that gives us more flexibility. Since Redux Thunk gives the dispatch
and getState
methods as parameters to the dispatched function from the store, you can also dispatch other actions and read the state to implement more complex business logic and workflows.
Another benefit is that if there’s something too complex for thunks to express without changing the component, we can use another middleware library to have more control: Redux Saga.
Redux Saga is a library that aims to make side effects easier to work with through sagas, which are design patterns that come from the distributed transactions world. If you want a deep dive on sagas, I’d suggest watching Caitie McCaffrey’s lecture, Applying the Saga Pattern.
A saga manages processes that need to be executed in a transactional way, maintaining the state of the execution and compensating for failed processes. In the context of Redux, a saga is implemented as a middleware because we can’t use a reducer, which must be a pure function, to coordinate and trigger asynchronous actions (side effects).
Redux Saga does this with the help of ES2015 generators:
function* myGenerator(){ let first = yield 'first yield value'; let second = yield 'second yield value'; return 'third returned value'; }
Generators are functions that can be paused during execution and resumed, instead of executing all of a function’s statements in one pass.
When you invoke a generator function, it will return an iterator object. With each call of the iterator’s next()
method, the generator’s body will be executed until the next yield
statement, where it will then pause:
const it = myGenerator(); console.log(it.next()); // {value: "first yield value", done: false} console.log(it.next()); // {value: "second yield value", done: false} console.log(it.next()); // {value: "third returned value", done: true} console.log(it.next()); // {value: "undefined", done: true}
This can make asynchronous code easy to write and understand. For example, instead of doing this:
const data = await fetch(url); console.log(data);
With generators, we can do this:
let val = yield fetch(url); console.log(val);
And with Redux Saga, we generally have a saga whose job is to watch for dispatched actions:
function* watchRequestDog(){ }
To coordinate the logic we want to implement inside the saga, we can use a helper function like takeEvery
to spawn a new saga to perform an operation:
// Watcher saga for distributing new tasks function* watchRequestDog(){ yield takeEvery('FETCHED_DOG', fetchDogAsync) } // Worker saga that performs the task function* fetchDogAsync(){ }
The watcher saga is another layer of indirection that increases your flexibility to implement complex logic, but may be unnecessary for simple apps.
If there are multiple requests, takeEvery
will start multiple instances of the worker saga; in other words, it handles concurrency for you.
Recalling our example, we could implement the fetchDogAsync()
function with something like this (assuming we had access to the dispatch
method):
function* fetchDogAsync(){ try{ yield dispatch(requestDog()); const data = yield fetch(...); yield dispatch(requestDogSuccess(data)); }catch (error){ yield dispatch(requestDogError()); } }
But Redux Saga allows us to yield an object that declares our intention to perform an operation, rather than yielding the result of the executed operation itself. In other words, the above example is implemented in Redux Saga in this way:
function* fetchDogAsync(){ try{ yield put(requestDog()) const data = yield call(() => fetch(...)) yield put(requestDogSuccess(data)) }catch(error){ yield put(requestDogError()) } }
Instead of invoking the asynchronous request directly, the method call
will return only a plain object describing the operation. Redux Saga then takes care of the invocation and return the result to the generator.
The same thing happens with the put
method. Instead of dispatching an action inside the generator, put
returns an object with instructions for the middleware to dispatch the action.
Those returned objects are called effects. Here’s an example of the effect returned by the call
method:
{ CALL: { fn: () => {/* ... */}, args: [] } }
Another added benefit is the ability to easily compose many effects into a complex workflow. In addition to takeEvery
, call
, and put
, Redux Saga offers a lot of effect creators for throttling, getting the current state, running tasks in parallel, and cancel tasks, just to mention a few.
Back to our example, this is the complete implementation in Redux Saga:
See the Pen
redux-saga-3 by Olu (@olu-damilare)
on CodePen.
This is what happens behind the scenes after you click the button:
FETCHED_DOG
is dispatchedwatchFetchDog
takes the dispatched action and calls the worker saga fetchDogAsync
success
or fail
)By working with effects, Redux Saga makes sagas declarative, rather than imperative, which adds the benefit of a function that returns a simple object, which is easier to test than a function that directly makes an asynchronous call.
To run the test, you don’t need to use the real API, fake it, or mock it — you can just iterate over the generator function, asserting for equality on the values yielded:
const iterator = requestTrivia(); asserts.deepEqual( iterator.next().value, call(fetch(...)), "requestDog should yield the Effect call(fetch)" )
If you believe some layers of indirection and a little bit of additional work is worth it, Redux Saga can give you more control to handle side-effects in a functional way.
This post has shown you how to implement an asynchronous operation in Redux with action creators, thunks, and sagas, going from the simplest approach to the most complex.
Redux doesn’t prescribe a solution for handling side effects. When deciding which approach to take, you have to consider the complexity of your application. My recommendation is starting with the simplest solution.
There are other alternatives to Redux Saga that are worth trying. Two of the most popular options are Redux Observable (based on RxJS) and Redux Logic (also based on RxJS observables, but with the freedom to write your logic in other styles).
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.
5 Replies to "Understanding Redux Saga: From action creators to sagas"
Great into to redux sagas, thank you!
Very good explained!!!
Great Article!
“A middleware is a piece of code that is executed after an action is dispatched but before reaching the reducer”.
It’s the opposite – first reducer, then saga
Hi,
Not sure if I am missing something here, but with just one, simple call to API what is the point of using SAGA, instead of simple Thunk ?