Reactive Redux state with RxJS

If you're like me, you're using Redux every day, to manage your state, probably with React, or any other view library or framework. You definitely know how hard is when it comes to handling async code and side effects. I know that very well, been there, tried all kinds of stuff and I think that I finally found the best way (in my opinion) to do it. Today I'd like to show you that it doesn't have to be that hard, you can still have fun while you write your actions and reducers and communicate from API or WebSockets.

What is the problem with Redux and its actions and reducers? Actions are pure functions that tell Redux what it has to do. Then it transfers that action to reducer and we get some data in the store. But it doesn't tell it how to do it. Reducers are handling the state changes but they're doing that synchronously. The problem happens when we need to wait for some async event to happen, like fetching data from the API or listening to some message from WebSocket. We need some async middleware to help us do all those async work and synchronously send data to the reducer. This is the place where RxJS and Reactive programming methodology help us!

What is Reactive programming?

Reactive programming (RP) is programming with asynchronous data streams. The idea behind RP is to create a data stream from anything, clicks, ajax calls, WebSockets etc. and to use the same API to work with them. Sounds cool? If you want to learn more about reactive programming, check gist from André Staltz https://gist.github.com/staltz/868e7e9bc2a7b8c1f754

Most popular RP library is RxJS. It has a lot of operators and it's really powerful! RxJS official documentation can help you to learn it and I also recommend learn-rxjs website. Also check RxMarbles, the interactive diagrams of Rx observables.

How can Reactive programming help me?

Good question. Reactive programming can make your state reactive and all data flow can be pure. All your actions will be buffers in streams, and you can easily manipulate with them, transform them, do async stuff like HTTP requests and send the results to the reducers.

Main advantages:

  • Abstract all your async processes and use just one API for them
  • Cancel requests
  • Have an easy way to rescue from errors
  • Better performance

How to integrate RxJS into Redux?

As you probably know, main Redux parts are actions and reducers. It also has middlewares, functions that are between actions and reducers and they're usually used for async stuff. If you probably used redux-saga you know what I am talking about.

The middleware that we're going to use is redux-observable. It's a small library and it helps us to abstract actions into streams and we can easily transform them using RxJS. It's made for RxJS v5, but any other library like Most.js or XStream can be used too. Just check the documentation.

Middlewares in redux-observable are called Epics (like Sagas in redux-saga). Epics functions that are getting the stream of actions as an argument and must return the stream of actions.

Example

Now when we know what is Reactive programming and how can it make our life easier, let's see some code and some real examples. Since React is my favorite framework, I am going to use it as a view library.

TL;DR All code is on GitHub: https://github.com/IvanJov/react-rxjs-github-search

Let's make a simple GitHub search by username. It should look like this:

The main features are:

  • Add type to search functionality
  • Show just the avatar of the user
  • Search GitHub API using RxJS ajax functionality
  • Retry to send the request if it fails less than 3 times
  • Cancel the previous action if the new one is dispatched

Let's start with the setup of a react app using create-react-app by running:

npm i -g create-react-app #if you don't have it installed
create-react-app react-rxjs-github-search
cd react-rxjs-github-search

Okay, let's now install all packages we need:

yarn add redux redux-react rxjs redux-observable

Open the index.js file and update it to look like this:

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import { Provider } from 'react-redux';
import { createStore, compose, applyMiddleware } from 'redux';
import reducer from './reducer';
import { createEpicMiddleware } from 'redux-observable';
import epics from './epics';

const epicMiddleware = createEpicMiddleware(epics);

const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;

const store = createStore(
  reducer,
  composeEnhancers(
    applyMiddleware(epicMiddleware)
  )
);

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>
, document.getElementById('root'));

Here we did the usual boilerplate stuff, initialized redux, injected reducers and then injected that into the Provider HOC. The new thing here is createEpicMiddleware. That function comes from redux-observable and it takes the combined epics and makes the epic middleware that's later injected into the redux store as a middleware. All epics will get the actions as streams thanks to this.

Let's now make the constants.js file with the constants that we're going to use in redux actions:

export const FETCH_USER = 'FETCH_USER';
export const FETCH_USER_SUCCESS = 'FETCH_USER_SUCCESS';
export const FETCH_USER_FAILED = 'FETCH_USER_FAILED';

Cool, let's make actions.js file:

import { FETCH_USER, FETCH_USER_SUCCESS, FETCH_USER_FAILED } from './constants';

export const fetchUser = username => ({
  type: FETCH_USER,
  payload: { username }
});

export const fetchUserSuccess = user => ({
  type: FETCH_USER_SUCCESS,
  payload: { user }
});

export const fetchUserFailed = () => ({
  type: FETCH_USER_FAILED
});

As you see, we have 3 actions:

  • fetchUser - we'll dispatch this action on username field change
  • fetchUserSuccess - Epic middleware will dispatch this action with the user data from GitHub if HTTP request is successful
  • fetchUserFailed - Epic middleware will dispatch this action if HTTP request has failed

We also need reducer.js:

import { FETCH_USER_SUCCESS, FETCH_USER_FAILED } from './constants';
import { combineReducers } from 'redux';

const initialState = {};

export const user = (state = initialState, action) => {
  switch (action.type) {
    case FETCH_USER_SUCCESS:
      return action.payload.user
    case FETCH_USER_FAILED:
      return {};
    default:
      return state;
  }
};

export default combineReducers({
  user
});

This reducer is simple, it just puts all data from the payload into the store if the success action is dispatched. If the fail action is dispatched, it saves the empty object in the store.

Let's now update our App.js component to match the design we want:

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { fetchUser } from './actions';

class App extends Component {
  constructor(props) {
    super(props);

    this.searchUser = this.searchUser.bind(this);
  }

  searchUser(event) {
    this.props.fetchUser(event.target.value);
  }

  render() {
    return (
      <div>
        <h2>Github Search:</h2>
        <input placeholder='Username' onChange={this.searchUser} />
        <p>
          <img src={this.props.image} alt='Not Found' width={100} />
        </p>
      </div>
    );
  }
}

const mapStateToProps = state => ({
  image: state.user.avatar_url
});

const mapDispatchToProps = {
  fetchUser
};

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(App);

Now we have everything from React and Redux to make our app work. We have connected our App component to Redux and we're going to take the image of the GitHub user from state and render that. The app will call the fetchUser action on every change of the username field. Let's now add the Epic middleware and fetch the data from the GitHub every time the fetchUser action is dispatched.

We need to create epics.js file:

import 'rxjs';
import { combineEpics } from 'redux-observable';
import { FETCH_USER } from './constants';
import { fetchUserSuccess, fetchUserFailed } from './actions';
import { ajax } from 'rxjs/observable/dom/ajax';
import { Observable } from 'rxjs';

export const fetchUser = actions$ =>
  actions$
    .ofType(FETCH_USER)
    .mergeMap(action =>
      ajax.getJSON(`https://api.github.com/users/${action.payload.username}`)
        .map(user => fetchUserSuccess(user))
        .takeUntil(actions$.ofType(FETCH_USER))
        .retry(2)
        .catch(error => Observable.of(fetchUserFailed()))
    );


export default combineEpics(
  fetchUser
);

This is the main part of this example. Without the Epic none of this will work. Let's see how it works. At the top, we import full rxjs. Don't do this on a production app, it will import everything from rxjs. Instead just import the stuff you're going to use. We also import actions and constants, ajax observable for handling HTTP requests and the Observable for making the observable from anything (function, promise, generator etc.). We also need combineEpics to combine all our epics that we want to export. It's something like combine combineReducers in redux.

The fetchUser epic does all the functionality. It's a function where the first argument is the actions$ stream (variables that are streams have $ sign at the end) and it can take store argument if you want to get data from the store. actions$ stream will get have all actions dispatched, so with ofType(FETCH_USER) operator we want to filter only ones with the type FETCH_USER.

Then we call a mergeMap operator. It maps the buffer to Observable and emits the resulted value. It takes a callback function with one argument, the current buffer in the stream. In our case, that's the current action that's dispatched. Now we want to make the ajax stream and send a request to GitHub. We use ajax for that and getJSON helper. If it successful, it will map to a fetchUserSuccess action. If it fails, it will go to a catch and map to a new Observable we make from fetchUserFailed action.

takeUntil and retry operators are the "magic" from the RxJS and that's why we want to have the reactive state. With takeUntil we cancel the current ajax request if the new action is dispatched. Since the request will be sent on every field change, we want to get only the last one and cancel all other requests. If the user types my username ivanjov, the app will send a request for i, iv, iva ... ivanjov but we want to get a response just for the last one, ivanjov. takeUntil makes that really easy and it really cancels the request. Promises can't cancel requests, they can just ignore the response from the previous one, but the browser will still wait for other requests to complete and that will kill our app.

retry is used for the user-friendly error handling. It takes one argument, the number of errors. If you give 2, like we did in this example, it will retry to send the request after 2 errors. If the 3rd error occurs, it will go to catch. This is great because sometimes the user needs to fill some big form and when he clicks Save button, API can be down for 2-3 secs, for example, he'll get an error, and we don't want that. It's much nicer to try again the couple more times, API can come up again and the data will be saved.

At the end, to make everything centered, update the index.css file:

body {
  margin: 0;
  padding: 0;
  font-family: sans-serif;
  text-align: center;
}

Testing

Let's see how can we write the unit tests for the Epics. It's probably not a perfect way, but it's probably the easiest one to make sure that Epic is really doing what it's supposed to do.

First, we'll need to install the packages for testing:

yarn add redux-mock-store nock xhr2

We need redux-mock-store to mock the redux store for the test, nock to mock the GitHub URL and xhr2 to make RxJS ajax work in Node, because it's made for using XHR from the browser.

Let's now make epics.test.js file and add our 2 tests:

import nock from 'nock';
import configureMockStore from 'redux-mock-store';
import { createEpicMiddleware } from 'redux-observable';
import { FETCH_USER, FETCH_USER_FAILED, FETCH_USER_SUCCESS } from './constants';
import epics from './epics';
import { fetchUser } from "./actions";
import XMLHttpRequest from 'xhr2';
global.XMLHttpRequest = XMLHttpRequest;

const epicMiddleware  = createEpicMiddleware(epics);
const mockStore = configureMockStore([epicMiddleware]);

describe('fetchUser', () => {
  let store;

  beforeEach(() => {
    store = mockStore();
  })

  afterEach(() => {
    nock.cleanAll();
    epicMiddleware.replaceEpic(epics);
  })

  it('returns user from github', done => {
    const payload = { username: 'user' };
    nock('https://api.github.com')
      .get('/users/user')
      .reply(200, payload);

    const expectedActions = [
      { type: FETCH_USER, payload },
      { type: FETCH_USER_SUCCESS, "payload": {"user": {"username": "user"} } }
    ];

    store.subscribe(() => {
      const actions = store.getActions();
      if (actions.length === expectedActions.length) {
        expect(actions).toEqual(expectedActions);
        done();
      }
    });

    store.dispatch(fetchUser('user'));
  });

  it('handles error', done => {
    const payload = { username: 'user' };
    nock('https://api.github.com')
      .get('/users/user')
      .reply(404);

    const expectedActions = [
      { type: FETCH_USER, payload },
      { type: FETCH_USER_FAILED }
    ];

    store.subscribe(() => {
      const actions = store.getActions();
      if (actions.length === expectedActions.length){
        expect(actions).toEqual(expectedActions);
        done();
      }
    });

    store.dispatch(fetchUser('user'));
  });
});

At the top, we import all stuff that we need and make global XMLHttpRequest variable for ajax to work. Then we make the epic middleware and mock store. configureMockStore accepts an array of the middlewares as the argument.

We have 2 tests, one for user successful GitHub request and one for the failed one. In both of them, we dispatch the fetchUser action and expect some action to be dispatched by the Epic.

Before every test, we mock the store and make the store variable. And after every the test, we clean the URLs that nock mocked and make the new Epic middleware.

The first test mocks https://api.github.com/users/user URL and returns some object with the user data. Then we define what actions we want to be successfully dispatched at the bottom of the test we dispatch the fetchUser action. In the middle, we subscribe to the store and we listen to all changes. When the number of actions dispatched is the same as the ones we're waiting for, they will be compared. If they match, the test will pass, if not, it will fail.

The same thing we do for the second test. We mock the https://api.github.com/users/user to return 404 Not Found error and check if the correct actions are dispatched.

Now run:

yarn test

and see how cool it is when all test pass 😎

This two tests will confirm that our Epics are working properly. We can also test many other scenarios, like testing if the requests are properly canceled, but that's left for you to play with 😉

Conclusion

Just give it a try. I hope that this example was good enough for you to at least try RxJS with Redux and feel the magic. It becomes so good if you have some big search forms, like the one on Booking.com website or a lot of user interactivity. You'll just use Observables for that and it will be much easier to develop complex apps.

Comments