Async actions in Redux with RxJS and Redux Observable

May 29, 2018 0 Comments

Async actions in Redux with RxJS and Redux Observable

 

 


Redux is an amazing library. For those of you who don't know what Redux is, it is a predictable state container for JavaScript apps. In English, it acts as a single source of truth for your application's state. The state, or Redux store, as it is called, can only be altered by dispatching actions, which are handled by reducers, who dictate how the state should be modified depending on the type of action dispatched. For those of you who aren't familiar with Redux, check out this link.

Now, Redux is most commonly used in combination with React, although it's not bound to it - it can be used together with any other view library.

Redux's issue

However, Redux has one, but very significant problem - it doesn't handle asynchronous operations very well by itself. On one side, that's bad, but on the other, Redux is just a library, there to provide state management for your application, just like React is only a view library. None of these constitute a complete framework, and you have to choose the tools you use for different things by yourself. Some view that as a bad thing since there's no one way of doing things, some, including me, view it as good, since you're not bound to any specific technology. And that's good, because everyone can choose the tech that they think fits the best to their needs.

Handling asynchronous actions

Now, there are a couple of libraries which provide Redux middleware for handling asynchronous operations. When I first started working with React and Redux, the project that I was assigned to used Redux-Thunk. Redux-Thunk allows you to write action creators which return functions instead of plain objects (by default all actions in Redux must be plain objects), which in turn allow you to delay dispatching of certain actions.

And as a beginner in React/Redux back then, thunks were pretty awesome. They were easy to write and understand, and didn't require any additional functions - you were basically just writing action creators, just in a different way.

However, once you start to get into the workflow with React and Redux, you realize that, although very easy to use, thunks aren't quite that good, because, 1. You can end up in callback hell, especially when making API requests, 2. You either stuff your callbacks or your reducer with business logic for handling the data (because, let's be honest, you're not going to get the perfectly formatted data EVERY time, especially if you use third-party APIs), and 3. They're not really testable (you'd have to use spy methods to check whether dispatch has been called with the right object). So, I started to research for other possible solutions that would be a better fit. That's when I ran into Redux-Saga.

Redux Saga was very close to what I was looking for. From its website, The mental model is that a saga is like a separate thread in your application that's solely responsible for side effects. What that basically means is that sagas run separately from your main application and listen for dispatched actions - once the action that that particular saga is listening for is dispatched it executes some code which produces side effects, like an API call. It also allows you to dispatch other actions from within the sagas, and is easily testable, since sagas return Effects which are plain objects. Sounds great, right?

Redux-Saga DOES come with a tradeoff, and a big one for most devs - it utilizes Javascript's generator functions, which have a pretty steep learning curve. Now, props (see what I did there, hehe) to the Redux Saga creators for using this powerful feature of JS, however, I do feel that generator functions feel pretty unnatural to use, at least for me, and even though I know how they work and how to use them, I just couldn't get around to actually using them. It's like that band or singer that you don't really have a problem listening to when they're played on the radio, but you would never even think about playing them on your own. Which is why my search for the async-handling Redux middleware continued.

One more thing Redux-Saga doesn't handle very nicely is cancellation of already dispatched async actions - such as an API call (something Redux Observable does very nicely due to its reactive nature).

The next step

A week or so ago, I was looking at an old Android project a friend and I had written for college and saw some RxJava code in there, and thought to myself: what if there's a Reactive middleware for Redux? So I did some research and, well, the gods heard my prayers: Cue Redux Observable.

So what is Redux Observable? It is another middleware for Redux that lets you handle asynchronous data flow in a functional, reactive and declarative way. What does this mean? It means that you write code that works with asynchronous data streams. In other words, you basically listen for new values on those streams (subscribe to the streams) and react to those values accordingly.

For the most in-depth guides on reactive programming in general, check out this link, and this link. Both give a very good overview on what (Functional) Reactive Programming is and give you a very good mental model.

What problems does Redux Observable solve?

The most important question when looking at a new library/tool/framework is how it's going to help you in your work. In general, everything that Redux Observable does, Redux-Saga does as well. It moves your logic outside of your action creators, it does an excellent job at handling asynchronous operations, and is easily testable. However, IN MY OPINION, Redux Observable's entire workflow just feels more natural to work with, considering that both of these have a steep learning curve (both generators and reactive programming are a bit hard to grasp at first as they not only require learning but also adapting your mindset).

From the Redux Observable official guide: The pattern of handling side effects this way is similar to the "process manager" pattern, sometimes called a "saga", but the original definition of saga is not truly applicable. If you're familiar with redux-saga, redux-observable is very similar. But because it uses RxJS it is much more declarative and you utilize and expand your existing RxJS abilities.

Can we start coding now?

So, now that you know what functional reactive programming is, and if you're like me, you really like how natural it feels to work with data. Time to apply this concept to your React/Redux applications.

First of all, as any Redux middleware, you have to add it to your Redux application when creating the store.

First, to install it, run

npm install --save rxjs rxjs-compat redux-observable
or


yarn add rxjs rxjs-compat redux-observable



depending on the tool that you're using.

Now, the basis of Redux Observable are epics. Epics are similar to sagas in Redux-Saga, the difference being that instead of waiting for an action to dispatch and delegating the action to a worker, then pausing execution until another action of the same type comes using the yield keyword, epics run separately and listen to a stream of actions, and then reacting when a specific action is received on the stream. The main component is the ActionsObservable in Redux-Observable which extends the Observable from RxJS. This observable represents a stream of actions, and every time you dispatch an action from your application it is added onto the stream.

Okay, let's start by creating our Redux store and adding Redux Observable middleware to it (small reminder, to bootstrap a React project you can use the create-react-app CLI). After we're sure that we have all the dependencies installed (redux, react-redux, rxjs, rxjs-compat, redux-observable), we can start by modifying our index.js file to look like this

import React from 'react'; 
import ReactDOM from 'react-dom';
import './index.css';
import App from './App'; import { createStore, applyMiddleware } from 'redux';
import { createEpicMiddleware } from 'redux-observable';
import { Provider } from 'react-redux'; const epicMiddleware = createEpicMiddleware(rootEpic); const store = createStore(rootReducer, applyMiddleware(epicMiddleware)); const appWithProvider = ( <Provider store={store}> <App /> </Provider>
); ReactDOM.render(appWithProvider, document.getElementById('root'));

As you might have noticed, we're missing the rootEpic and rootReducer. Don't worry about this, we'll add them later. For now, let's take a look at what's going on here:

First of all, we're importing the necessary functions to create our store and apply our middleware. After that, we're using the createEpicMiddleware from Redux Observable to create our middleware, and pass it the root epic (which we'll get to in a moment). Then we create our store using the createStore function and pass it our root reducer and apply the epic middleware to the store.

Okay, now that we have everything set up, let's first create our root reducer. Create a new folder called reducers, and in it, a new file called root.js. Add the following code to it:

const initialState = { whiskies: [], // for this example we'll make an app that fetches and lists whiskies isLoading: false, error: false 
}; export default function rootReducer(state = initialState, action) { switch (action.type) { default: return state; }
}

Anyone familiar with Redux already knows what's going on here - we're creating a reducer function which takes state and action as parameters, and depending on the action type it returns a new state (since we don't have any actions defined yet, we just add the default block and return the unmodified state).

Now, go back to your index.js file and add the following import:

import rootReducer from './reducers/root'; 

As you can see, now we don't have the error about rootReducer not existing. Now let's create our root epic; first, create a new folder epics and in it create a file called index.js. In it, add the following code for now:

import { combineEpics } from 'redux-observable'; export const rootEpic = combineEpics(); 

Here we're just using the provided combineEpics function from Redux Observable to combine our (as of now, nonexistent) epics and assign that value to a constant which we export. We should probably fix our other error in the entry index.js file now by simply adding the following import:

import { rootEpic } from './epics'; 

Great! Now that we handled all the configuration, we can go and define the types of actions that we can dispatch and also action creators for those whiskies.

To get started, create a new folder called actions and an index.js file inside.

(Note: for large, production-grade projects you should group your actions, reducers and epics in a logical way instead of putting it all in one file, however, it makes no sense here since our app is very small)

Before we start writing code, let's think about what types of actions we can dispatch. Normally, we would need an action to notify Redux/Redux-Observable that it should start fetching the whiskies, let's call that action FETCH_WHISKIES. Since this is an async action, we don't know when exactly it will finish, so we will want to dispatch a FETCH_WHISKIES_SUCCESS action whenever the call completes successfully. In similar manner, since this is an API call and it can fail we would like to notify our user with a message, hence we would dispatch a FETCH_WHISKIES_FAILURE action and handle it by showing an error message.

Let's define these actions (and their action creators) in code:

export const FETCH_WHISKIES = 'FETCH_WHISKYS';
export const FETCH_WHISKIES_SUCCESS = 'FETCH_WHISKYS_SUCCESS'; 
export const FETCH_WHISKIES_FAILURE = 'FETCH_WHISKYS_FAILURE'; export const fetchWhiskies = () => ({ type: FETCH_WHISKIES,
}); export const fetchWhiskiesSuccess = (whiskies) => ({ type: FETCH_WHISKIES_SUCCESS, payload: whiskies
}); export const fetchWhiskiesFailure = (message) => ({ type: FETCH_WHISKIES_FAILURE, payload: message
});

For anyone who is unclear about what I'm doing here, I'm simply defining constants for the action types and then using the lambda shorthand notation of ES6 I am creating arrow functions which return a plain object containing a type and (optional) payload property. The type is used to identify what kind of action has been dispatched and the payload is how you send data to the reducers (and the store) when dispatching actions (note: the second property doesn't have to be called payload, you can name it anything you want, I'm doing it this way simply because of consistency).

Now that we've created our actions and action creators, let's go and handle these actions in our reducer:

Update your reducers/index.js to the following.

import { FETCH_WHISKIES, FETCH_WHISKIES_FAILURE, FETCH_WHISKIES_SUCCESS 
} from '../actions'; const initialState = { whiskies: [], isLoading: false, error: null
}; export default function rootReducer(state = initialState, action) { switch (action.type) { case FETCH_WHISKIES: return { ...state, // whenever we want to fetch the whiskies, set isLoading to true to show a spinner isLoading: true, error: null }; case FETCH_WHISKIES_SUCCESS: return { whiskies: [...action.payload], // whenever the fetching finishes, we stop showing the spinner and then show the data isLoading: false, error: null }; case FETCH_WHISKIES_FAILURE: return { whiskies: [], isLoading: false, // same as FETCH_WHISKIES_SUCCESS, but instead of data we will show an error message error: action.payload }; default: return state; }
}

Now that we've done all that, we can FINALLY write some Redux-Observable code (sorry for taking so long!)

Go to your epics/index.js file and let's create our first epic. To start off, you're going to need to add some imports:

import { Observable } from 'rxjs'; 
import 'rxjs/add/operator/switchMap';
import 'rxjs/add/operator/map';
import 'rxjs/add/observable/of';
import 'rxjs/add/operator/catch';
import { ajax } from 'rxjs/observable/dom/ajax'; import { FETCH_WHISKIES, fetchWhiskiesFailure, fetchWhiskiesSuccess
} from "../actions";

What we did here is import the action creators that we will need to dispatch as well as the action type that we will need to watch for in the action stream, and some operators from RxJS as well as the Observable. Note that neither RxJS nor Redux Observable import the operators automatically, therefore you have to import them by yourself (another option is to import the entire 'rxjs' module in your entry index.js, however I would not recommend this as it will give you large bundle sizes). Okay, let's go through these operators that we've imported and what they do:

map - similar to Javascript's native Array.map(), map executes a function over each item in the stream and returns a new stream/Observable with the mapped items.

of - creates an Observable/stream out of a non-Observable value (it can be a primitive, an object, a function, anything).

ajax - is the provided RxJS module for doing AJAX requests; we will use this to call the API.

catch - is used for catching any errors that may have occured

switchMap - is the most complicated of these. What it does is, it takes a function which returns an Observable, and every time this inner Observable emits a value, it merges that value to the outer Observable (the one upon which switchMap is called). Here's the catch tho, every time a new inner Observable is created, the outer Observable subscribes to it (i.e listens for values and merges them to itself), and cancels all other subscriptions to the previously emitted Observables. This is useful for situations where we don't care whether the previous results have succeeded or have been cancelled. For example, when we dispatch multiple actions for fetching the whiskies we only want the latest result, switchMap does exactly that, it will subscribe to the latest result and merge it to the outer Observable and discard the previous requests if they still haven't completed. When creating POST requests you usually care about whether the previous request has completed or not, and that's when mergeMap is used. mergeMap does the same except it doesn't unsubscribe from the previous Observables.

With that in mind, let's see how the Epic for fetching the whiskies will look like:

const url = 'https://evening-citadel-85778.herokuapp.com/whiskey/'; // The API for the whiskies 
/
The API returns the data in the following format: { "count": number, "next": "url to next page", "previous": "url to previous page", "results: array of whiskies } since we are only interested in the results array we will have to use map on our observable */ function fetchWhiskiesEpic(action$) { // action$ is a stream of actions // action$.ofType is the outer Observable return action$ .ofType(FETCHWHISKIES) // ofType(FETCHWHISKIES) is just a simpler version of .filter(x => x.type === FETCHWHISKIES) .switchMap(() => { // ajax calls from Observable return observables. This is how we generate the inner Observable return ajax .getJSON(url) // getJSON simply sends a GET request with Content-Type application/json .map(data => data.results) // get the data and extract only the results .map(whiskies => whiskies.map(whisky => ({ id: whisky.id, title: "whisky.title," imageUrl: whisky.imgurl })))// we need to iterate over the whiskies and get only the properties we need // filter out whiskies without image URLs (for convenience only) .map(whiskies => whiskies.filter(whisky => !!whisky.imageUrl)) // at the end our inner Observable has a stream of an array of whisky objects which will be merged into the outer Observable }) .map(whiskies => fetchWhiskiesSuccess(whiskies)) // map the resulting array to an action of type FETCHWHISKIESSUCCESS // every action that is contained in the stream returned from the epic is dispatched to Redux, this is why we map the actions to streams. // if an error occurs, create an Observable of the action to be dispatched on error. Unlike other operators, catch does not explicitly return an Observable. .catch(error => Observable.of(fetchWhiskiesFailure(error.message)))
}

After this, there's one more thing remaining, and that is to add our epic to the combineEpics function call, like this:

export const rootEpic = combineEpics(fetchWhiskiesEpic); 

Okay, there's a lot going on here, I'll give you that. But let's break it apart piece by piece.

ajax.getJSON(url) returns an Observable with the data from the request as a value in the stream.

.map(data => data.results) takes all values (in this case only 1) from the Observable, gets the results property from the response and returns a new Observable with the new value (i.e only the results array).

.map(whiskies => whiskies.map(whisky => ({ id: whisky.id, title: "whisky.title," imageUrl: whisky.imgurl }))) 

takes the value from the previous observable (the results array), calls Array.map() on it, and maps each element of the array (each whisky) to create a new array of objects which only hold the id, title and imageUrl of each whisky, since we don't need anything else.

.map(whiskies => whiskies.filter(whisky => !!whisky.imageUrl)) takes the array in the Observable and returns a new Observable with the filtered array.

The switchMap that wraps this code takes this Observable and merges the inner Observable's stream to the stream of the Observable that calls switchMap. If another request for the whiskies fetch came through, this operation would be repeated again and the previous result discarded, thanks to switchMap.

.map(whiskies => fetchWhiskiesSuccess(whiskies)) simply takes this new value we added to the stream and maps it to an action of type FETCHWHISKIESSUCCESS which will be dispatched after the Observable is returned from the Epic.

.catch(error => Observable.of(fetchWhiskiesFailure(error.message))) catches any errors that might have happened and simply returns an Observable. This Observable is then propagated through switchMap which again merges it to the outer Observable and we get an action of type FETCHWHISKIESFAILURE in the stream.

Take your time with this, it is a complicated process which, if you haven't ever touched Reactive programming and RxJS, can look and sound very scary (read those links I provided above!).

After this, all we need to do is render a UI, which will have a button that dispatches the action and a table to show the data. Let's do that; start off by creating a new folder called components and a new component called Whisky.jsx.

import React from 'react'; const Whisky = ({ whisky }) => ( <div> <img style={{ width: '300px', height: '300px' }} src={whisky.imageUrl} /> 
<h3>{whisky.title}</h3>
</div>
); export default Whisky;

This component simply renders a single whisky item, its image and title. (Please, for the love of God, never use inline styles. I'm doing them here because it's a simple example).

Now we want to render a grid of whisky elements. Let's create a new component called WhiskyGrid.jsx.

import React from 'react'; import Whisky from './Whisky'; const WhiskyGrid = ({ whiskies }) => ( <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr' }}> {whiskies.map(whisky => (<Whisky key={whisky.id} whisky={whisky} />))} 
</div>
); export default WhiskyGrid;

What WhiskyGrid does is it leverages CSS-Grid and creates a grid of 3 elements per row, simply takes the whiskies array which we will pass in as props and maps each whisky to a Whisky component.

Now let's take a look at our App.js:

import React, { Component } from 'react'; 
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import './App.css'; import { fetchWhiskies } from './actions'; import WhiskyGrid from './components/WhiskyGrid'; class App extends Component { render() { const { fetchWhiskies, isLoading, error, whiskies } = this.props; return ( <div className="App"> <button onClick={fetchWhiskies}>Fetch whiskies</button>
{isLoading && <h1>Fetching data</h1>}
{!isLoading && !error && <WhiskyGrid whiskies={whiskies} />}
{error && <h1>{error}</h1>}
</div>
); }
} const mapStateToProps = state => ({ ...state }); const mapDispatchToProps = dispatch => bindActionCreators({ fetchWhiskies }, dispatch); export default connect(mapStateToProps, mapDispatchToProps)(App);

As you can see, there's lots of modifications here. First we have to bind the Redux store and action creators to the component's props. We use the connect HOC from react-redux to do so. After that, we create a div which has a button whose onClick is set to call the fetchWhiskies action creator, now bound to dispatch. Clicking the button will dispatch the FETCHWHISKIES action and our Redux Observable epic will pick it up, thus calling the API. Next we have a condition where if the isLoading property is true in the Redux store (the FETCH_WHISKIES has been dispatched but has neither completed nor thrown an error) we show a text saying Load data. If the data is not loading and there is no error we render the WhiskyGrid component and pass the whiskies from Redux as a prop. If error is not null we render the error message.

Conclusion

Going reactive is not easy. It presents a completely different programming paradigm and it forces you to think in a different manner. I will not say that functional is better than object-oriented or that going Reactive is the best. The best programming paradigm, IN MY OPINION, is a combination of paradigms. However, I do believe that Redux Observable does provide a great alternative to other async Redux middleware and after you pass the learning curve, you are gifted with an amazing, natural method of handling asynchronous events.

If you have any questions please ask in the comments! If this gets enough interest we can look into delaying and cancelling actions.

Cheers :)


Tag cloud