Redux 4 Ways
Implementations of Thunk vs Saga vs Observable vs Redux Promise Middleware in 10 minutes.
At the last React Native online meetup, I gave a presentation on the differences of Thunk vs Saga vs Redux Observable (see slides here).
These libraries offer ways to handle side effects or asynchronous actions in a redux application. For more information on why you may need something like this, see this link.
I thought I would take this one step further and not only create a repo for viewing and testing these implementations, but walk through how they are implemented step by step and add one more implementation, Redux Promise Middleware.
When I first started with redux, wrapping my head around these asynchronous side effect options was overwhelming. Even though the documentation was not bad, I just wanted to see the most absolute basic implementations of each in action to give me a clear understanding of how to get started with them without wasting a bunch of time.
In this tutorial, I’ll walk through a basic example of fetching and storing data in a reducer using each of these libraries.
As displayed in the above diagram, one of the most common use cases for these side effect libraries is hitting an api, showing a loading indicator, then displaying the data once it has returned from the api (or showing an error if there is an error). We will implement this exact functionality in all four libraries.
Getting Started
I will be using React Native in this example, but feel free to use React, as it will all be exactly the same. If you are following along, just replace the View
with div
, and Text
with p
. In this first section, we will just be implementing a basic redux boilerplate to use with the four libraries.
I will run react-native init to create an empty project:
react-native init redux4ways
Or, using create-react-app:
create-react-app redux4ways
Then, cd into the project
cd redux4ways
Next, we will install all of the dependencies we will need for the rest of the project.
yarn add redux react-redux redux-thunk redux-observable redux-saga rxjs redux-promise-middleware
Next, we will create all of the files and folders we will need to get started:
mkdir reducers
touch reducers/index.js reducers/dataReducer.js
touch app.js api.js configureStore.js constants.js actions.js
Now that we have everything installed and the files we need, we will build out the basic redux implementation we will be using.
In index.ios
(ios) or index.android.js
(android), update the code to the following:
- We import
Provider
fromreact-redux
- import
configureStore
that we will create soon - import
App
which will be our main application component for this tutorial - create the store, calling
configureStore()
- wrap
App
in theProvider
, passing in the store
Next, we’ll create the constants we will use in our actions and reducer. In constants.js
, update the code to the following:
Next, we will create our dataReducer
. In dataReducer.js
, update the code to the following:
- We import the constants that we will be needing in this reducer.
- The
initialState
of the reducer is an object with adata
array, adataFetched
Boolean, anisFetching
Boolean, and anerror
Boolean. - The reducer checks for three actions, updating the state accordingly. For example, if
FETCHING_DATA_SUCCESS
is the action, then we update the state with the new data, and setisFetching
tofalse
.
Now, we need to create our reducer entrypoint, in which we will call combineReducers
on all of our reducers, which in our case is only one reducer: dataReducer.js
.
In reducers/index.js
:
Next, we create the actions. In actions.js
, update the code to the following:
- We import the constants that we will be needing in this reducer.
- Create four methods, three of them calling actions (
getData
,getDataSuccess
, andgetDataFailure
), the fourth will be updated to a thunk soon (fetchData
).
Now, let’s create the configureStore. In configureStore.js, update the code to the following:
- import the root reducer from
‘./reducers’
- export a function that will create the store
Finally, we will create the UI and hook into the props that we will need. In app.js
:
Everything here is pretty self explanatory. If you’re new to redux, the connect method transforms the current Redux store state and imported actions into the props you want to pass to a presentational component you are wrapping, in our case App
.
The final piece we will need is a mock api that will simulate a 3 second timeout and return a promise with the fetched data.
To do so, open api.js
and place in it the following code:
In api.js
, we are creating an array of people, and when this file is imported and executed, it will return a promise that will return after 3 seconds with the people array..
Redux Thunk
Now that redux is hooked up, we will sync it up with our first asynchronous library, Redux Thunk. (branch)
To do so, first we need to create a thunk.
“Redux Thunk middleware allows you to write action creators that return a function instead of an action. The thunk can be used to delay the dispatch of an action, or to dispatch only if a certain condition is met. The inner function receives the store methodsdispatch
andgetState
as parameters.” — Redux Thunk documentation
In actions.js
, update the fetchData
function and import the api:
The fetchData
function is now a thunk. When fetchData is called, it returns a function that will then dispatch the getData
action. Then, getPeople
is called. Once getPeople
resolves, it will then dispatch the getDataSuccess
action.
Next, we update configureStore
to apply the thunk middleware:
- import applyMiddleware from
redux
- import
thunk
fromredux-thunk
- call
createStore
, passing inapplyMiddleware
as the second argument.
Finally, we can update the app.js
file to use the new thunk.
Main takeaways from this change:
- We add an onPress method to the TouchableHighlight that calls
props.fetchData()
when pressed. - We add a check to see if
props.appData.isFetching
is true, and if so we return loading indicator text. - We add a check to
props.appData.data.length
, looping through the array if it is there and returning the name and age of the person.
Now, when we click the Load Data button, we should see the loading message, and then the data should display after 3 seconds.
Redux Saga
Redux Saga uses a combination of async await and generators to make for a smooth and fun to use api. (branch)
“It uses an ES6 feature called Generators to make those asynchronous flows easy to read, write and test. (if you’re not familiar with them here are some introductory links) By doing so, these asynchronous flows look like your standard synchronous JavaScript code. (kind of likeasync
/await
, but generators have a few more awesome features we need)” — Redux Saga documentation
To implement a Saga, we first need to update our actions.
In actions.js
, replace everything except the following function:
This action will trigger the saga we are about to create. In a new file called saga.js
, add the following code:
- We import the constants that we will be needing.
- We import
put
andtakeEvery
fromredux-saga/effects
. When we callput
, redux saga instructs the middleware to dispatch an action.takeEvery
will listen for dispatched action (in our caseFETCHING_DATA)
and call a callback function (in our casefetchData
) - When
fetchData
is called, we will wait to see ifgetPeople
returns successfully, and if it does, we will dispatchFETCHING_DATA_SUCCCESS
Finally, we need to update configureStore.js
to use the saga middleware instead of the thunk middleware.
The main things to note in this file is that we import our saga and also createSagaMiddleware
form redux-saga
. When we create the store, we pass in the sagaMiddleware
that we created, and then call sagaMiddleWare.run
before returning the store.
Now, we should be able to run the application and get the same functionality that we had when using redux thunk!
Notice that we only changed three files in the move from thunk to saga:saga.js
configureStore.js
andactions.js
.
Redux Observable
Redux Observable uses RxJS and observables to create asynchronous actions and data flow for a Redux app. (branch)
“RxJS 5-based middleware for Redux. Compose and cancel async actions to create side effects and more.” — Redux Observable documentation
The first thing we need to do to get started with redux observable is to again update our actions.js file:
As you can see, we’ve updated our actions to have our original three actions from before.
Next, we will create what is known as an epic. An epic is a function which takes a stream of actions and returns a stream of actions.
Create a file called epic.js
with the following code:
$ is a common RxJS convention to identify variables that reference a stream
- import the FETCHING_DATA constant.
- import
getDataSuccess
andgetDataFailure
functions from the actions. - import
rxjs
andObservable
from rxjs. - We create a function called
fetchUserEpic
. - We wait for the
FETCHING_DATA
action to come through the stream, and when it does we callmergeMap
on the action, returningObservable.fromPromise
fromgetPeople
and mapping the response to thegetDataSuccess
function from our actions.
Finally, we just need to update configureStore to use the new epic middleware.
In configureStore.js
:
Now we should be able to run our application and the functionality should all work as before!
Redux Promise Middleware
Redux Promise Middleware is a lightweight library for resolving and rejecting promises with conditional optimistic updates. (branch)
“Redux promise middleware enables robust handling of async code in Redux. The middleware enables optimistic updates and dispatches pending, fulfilled and rejected actions. It can be combined with redux-thunk to chain async actions.” — Redux Promise Middleware documentation
As you will see, Redux Promise Middleware reduces boilerplate pretty dramatically vs some of the other options.
It can also be combined with Thunk to chain the async actions.
Redux Promise Middleware is different in that it takes over your actions and appends _PENDING
, _FULFILLED
, or _REJECTED
actions depending on the outcome of your promise.
For example, if we called FETCHING like this:
function fetchData() {
return {
type: FETCH_DATA,
payload: getPeople()
}
}
Then FETCH_DATA_PENDING
would automatically be dispatched.
Once the getPeople
promise resolved, it would call either FETCH_DATA_FULFILLED
or FETCH_DATA_REJECTED
depending on the outcome of getPeople
.
Let’s see this in action in our existing app.
To get started, let’s first update our constants to match those that we will now be working with. In constants.js
:
Next, in actions.js
, let’s update our action to a single action: FETCH_DATA
:
Now, in our reducer (dataReducer.js
) we need to swap out the actions with the new constants we are working with:
Last, we just need to update configureStore
to use the new Redux Promise Middleware:
Now, we should be able to run our application and get the same functionality.
Conclusion
Overall, I think I like Saga for more complex applications, and Redux Promise Middleware for everything else. I really like working with generators and async-await with Saga, it is fun, but I also like the reduction in boilerplate that Redux Promise Middleware offers.
If I knew how to use RXJS a little better, I may sway to Redux Observable, but there are still quite a few things I don’t understand well enough to confidently use it in production.
My Name is Nader Dabit, and I am a software developer that specializes in building and teaching React and React Native.
If you like React Native, checkout out our podcast — React Native Radio on Devchat.tv with Gant Laborde Kevin Old Ali Najafizadeh and Peter Piekarczyk
Also, check out my book, React Native in Action now available from Manning Publications
If you enjoyed this article, please recommend and share it! Thanks for your time