Slide 1

Slide 1 text

redux-saga Or how I learned to stop worrying and love huge SPAs

Slide 2

Slide 2 text

REDUX ARCHITECTURE

Slide 3

Slide 3 text

REDUX ARCHITECTURE // component const fireEvent = () => { return this.props.dispatch({ type: 'ADD_TODO', payload: { … }} } // todos reducer function todos(state = [], action) { switch (action.type) { case 'ADD_TODO': return state.concat(action.payload) case ‘DELETE_TODO': default: return state } } export todos

Slide 4

Slide 4 text

REDUX ARCHITECTURE // on app initialisation: import { combineReducers, createStore } from 'redux' import todos from './reducers/todos' import other from './reducers/other' const rootReducer = combineReducers({ todos, other }) const store = createStore(rootReducer)

Slide 5

Slide 5 text

ASYNC FLOWS // in the reducer function todos(state = {}, action) { switch (action.type) { ... case 'FETCH_TODOS': fetchTodos({ page: 2 }) // boo return { loading: state, todos: [] }; ... } } // redux-thunk const fetchTodos = (params) => { return (dispatch) => { dispatch('FETCH_TODOS_REQUEST'); return api.get(‘http://api.com/todos', params) .then((data) => dispatch('FETCH_TODOS_SUCCESS', data)) .cach((error) => dispatch('FETCH_TODOS_ERROR', error)) } }

Slide 6

Slide 6 text

https://github.com/redux-saga/redux-saga “redux-saga is a library that aims to make application side effects (i.e. asynchronous things like data fetching and impure things like accessing the browser cache) easier to manage, more efficient to execute, easy to test, and better at handling failures.” The mental model is that a saga is like a separate thread in your application that's solely responsible for side effects. redux-saga is a redux middleware, which means this thread can be started, paused and cancelled from the main application with normal redux actions, it has access to the full redux application state and it can dispatch redux actions as well.

Slide 7

Slide 7 text

GENERATORS function* idMaker() { var index = 0; while(true) yield index++; } var gen = idMaker(); console.log(gen.next()); // { value: 0, done: false } console.log(gen.next()); // { value: 1, done: false } console.log(gen.next()); // { value: 2, done: false }

Slide 8

Slide 8 text

GENERATORS function* namesEmitter() { yield "William"; yield "Jacob"; const nextName = yield "James"; yield nextName; } var gen = namesEmitter(); console.log(gen.next()); // { value: "William", done: false } console.log(gen.next()); // { value: "Jacob", done: false } console.log(gen.next("Barry")); // { value: "James", done: false } console.log(gen.next()); // { value: "Barry", done: true } console.log(gen.next()); // { value: undefined, done: true } console.log(gen.next()); // { value: undefined, done: true }

Slide 9

Slide 9 text

SAGA AS THREADS ... case 'FETCH_TODOS': fetchTodos({ page: 2 }) return state; ... // => import { take, call, fork } from 'redux-saga/effects' function * watchFetchTodos = () => { while (true) { const { payload } = yield take('FETCH_TODOS') // simplified yield fork(fetchTodos, payload) // calls fetchTodos(payload) } }

Slide 10

Slide 10 text

SAGA AS THREADS … dispatch('FETCH_TODOS_REQUEST'); return api.get('http://api.com/todos', params) .then((data) => dispatch('FETCH_TODOS_SUCCESS', data)) .catch((error) => dispatch('FETCH_TODOS_ERROR', error)) … // to import { take, call, put } from 'redux-saga/effects' function * fetchTodos = (params) => { yield put('FETCH_TODOS_REQUEST') const { success, data, error } = yield call(api.get, 'url', params) if (success) { yield put('FETCH_TODOS_SUCCESS', data); // simplified } else { yield put('FETCH_TODOS_ERROR', params) // again simplified } }

Slide 11

Slide 11 text

SAGA MIDDLEWARE // before: import { combineReducers, createStore } from 'redux' import todos from './reducers/todos' import other from './reducers/other' const rootReducer = combineReducers({ todos, other }) const store = createStore(rootReducer)

Slide 12

Slide 12 text

SAGA MIDDLEWARE // after: import { createStore, applyMiddleware } from 'redux' import createSagaMiddleware from 'redux-saga' import todosReducer from './reducers/todos' import otherReducer from './reducers/other' import watchFetchTodos from './sagas/todos' const sagaMiddleware = createSagaMiddleware() const rootReducer = combineReducers({ todos, other }) const store = createStore( combineReducers(todosReducer, otherReducer), applyMiddleware(sagaMiddleware) ) // then run the saga sagaMiddleware.run(watchFetchTodos) // render the application

Slide 13

Slide 13 text

COMBINING SAGAS // sagas/todos export function * rootTodosSaga () { yield fork([ watchFetchTodos, watchCreateTodo, ... ]) } // sagas/index.js import { rootTodosSaga } from './todos' ... export function * rootSaga () { yield fork([ rootTodosSaga, rootOtherSaga, ... ]) }

Slide 14

Slide 14 text

SAGA MIDDLEWARE II // after: import { createStore, applyMiddleware } from 'redux' import createSagaMiddleware from 'redux-saga' import rootReducer from './reducers' import rootSaga from './sagas' const sagaMiddleware = createSagaMiddleware() const store = createStore( rootReducer applyMiddleware(sagaMiddleware) ) // then run the saga sagaMiddleware.run(rootSaga) // render the application

Slide 15

Slide 15 text

TESTING const fetchTodosAction = createAction(‘FETCH_TODOS') function * watchFetchTodos = () => { while (true) { const { payload } = yield take(fetchTodosAction) yield fork(fetchTodos, payload) } } // testing it('triggers fetchTodos', () => { const gen = watchFetchTodos(); expect(gen.next().value).to.eql(take(fetchTodosAction)) expect(gen.next(fetchTodosAction).value).to.eql(fork(fetchTodos, fetchTodosAction.payload)) }

Slide 16

Slide 16 text

TESTING function * fetchTodos = (params) => { yield put('FETCH_TODOS_REQUEST') const { success, data, error } = yield call(api.get, 'url', params) if (success) { yield put('FETCH_TODOS_SUCCESS', data); // simplified } else { yield put('FETCH_TODOS_ERROR', params) // simplified } } it('handles success correctly', () => { const gen = fetchTodos(); expect(gen.next().value).to.eql(put(...)) expect(gen.next().value).to eql(call(api.get, ...)) expect(gen.next({ success: true, data: 10 })).to.eql(put(...)) // successAction expect(gen.next().value).to.eql(undefined) // all done })

Slide 17

Slide 17 text

TESTING function * fetchTodos = (params) => { yield put('FETCH_TODOS_REQUEST') const { success, data, error } = yield call(api.get, 'url', params) if (success) { yield put('FETCH_TODOS_SUCCESS', data); // simplified } else { yield put('FETCH_TODOS_ERROR', params) // simplified } } it('handles failure correctly', () => { const gen = fetchTodos(); expect(gen.next().value).to.eql(put(...)) expect(gen.next().value).to eql(call(api.get, ...)) expect(gen.next({ error: “oops”)).to.eql(put(…)) // failureAction expect(gen.next().value).to.eql(undefined) // all done })

Slide 18

Slide 18 text

OTHER EFFECTS // takeEvery function * watchLikeButton = () => { while (true) { const { payload } = yield takeEvery('LIKE_CLICK') // simplified yield fork(handleLikeClick, payload) } } // takeLatest function * watchAutocomplete = () => { while (true) { const { payload } = yield takeLatest('AUTOCOMPLETE_SEARCH') // simplified yield call(handleAutocomplete, payload) } }

Slide 19

Slide 19 text

OTHER EFFECTS // race && delay function * slowApiCall = () => { const { complete, timedOut } = yield race({ complete: call(api.get, '....'), timedOut: delay(2000) }) ... }

Slide 20

Slide 20 text

OTHER EFFECTS // select function * fetchTodos = (params) => { yield put('FETCH_TODOS_REQUEST') const pagination = yield select((state) => state.paginators.todos) const { success, data, error } = yield call(api.get, 'url', params.merge(pagination.urlParams)) if (success) { yield put('FETCH_TODOS_SUCCESS', data); // simplified } else { yield put('FETCH_TODOS_ERROR', params) // simplified } }

Slide 21

Slide 21 text

SUMMARY - Useful at scale e.g. 100s of unique actions, 100s of unique async workflows, routing, API requests, due to composition, isolation of frontend side effects, and ease of testing. (Typescript types available too) - Easy to hot-reload in development as sagas are suspendible and replaceable. - Definitely not required for simple React/Redux apps, cognitive overhead and plenty of plumbing - Elm <=> Redux-loop (reducers return state and Cmd, middleware handles Cmd) - Async/Await is an option, but different testing strategy required - Can’t speak for redux-observable or RxJS

Slide 22

Slide 22 text

RESOURCES https://github.com/redux-saga/redux-saga/issues https://medium.freecodecamp.org/redux-saga-common-patterns-48437892e11c https://www.slideshare.net/nachomartin/redux-sagas-react-alicante https://speakerdeck.com/ryyppy/manage-side-effects-efficiently-with-redux-sagas https://github.com/reduxjs/redux/issues/1139