Redux-Observable is a Redux middleware that allows you to filter and map actions using RxJS operators. RxJS operators like filter()
and map()
let you transform streams of actions just like how JavaScript’s Array.prototype.filter()
lets you transform arrays.
In this article, I’ll show you how to get started with redux-observable using scripts you can run from Node.js. I will also provide a practical example of using Redux-Observable for HTTP requests with fetch()
.
In redux-observable, an “epic” is a function that takes a stream of actions and returns a modified stream of actions. You can think of an epic as a description of what additional actions redux-observable should dispatch. An epic is analogous to the concept of a “saga” in redux-saga.
Before you write your first epic, you need to install redux-observable. This article assumes you already have Node.js and npm installed. To install redux-observable along with redux and RxJS, run the below command:
install [email protected] [email protected] [email protected]
The most fundamental function in the redux-observable API is the createEpicMiddleware()
function. This function creates the actual Redux middleware you should pass to Redux’s applyMiddleware()
function.
Here is an example of how to create a middleware that transforms actions with type ‘CLICK_INCREMENT’ into actions with type ‘INCREMENT’:
const { createEpicMiddleware } = require('redux-observable'); const { filter, map } = require('rxjs/operators'); const redux = require('redux'); // An 'epic' takes a single parameter, `action$`, which is an RxJS observable // that represents the stream of all actions going through Redux const countEpic = action$ => action$.pipe( filter(action => action.type === 'CLICK_INCREMENT'), map(action => { return { type: 'INCREMENT', amount: 1 }; }) ); const observableMiddleware = createEpicMiddleware(); const store = redux.createStore(reducer, redux.applyMiddleware(observableMiddleware)); // **Must** add the epic to the observable after calling `applyMiddleware()`. // Otherwise you'll get a warning: "epicMiddleware.run(rootEpic) called before // the middleware has been setup by redux. Provide the epicMiddleware instance // to createStore() first" observableMiddleware.run(countEpic); // Sample Redux reducer function reducer(state = 0, action) { console.log('Action', action); switch (action.type) { case 'INCREMENT': return state + action.amount; default: return state; } }
Say you dispatch an action with type ‘CLICK_INCREMENT’ to the above store as shown below:
store.dispatch({ type: 'CLICK_INCREMENT' });
Your filter()
and map()
calls will run, and redux-observable will dispatch an additional action of type ‘INCREMENT’.
Here is the output from the console.log()
statement in the reducer()
function:
{ type: '@@redux/INIT7.2.m.z.p.l' } Action { type: 'CLICK_INCREMENT' } Action { type: 'INCREMENT', amount: 1 }
Note that redux-observable dispatches an additional action. The ‘CLICK_INCREMENT’ action still gets through to the reducer. Epics add actions to the stream by default.
The example shown above serves as a simple introduction but doesn’t capture why you would want to use redux-observable in the first place.
What makes redux-observable so interesting is the ability to use RxJS’ mergeMap()
function to handle asynchronous functions. In other words, redux-observable is a viable alternative to redux-saga and redux-thunk.
Here is an example of how to use redux-observable with a simple async function:
const { createEpicMiddleware } = require('redux-observable'); const { filter, mergeMap } = require('rxjs/operators'); const redux = require('redux'); const startTime = Date.now(); const countEpic = action$ => action$.pipe( filter(action => action.type === 'CLICK_INCREMENT'), // `mergeMap()` supports functions that return promises, as well as observables mergeMap(async (action) => { await new Promise(resolve => setTimeout(resolve, 1000)); return { type: 'INCREMENT', amount: 1 }; }) ); const observableMiddleware = createEpicMiddleware(); const store = redux.createStore(reducer, redux.applyMiddleware(observableMiddleware)); observableMiddleware.run(countEpic); // Sample Redux reducer function reducer(state = 0, action) { console.log(`+${Date.now() - startTime}ms`, action); switch (action.type) { case 'INCREMENT': return state + action.amount; default: return state; } } store.dispatch({ type: 'CLICK_INCREMENT' });
The countEpic()
will now wait about 1 second before dispatching the ‘INCREMENT’ action:
+1ms { type: '@@redux/INIT7.i.8.v.i.t' } +7ms { type: 'CLICK_INCREMENT' } +1012ms { type: 'INCREMENT', amount: 1 }
If you’ve read Mastering Async/Await, you know that this isn’t the whole story with supporting async/await. What happens if your async function errors out? The below countEpic()
will crash:
const countEpic = action$ => action$.pipe( filter(action => action.type === 'CLICK_INCREMENT'), mergeMap(async () => { throw new Error('Oops!'); }) );
To handle errors, you should always put an RxJS catchError()
at the end of your epic as shown below:
const { createEpicMiddleware } = require('redux-observable'); const { catchError, filter, mergeMap } = require('rxjs/operators'); const redux = require('redux'); const startTime = Date.now(); const countEpic = action$ => action$.pipe( filter(action => action.type === 'CLICK_INCREMENT'), mergeMap(async () => { throw new Error('Oops!'); }), catchError(err => Promise.resolve({ type: 'Error', message: err.message })) );
The countEpic()
will now dispatch an action of type ‘ERROR’ with the error message:
+1ms { type: '@@redux/INIT0.a.g.q.3.o' } +6ms { type: 'CLICK_INCREMENT' } +8ms { type: 'Error', message: 'Oops!' }
The above examples are simple but not very realistic. Let’s use redux-observable for a more realistic use case: making an HTTP request using node-fetch to get the current MongoDB stock price from the IEX API. To get the stock price, you need to make a GET request to the following URL:
://api.iextrading.com/1.0/stock/MDB/price
Since you can use async/await with mergeMap()
, making an HTTP request with redux-observable is similar to the asynchronous dispatch example. Node-fetch returns a promise, so you can await
on an HTTP request and then dispatch a new action with the result of the request.
In the code below, fetchEpic()
fires off a GET request to the IEX API every time an action of type ‘FETCH_STOCK_PRICE’ comes through the system. If the request succeeds, fetchEpic()
dispatches a new action of type ‘FETCH_STOCK_PRICE_SUCCESS’ with the stock price:
const fetch = require('node-fetch'); // ... const fetchEpic = action$ => action$.pipe( filter(action => action.type === 'FETCH_STOCK_PRICE'), mergeMap(async (action) => { const url = `https://api.iextrading.com/1.0/stock/${action.symbol}/price`; const price = await fetch(url).then(res => res.text()); return Object.assign({}, action, { type: 'FETCH_STOCK_PRICE_SUCCESS', price }); }), catchError(err => Promise.resolve({ type: 'FETCH_STOCK_PRICE_ERROR', message: err.message })) );
To glue fetchEpic()
to Redux, the reducer, shown below, stores a map prices
that maps stock symbols to prices. To store the stock price of MongoDB in Redux, the reducer listens for actions of type ‘FETCH_STOCK_PRICE_SUCCESS’, not ‘FETCH_STOCK_PRICE’:
// Sample Redux reducer function reducer(state = { prices: {} }, action) { console.log(`+${Date.now() - startTime}ms`, action); switch (action.type) { case 'FETCH_STOCK_PRICE_SUCCESS': const prices = Object.assign({}, state.prices, { [action.symbol]: action.price }); state = Object.assign({}, state, { prices }); console.log('New state', state); return state; default: return state; } } store.dispatch({ type: 'FETCH_STOCK_PRICE', symbol: 'MDB' });
Shown below is the sample output from running a ‘FETCH_STOCK_PRICE’ action through a Redux store with fetchEpic()
and reducer()
. The ‘FETCH_STOCK_PRICE’ action goes through, fetchEpic()
sees this action and sends off an HTTP request.
When fetchEpic()
gets a response from the IEX API, it sends out a ‘FETCH_STOCK_PRICE_SUCCESS’ action, and then the reducer updates the state:
+1ms { type: '@@redux/INITg.3.m.s.8.f.i' } +5ms { type: 'FETCH_STOCK_PRICE', symbol: 'MDB' } +198ms { type: 'FETCH_STOCK_PRICE_SUCCESS', symbol: 'MDB', price: '79.94' } New state { prices: { MDB: '79.94' } }
Redux-observable is a tool for handling async logic with React and Redux. This is important because React doesn’t generally support async functions. Redux-observable is an interesting alternative to redux-saga and redux-thunk, particularly if you’re already experienced with RxJS. So next time you find yourself wanting to write your own promise middleware, give redux-observable a shot.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
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.