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

redux-saga

 redux-saga

Luke Williams

July 17, 2018
Tweet

More Decks by Luke Williams

Other Decks in Programming

Transcript

  1. REDUX ARCHITECTURE // component <Button onClick={fireEvent} /> 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
  2. 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)
  3. 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)) } }
  4. 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.
  5. 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 }
  6. 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 }
  7. 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) } }
  8. 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 } }
  9. 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)
  10. 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
  11. 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, ... ]) }
  12. 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
  13. 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)) }
  14. 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 })
  15. 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 })
  16. 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) } }
  17. OTHER EFFECTS // race && delay function * slowApiCall =

    () => { const { complete, timedOut } = yield race({ complete: call(api.get, '....'), timedOut: delay(2000) }) ... }
  18. 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 } }
  19. 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