Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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?

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

No content

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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...

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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: {...}}

Slide 14

Slide 14 text

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!

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

No content

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

Questions? @ryyppy [email protected]

Slide 20

Slide 20 text

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