langton-ant-dojo

Dojo : TDD Against the Time with React and Redux

This project is maintained by Bogala

Asynchronous logic with Redux

Redux reducers handle state transitions, but they must be handled synchronously.

But what about Async events like User interactions, ajax calls (with cancellation), web sockets or animations?

See more in my presentation async-reduc-observable

Observables and RxJS

What is an observable ?

What is RxJS ? It contains Observables and functions to create and compose Observables, also known as “Lodash for async”. RxJS combines the Observer and Iterator patterns, functional programming and collections in an ideal way to manage sequences of events.

Create Observables

Subscribe an Observable

myObservable.subscribe(
  value => console.log('next', value),
  err => console.roor('error', err),
  () => console.info('complete!')
);

Redux and Epics

An Epic is the core primitive of redux-observable.

It is a function that takes a stream of all actions dispatched and returns a stream of new actions to dispatch.

Refactor

Change actions name

To prepare next step, we have to change the PLAY action :

actions.ts

export const PLAY = 'PLAY';
export const PLAYED = 'PLAYED';
export const PAUSED = 'PAUSED';

Now, in the reducer, PLAY becomes PLAYED. (PLAY will be used for the loop)

export default (state: MainState = initialState, action: Action) => {
  switch (action.type) {
    case PLAYED: {
      const finalState = play(state);
      return { ...finalState };
    }
    default:
      return state;
  }
};

NPM packages

First, we have to install packages:

yarn add rxjs redux-observable

Types are included in each package. We don’t have to add any @types/rxjs or @types/redux-observable

Epic by the test

Let’s code our first test: epic.spec.ts

configure({ adapter: new Adapter() });

jest.useFakeTimers();

const epicMiddleware = createEpicMiddleware(epic);
const mockStore = configureStore([epicMiddleware]);

let store: MockStore<{}>;

describe('Epic', () => {
    beforeEach(() => {
        store = mockStore({});
    });

    afterEach(() => {
        epicMiddleware.replaceEpic(epic);
    });

    test('Dispatch played when launched', () => {
        store.dispatch({ type: PLAY });
        jest.runOnlyPendingTimers();
        expect(store.getActions()).toEqual([
            { type: PLAY },
            { type: PLAYED }
        ]);
    });
});

That makes an epic like this:

export default (action$: ActionsObservable<Action>) => 
    action$.ofType(PLAY)
        .switchMap(() =>
            Observable.interval(50)
            .mapTo({ type: PLAYED })
        );

switchMap ?

A switchMap is a comination of a switchAll and a map.

switchAll subscribes and produces values only from the most recent inner sequence ignoring previous streams.

In the diagram below you can see the H higher-order stream that produces two inner streams A and B. The switchAll operator takes values from the A stream first and then from the stream B and passes them through the resulting sequence.

Here is the code example that demonstrates the setup shown by the above diagram:

const a = stream(a, 200, 3);
const b = stream(b, 200, 3);
const h = interval(100).pipe(take(2), map(i => [a, b][i]));
h.pipe(switchAll()).subscribe(fullObserver(switchAll));

If we use directly switchMap :

const a = stream(a, 200, 3);
const b = stream(b, 200, 3);
const h = interval(100).pipe(take(2), switchMap(i => [a, b][i]));
h.subscribe(fullObserver(switchAll));

for more informations, please refer to the Max NgWizard K’s post on medium

A second test:

    test('Dispatch cancelled when paused', () => {
        store.dispatch({ type: PLAY });
        jest.runOnlyPendingTimers();
        store.dispatch({ type: PAUSED });
        jest.runOnlyPendingTimers();
        expect(store.getActions()).toEqual([
            { type: PLAY },
            { type: PLAYED },
            { type: PLAYED },
            { type: PAUSED }
        ]);
    });

Our new epic:

export default (action$: ActionsObservable<Action>) => 
    action$.ofType(PLAY)
        .switchMap(() =>
            Observable.interval(50)
            .takeUntil(action$.ofType(PAUSED))
            .mapTo({ type: PLAYED })
        );

Add Middleware

We have to change our store index.ts:

import { createStore, applyMiddleware } from 'redux';
import { createEpicMiddleware } from 'redux-observable';
import reducer from './reducer';
import { compose } from 'recompose';
import epic from './epic';

// tslint:disable-next-line:no-any
const composeEnhancers =
    // tslint:disable-next-line:no-any
    (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;

const epicMiddleware = createEpicMiddleware(epic, {
    dependencies: {  }
});

export const configureStore = () => (
    createStore(
        reducer,
        composeEnhancers(applyMiddleware(epicMiddleware))
    )
);

export {MainState, Ant} from './reducer';

Now, we are using compose to enhance previous configuration with epicMiddleWare (redux-observable).

Update App container

Our mapDispatchToProps doesn’t need interval anymore. we will use dispatch to call redux an epic middleware :

const mapDispatchToProps: MapDispatchToProps<AppEventProps, AppProps> = (dispatch, ownProps) => ({
    onPlay: () => {
        dispatch({ type: PLAY } as Action);
    },
    onPause: () => {
        dispatch({ type: PAUSED } as Action);
    }
});

But with this our test won’t work, we make one too many dispatch call when the button is clicked:

    test('Pause button stop dispatchs', async () => {
        // tslint:disable-next-line:no-any
        (store.dispatch as any).mockClear();
        
        const wrapper = 
            mount(<Provider store={store}><MemoryRouter initialEntries={['/']}><App /></MemoryRouter></Provider>);
        await wrapper.find(AvPlayArrow).simulate('click');

        jest.runOnlyPendingTimers();
        expect(store.dispatch).toHaveBeenCalled();

        await wrapper.find(AvPause).simulate('click');
        jest.runOnlyPendingTimers();
        expect(store.dispatch).toHaveBeenCalledTimes(2);
    });

    test('stop stopped should not make exception', async () => {
        // tslint:disable-next-line:no-any
        (store.dispatch as any).mockClear();
        
        const wrapper = 
            mount(<Provider store={store}><MemoryRouter initialEntries={['/']}><App /></MemoryRouter></Provider>);
        
        await wrapper.find(AvPause).simulate('click');
        jest.runOnlyPendingTimers();
        expect(store.dispatch).toHaveBeenCalledTimes(1);
    });

If you want to begin here, you can download this sources

New functional need

Please try over 900 movements… Your ant needs a bigger grid. So, now we need a dynamic size for our grid.

If your Ant is on the border of the grid:

When you’re done, you can go to the next step : Advanced Typescript

Reminders

TDD Cycles 5 Steps to reproduce every cycle:

  1. Add a new test
  2. Run all tests and verify if the new test fails
  3. Write code to pass the new test to green
  4. Run all tests and verify all are green
  5. Refactor

Before each test, launch a five minutes timer.

All of your code must be covered by unit tests.

We’ll avoid any as much as possible (implicit or not).

Exercice Solution

Download Example