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
  2. 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?
  3. 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
  4. 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
  5. Requirements: • Query an URL via fetch() • Should trigger

    on HTTP_FETCH • After fetching, dispatch... ◦ HTTP_FETCH_SUCCESS ◦ HTTP_FETCH_FAILURE sagaMiddleware rootSaga* watchFetch*
  6. 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
  7. 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
  8. 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...
  9. 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
  10. 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: {...}}
  11. 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!
  12. 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
  13. 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