Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Manage side-effects efficiently with redux-sagas

Manage side-effects efficiently with redux-sagas

Redux sure does look clean in your daily Todo-Showcase-Example... it separates your code in it's different concerns and leverages FRP concepts with easily testable pure functions.

But while you develop your actual app, you start to realize that asynchronous http requests will mess up your totally awesome pure action-creators and makes them harder to unit-test.There are many ways to deal with side-effects... Thunks, middleware, Promises,... but they all seem more like a compromise than a solution.

In this talk, I will show you how to deal with side-effects by using redux-sagas and will explain how this system works in an actual game project.

Presented on:
March 17th, 2016 - ReactJS Vienna Meetup
June 3rd, 2016 - React-Europe 2016, Paris

Patrick Stapfer

March 17, 2016
Tweet

More Decks by Patrick Stapfer

Other Decks in Programming

Transcript

  1. Manage side-effects efficiently
    with
    Redux Sagas
    Patrick Stapfer
    @ryyppy
    JavaScript-Engineer / Runtastic
    St.Patrick’s Day 2016 - ReactJS Meetup Vienna

    View full-size slide

  2. redux-saga?
    “A redux-middleware to declaratively control
    async behavior in a synchronous style”

    View full-size slide

  3. Features
    ● Leverages generator functions (sagas) for emitting saga actions
    ● Enables thread-like behavior by running multiple sagas in parallel
    ● Provides mechanisms to manage async concurrency
    ● Makes action-creators pure, since all async code lives in the sagas
    ● Sagas are composable (takes advantage of the generator syntax)
    But how does this work?

    View full-size slide

  4. function* countGenerator(until) {
    for(let i = 0; i < until; i++) {
    yield i;
    }
    }
    const gen = countGenerator(3);
    console.log(gen.next()); // { value: 0, done: false }
    console.log(gen.next().value); //1
    console.log(gen.next().value); //2
    console.log(gen.next().done); // true -> ‘return’ reached
    ● Generators can yield multiple
    results during execution
    ● Generators are constructed via
    function* (no new keyword
    allowed!)
    ● yield will pause the fn execution
    until iter.next() is called
    ● Generator specs not yet official,
    but usable via babel-transform
    ● Requires babel regenerator
    runtime (import ‘babel-polyfill’ )
    function* & yield

    View full-size slide

  5. function* countGenerator(until) {
    for(let i = 0; i < until; i++) {
    yield i;
    }
    }
    function* countMoreGenerator(until) {
    yield* countGenerator(until);
    yield 200;
    }
    const gen = countMoreGenerator(3);
    console.log(gen.next().value); //0
    console.log(gen.next().value); //1
    console.log(gen.next().value); //2
    console.log(gen.next().value); //200
    ● yield* (called ‘yield-each’) takes
    another generator to yield all
    values from
    ● Useful for composing multiple
    generators into one
    yield* keyword

    View full-size slide

  6. Requirements:
    ● Query an URL via fetch()
    ● Should trigger on HTTP_FETCH
    ● After fetching, dispatch...
    ○ HTTP_FETCH_SUCCESS
    ○ HTTP_FETCH_FAILURE
    sagaMiddleware
    rootSaga*
    watchFetch*

    View full-size slide

  7. import createSagaMiddleware from ‘redux-saga’;
    import rootSaga from ‘../sagas’;
    const sagaMiddleware = createSagaMiddleware(rootSaga);
    const enhancer = applyMiddleware(myMiddleware, otherMiddleware);
    const store = createStore(myReducer, INIT_STATE, enhancer);
    sagaMiddleware
    rootSaga*
    watchFetch*
    ● Typical Redux Middleware
    ● Will listen for Saga effects:
    ○ take, put, call
    ○ fork, (race, cancel, join,...)
    ● Usually, rootSaga should emit
    an indefinite number of effects

    View full-size slide

  8. export default function* rootSaga(getState){
    yield fork(watchFetch, getState, httpGet);
    }
    {
    ‘@@redux-saga/IO’: true,
    FORK: {
    context: null,
    fn: [Function: watchFetch],
    args: [ [Function: httpGetLink] ]
    }
    }
    sagaMiddleware
    rootSaga*
    watchFetch*
    ● On initialisation, the MW retrieves
    all the effects
    ● fork() tells the MW to perform a
    non-blocking call of watchFetch

    View full-size slide

  9. function* watchFetch(getState, httpGet) {
    while (true) {
    const action = yield take(HTTP_FETCH);
    const { url, options } = action;
    try {
    let data = yield call(httpGet, url, options);
    yield put(fetchSuccess(data));
    } catch(err) {
    yield put(fetchFailure(err));
    }
    }
    }
    sagaMiddleware
    rootSaga*
    watchFetch*
    ● take() waits for an specific pattern to
    happen, MW feeds action data to saga
    ● BUT: call() is blocking!
    ● We want to fetch urls concurrently!
    Almost working...

    View full-size slide

  10. function* fetchSaga(httpGet, action) {
    const { url, options } = action;
    try {
    let data = yield call(httpGet, url, options);
    yield put(fetchSuccess(data));
    } catch(err) {
    yield put(fetchFailure(err));
    }
    }
    function* watchFetch(httpGet) {
    yield* takeEvery(HTTP_FETCH, fetchSaga, httpGet);
    }
    sagaMiddleware
    rootSaga*
    watchFetch*
    ● takeEvery()helps us spawn new
    fetchSaga calls on each HTTP_FETCH
    ● yield* will yield yielded values of
    fetchSaga (generator composition)
    This is how we want it

    View full-size slide

  11. watchFetch*
    So how do we start our fetch call?
    store.dispatch({
    type: ‘HTTP_FETCH’,
    url: ‘https://twitter.com/ryyppy’
    });
    sagaMiddleware
    “saga action queue”
    put({
    type: ‘HTTP_FETCH_SUCCESS,
    data: {...}
    })
    normal action dispatch
    take(HTTP_FETCH)
    notify sagas listening for action
    ‘HTTP_FETCH’ via take()
    action object
    1, 2, 3 … n
    rootReducer
    { type: ‘HTTP_FETCH_SUCCESS’, data: {...}}

    View full-size slide

  12. Advantages
    ● Easy concurrency model with takeLatest() , race() or cancel()
    ● Isolates side-effect code to a single domain of the application (sagas)
    ● Composition of multiple sagas
    ● Reduces promise boilerplate
    ● Action-Creators are pure
    ● So much easier to test!

    View full-size slide

  13. import { call, put } from 'react-saga/effects';
    import { fetchSuccess } from '../actions';
    import { fetchSaga } from '../sagas';
    function* fetchSaga(httpGet, action) {
    const { url, options } = action;
    try {
    let data = yield call(httpGet, url, options);
    yield put(fetchSuccess(data));
    } catch(err) {
    yield put(fetchFailure(err));
    }
    }
    function* watchFetch(httpGet) {
    yield* takeEvery(HTTP_FETCH, fetchSaga, httpGet);
    }
    import { call, put } from 'react-saga/effects';
    import { fetchSuccess } from '../actions';
    import { fetchSaga } from '../sagas';
    describe('fetchSaga', () => {
    describe('should call httpGet with url extracted from action', () => {
    const httpGet = () => {};
    const action = {
    type: 'HTTP_FETCH',
    url: 'https://twitter.com/ryyppy',
    options: {},
    };
    let ret;
    const iter = fetchSaga(httpGet, action);
    // Returns the first yield value and injects the data returned by this call
    // (that is what the saga-middleware does in the real implementation)
    ret = iter.next({ data: 'https://twitter.com/ryyppy});
    // iter.throw('Some error') // if you wanna test the catch() condition
    expect(ret.value).to.deep.equal(call(httpGet, 'https://twitter.com/ryyppy', {}))
    // Next yield should emit the put(...) effect
    ret = iter.next();
    expect(ret.value).to.deep.equal(
    put(fetchSuccess({ data: 'https://twitter.com/ryyppy}))
    );
    });
    });
    IMPLEMENTATION TEST

    View full-size slide

  14. Let’s build a game!
    Requirements:
    ● Keyboard handling
    ○ Enter, Backspace, A-Z letters
    ● Spawn rows on the board
    ○ Only if game is running
    ○ In a specific speed (score dependent)
    ○ => Gameloop
    ● Handle game logic
    ○ Remove successfully entered words from the
    board
    ○ Check loose-conditions

    View full-size slide

  15. Code Demo
    https://github.com/ryyppy/redux-word-gl

    View full-size slide

  16. Appendix / References
    ● https://github.com/yelouafi/redux-saga
    ● https://github.com/yelouafi/redux-saga/tree/master/docs/api
    ● https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*
    ● https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators

    View full-size slide