Using Redux Observable For Async Stuff

Redux doesn’t handle async work too well. Thunk is the go-to solution, but it’s not always great for testing.

Here is how to do a basic async data fetch with redux observable;

export const exampleEpic = (action$, state$, { later }) =>
  action$.pipe(
    ofType('projects/updateTitle'),
    debounceTime(1000),
    switchMap(({ payload }) =>
      from(later(2000, payload)).pipe(
        map(res => ({
          type: 'projects/fetchFulfilled',
          payload: res,
        }))
      )
    )
  );

Thunks are called epics in redux observable. All epics take three parameters (action$, state$, { dependancies }). The last two are optional.

The action$ parameter is a stream of all redux actions emitted over time. state$ is the state of your redux store. dependencies can contain any side effects that you want to use in your epic. Passing in side effects as dependencies is super handy because it makes things easy to test.

Rather than responding to every redux action in the action$ stream, you listen for actions and react to the ones you want. You get your epic to only respond to specific actions by filtering out all other actions with ofType().

In the example above I’m debouncing the input, then I switch to a new observable which I create using a fake promise that resolves after 2 seconds called later. I turn the promise into an observable by wrapping it in from. Then I map the response of the promise to an action with the type 'projects/fetchFulfilled'.

I’ll admit this is overkill for such a simple example. I’d probably stick to a thunk for something like this. However, as your asynchronous logic gets more complex rxjs to start doing the heavy lifting, whereas thunks begin to melt into untestable spaghetti.

To test the epic above you use something called a test scheduler. The scheduler creates a sandbox with pretend time so that you can run your tests without having to spend real time waiting for all your tests to come in.

Here is the basic setup. I’m using jest to make assertions.

import { exampleEpic } from '../exampleEpic';
import { TestScheduler } from 'rxjs/testing';

test('example epic works as expected', () => {

  const testScheduler = new TestScheduler((actual, expected) => {
    expect(actual).toEqual(expected);
  });

  testScheduler.run(({ hot, cold, expectObservable }) => {

    const action$ = hot('a', {
      a: {
        type: 'projects/updateTitle',
        payload: 'example input',
      },
    });

    const state$ = null;

    const dependencies = {
      later: () => cold('a', { b: 'example output' }),
    };
    const output$ = stageTitleUpdate(action$, state$, dependencies);

    expectObservable(output$).toBe('1000ms x', {
      x: {
        type: 'projects/fetchFulfilled',
        payload: 'example output',
      },
    });
  });
});

const testScheduler = new TestScheduler(...) is where you run the actual assertions. All this bit does is gives you the actual and expected value so that you can compare them.

The actual value is produced here const output$ = exampleEpic(action$, state$, dependencies);. You import this exampleEpic (the one that we created earlier) at the top of this test file. Then you pass it three mock values for action$, state$, dependencies.

You create the mock action stream by using the hot method that the testScheduler provides. You define an action a on a marble diagram and then define the shape of a.

You can provide an object to mock out state and use the cold method to mock out any dependencies.

The expectObservable then lets you define the sequence of actions you expect to be emitted by the epic by using a marble diagram. Since we’re debouncing the input by 1-second the expected output is '1000mx x' instead of just 'x'.

This set up is beautiful to work with because you get a pretend time box where you can feed in actions in specific sequences using a marble diagram and then test that all the right actions come out the other end in the right sequence.

Resources #

 
0
Kudos
 
0
Kudos

Now read this

React setState takes an updater function

If you increment a count in state using a state update function 3 times then the final count will be 3. state = {count: 0} this.setState( previousState => ({count: previousState.count++})) this.setState( previousState => ({count:... Continue →