Pro Yearly is on sale from $80 to $50! »

JazzCon 2017: State, Side Effects, and Redux. Oh my!

JazzCon 2017: State, Side Effects, and Redux. Oh my!

94bd558238b69c45d3d3e15797ae94f7?s=128

Jeremy Fairbank

March 23, 2017
Tweet

Transcript

  1. State, Side Effects, and Redux Oh my! Jeremy Fairbank @elpapapollo

    / jfairbank
  2. sigient.com

  3. State

  4. Controller View Model Model Model Model Model View View View

    View MVC
  5. View Model Model Model Model Model View Model View Model

    View Model View Model Model Two-way Data Binding
  6. Disorganized Changes {...}

  7. ?

  8. None
  9. Redux Unidirectional Organized changes Read-only state

  10. Reducer View State Actions

  11. Reducer View State Actions

  12. Reducer View State Actions

  13. Reducer View React, Angular, Vue, Vanilla JS, etc. State Actions

  14. Reducer View State Actions Dispatch

  15. Reducer View State Actions Repeat

  16. Store Reducer State View

  17. Store Reducer State View

  18. Store Reducer State View

  19. Store Reducer State View

  20. Store Reducer State View Action

  21. Store Reducer State View Action

  22. Store Reducer State View

  23. Store Reducer State View

  24. Store Reducer State View

  25. Store Reducer State View

  26. let state = 0; incrementBtn.addEventListener('click', () => { state +=

    1; }); decrementBtn.addEventListener('click', () => { state -= 1; });
  27. let state = 0; incrementBtn.addEventListener('click', () => { state +=

    1; }); decrementBtn.addEventListener('click', () => { state -= 1; });
  28. let state = 0; incrementBtn.addEventListener('click', () => { state +=

    1; }); decrementBtn.addEventListener('click', () => { state -= 1; });
  29. let state = 0; state += 1; state += 1;

    state -= 1; console.log(state); // 1
  30. let state = 0; state += 1; state += 1;

    state -= 1; console.log(state); // 1
  31. let state = 0; state += 1; state += 1;

    state -= 1; console.log(state); // 1 { type: 'INCREMENT' }; { type: 'INCREMENT' }; { type: 'DECREMENT' };
  32. let state = 0; state += 1; state += 1;

    state -= 1; console.log(state); // 1 { type: 'INCREMENT' }; { type: 'INCREMENT' }; { type: 'DECREMENT' }; Action
  33. Reducer

  34. let state = 0; state = reducer(state, { type: 'INCREMENT'

    }); state = reducer(state, { type: 'INCREMENT' }); state = reducer(state, { type: 'DECREMENT' }); console.log(state); // 1
  35. let state = 0; state = reducer(state, { type: 'INCREMENT'

    }); state = reducer(state, { type: 'INCREMENT' }); state = reducer(state, { type: 'DECREMENT' }); console.log(state); // 1
  36. let state = 0; state = reducer(state, { type: 'INCREMENT'

    }); state = reducer(state, { type: 'INCREMENT' }); state = reducer(state, { type: 'DECREMENT' }); console.log(state); // 1
  37. let state = 0; state = reducer(state, { type: 'INCREMENT'

    }); state = reducer(state, { type: 'INCREMENT' }); state = reducer(state, { type: 'DECREMENT' }); console.log(state); // 1
  38. let state = 0; state = reducer(state, { type: 'INCREMENT'

    }); state = reducer(state, { type: 'INCREMENT' }); state = reducer(state, { type: 'DECREMENT' }); console.log(state); // 1 Action
  39. let state = 0; state = reducer(state, { type: 'INCREMENT'

    }); state = reducer(state, { type: 'INCREMENT' }); state = reducer(state, { type: 'DECREMENT' }); console.log(state); // 1
  40. let state = 0; state = reducer(state, { type: 'INCREMENT'

    }); state = reducer(state, { type: 'INCREMENT' }); state = reducer(state, { type: 'DECREMENT' }); console.log(state); // 1
  41. let state = 0; state = reducer(state, { type: 'INCREMENT'

    }); state = reducer(state, { type: 'INCREMENT' }); state = reducer(state, { type: 'DECREMENT' }); console.log(state); // 1
  42. let state = 0; state = reducer(state, { type: 'INCREMENT'

    }); state = reducer(state, { type: 'INCREMENT' }); state = reducer(state, { type: 'DECREMENT' }); console.log(state); // 1
  43. function reducer(state = 0, action) { switch (action.type) { case

    'INCREMENT': return state + 1; case 'DECREMENT': return state - 1; default: return state; } }
  44. function reducer(state = 0, action) { switch (action.type) { case

    'INCREMENT': return state + 1; case 'DECREMENT': return state - 1; default: return state; } }
  45. function reducer(state = 0, action) { switch (action.type) { case

    'INCREMENT': return state + 1; case 'DECREMENT': return state - 1; default: return state; } }
  46. function reducer(state = 0, action) { switch (action.type) { case

    'INCREMENT': return state + 1; case 'DECREMENT': return state - 1; default: return state; } }
  47. function reducer(state = 0, action) { switch (action.type) { case

    'INCREMENT': return state + 1; case 'DECREMENT': return state - 1; default: return state; } }
  48. function reducer(state = 0, action) { switch (action.type) { case

    'INCREMENT': return state + 1; case 'DECREMENT': return state - 1; default: return state; } }
  49. function reducer(state = 0, action) { switch (action.type) { case

    'INCREMENT': return state + 1; case 'DECREMENT': return state - 1; default: return state; } }
  50. Store

  51. import { createStore } from 'redux'; const store = createStore(reducer);

    store.getState(); // 0
  52. import { createStore } from 'redux'; const store = createStore(reducer);

    store.getState(); // 0
  53. import { createStore } from 'redux'; const store = createStore(reducer);

    store.getState(); // 0
  54. import { createStore } from 'redux'; const store = createStore(reducer);

    store.getState(); // 0
  55. store.dispatch({ type: 'INCREMENT' }); store.dispatch({ type: 'INCREMENT' }); store.dispatch({ type:

    'DECREMENT' }); store.getState(); // 1
  56. store.dispatch({ type: 'INCREMENT' }); store.dispatch({ type: 'INCREMENT' }); store.dispatch({ type:

    'DECREMENT' }); store.getState(); // 1
  57. store.dispatch({ type: 'INCREMENT' }); store.dispatch({ type: 'INCREMENT' }); store.dispatch({ type:

    'DECREMENT' }); store.getState(); // 1
  58. store.dispatch({ type: 'INCREMENT' }); store.dispatch({ type: 'INCREMENT' }); store.dispatch({ type:

    'DECREMENT' }); store.getState(); // 1
  59. store.subscribe(() => { console.log('state =', store.getState()); }); store.dispatch({ type: 'INCREMENT'

    }); store.dispatch({ type: 'INCREMENT' }); store.dispatch({ type: 'DECREMENT' }); // state = 1 // state = 2 // state = 1
  60. store.subscribe(() => { console.log('state =', store.getState()); }); store.dispatch({ type: 'INCREMENT'

    }); store.dispatch({ type: 'INCREMENT' }); store.dispatch({ type: 'DECREMENT' }); // state = 1 // state = 2 // state = 1
  61. store.subscribe(() => { console.log('state =', store.getState()); }); store.dispatch({ type: 'INCREMENT'

    }); store.dispatch({ type: 'INCREMENT' }); store.dispatch({ type: 'DECREMENT' }); // state = 1 // state = 2 // state = 1
  62. store.subscribe(() => { console.log('state =', store.getState()); }); store.dispatch({ type: 'INCREMENT'

    }); store.dispatch({ type: 'INCREMENT' }); store.dispatch({ type: 'DECREMENT' }); // state = 1 // state = 2 // state = 1
  63. Action Creators

  64. const increment = () => ({ type: 'INCREMENT', }); const

    decrement = () => ({ type: 'DECREMENT', });
  65. const increment = () => ({ type: 'INCREMENT', }); const

    decrement = () => ({ type: 'DECREMENT', });
  66. const increment = () => ({ type: 'INCREMENT', }); const

    decrement = () => ({ type: 'DECREMENT', });
  67. const unsubscribe = store.subscribe(() => { console.log('state =', store.getState()); });

    store.dispatch(increment()); store.dispatch(increment()); store.dispatch(decrement()); // state = 1 // state = 2 // state = 1
  68. const unsubscribe = store.subscribe(() => { console.log('state =', store.getState()); });

    store.dispatch(increment()); store.dispatch(increment()); store.dispatch(decrement()); // state = 1 // state = 2 // state = 1
  69. import { bindActionCreators } from 'redux'; const actions = bindActionCreators(

    { increment, decrement }, store.dispatch ); actions.increment(); actions.increment(); actions.decrement(); // state = 1 // state = 2 // state = 1
  70. import { bindActionCreators } from 'redux'; const actions = bindActionCreators(

    { increment, decrement }, store.dispatch ); actions.increment(); actions.increment(); actions.decrement(); // state = 1 // state = 2 // state = 1
  71. import { bindActionCreators } from 'redux'; const actions = bindActionCreators(

    { increment, decrement }, store.dispatch ); actions.increment(); actions.increment(); actions.decrement(); // state = 1 // state = 2 // state = 1
  72. import { bindActionCreators } from 'redux'; const actions = bindActionCreators(

    { increment, decrement }, store.dispatch ); actions.increment(); actions.increment(); actions.decrement(); // state = 1 // state = 2 // state = 1
  73. import { bindActionCreators } from 'redux'; const actions = bindActionCreators(

    { increment, decrement }, store.dispatch ); actions.increment(); actions.increment(); actions.decrement(); // state = 1 // state = 2 // state = 1
  74. incrementBtn.addEventListener('click', actions.increment); decrementBtn.addEventListener('click', actions.decrement); store.subscribe(() => { counterElement.innerHTML = store.getState();

    }); incrementBtn.click(); incrementBtn.click(); decrementBtn.click(); console.log(counterElement.innerHTML); // 1
  75. incrementBtn.addEventListener('click', actions.increment); decrementBtn.addEventListener('click', actions.decrement); store.subscribe(() => { counterElement.innerHTML = store.getState();

    }); incrementBtn.click(); incrementBtn.click(); decrementBtn.click(); console.log(counterElement.innerHTML); // 1
  76. incrementBtn.addEventListener('click', actions.increment); decrementBtn.addEventListener('click', actions.decrement); store.subscribe(() => { counterElement.innerHTML = store.getState();

    }); incrementBtn.click(); incrementBtn.click(); decrementBtn.click(); console.log(counterElement.innerHTML); // 1
  77. incrementBtn.addEventListener('click', actions.increment); decrementBtn.addEventListener('click', actions.decrement); store.subscribe(() => { counterElement.innerHTML = store.getState();

    }); incrementBtn.click(); incrementBtn.click(); decrementBtn.click(); console.log(counterElement.innerHTML); // 1
  78. incrementBtn.addEventListener('click', actions.increment); decrementBtn.addEventListener('click', actions.decrement); store.subscribe(() => { counterElement.innerHTML = store.getState();

    }); incrementBtn.click(); incrementBtn.click(); decrementBtn.click(); console.log(counterElement.innerHTML); // 1
  79. Immutable Object State

  80. const initialState = { counter: 0, car: { color: 'red',

    }, };
  81. const initialState = { counter: 0, car: { color: 'red',

    }, };
  82. const initialState = { counter: 0, car: { color: 'red',

    }, };
  83. function reducer(state = initialState, action) { switch (action.type) { case

    'INCREMENT': return { ...state, counter: state.counter + 1 }; case 'DECREMENT': return { ...state, counter: state.counter - 1 }; case 'CHANGE_COLOR': return { ...state, car: { color: action.payload } }; default: return state; } }
  84. function reducer(state = initialState, action) { switch (action.type) { case

    'INCREMENT': return { ...state, counter: state.counter + 1 }; case 'DECREMENT': return { ...state, counter: state.counter - 1 }; case 'CHANGE_COLOR': return { ...state, car: { color: action.payload } }; default: return state; } }
  85. function reducer(state = initialState, action) { switch (action.type) { case

    'INCREMENT': return { ...state, counter: state.counter + 1 }; case 'DECREMENT': return { ...state, counter: state.counter - 1 }; case 'CHANGE_COLOR': return { ...state, car: { color: action.payload } }; default: return state; } }
  86. function reducer(state = initialState, action) { switch (action.type) { case

    'INCREMENT': return { ...state, counter: state.counter + 1 }; case 'DECREMENT': return { ...state, counter: state.counter - 1 }; case 'CHANGE_COLOR': return { ...state, car: { color: action.payload } }; default: return state; } }
  87. function reducer(state = initialState, action) { switch (action.type) { case

    'INCREMENT': return { ...state, counter: state.counter + 1 }; case 'DECREMENT': return { ...state, counter: state.counter - 1 }; case 'CHANGE_COLOR': return { ...state, car: { color: action.payload } }; default: return state; } }
  88. function reducer(state = initialState, action) { switch (action.type) { case

    'INCREMENT': return { ...state, counter: state.counter + 1 }; case 'DECREMENT': return { ...state, counter: state.counter - 1 }; case 'CHANGE_COLOR': return { ...state, car: { color: action.payload } }; default: return state; } }
  89. const increment = () => ({ type: 'INCREMENT', }); const

    decrement = () => ({ type: 'DECREMENT', }); const changeColor = color => ({ type: 'CHANGE_COLOR', payload: color, });
  90. const increment = () => ({ type: 'INCREMENT', }); const

    decrement = () => ({ type: 'DECREMENT', }); const changeColor = color => ({ type: 'CHANGE_COLOR', payload: color, });
  91. const increment = () => ({ type: 'INCREMENT', }); const

    decrement = () => ({ type: 'DECREMENT', }); const changeColor = color => ({ type: 'CHANGE_COLOR', payload: color, });
  92. store.subscribe(() => { console.log('state =', store.getState()); }); store.dispatch(increment()); store.dispatch(changeColor('green')); //

    state = { counter: 1, car: { color: 'red' } } // state = { counter: 1, car: { color: 'green' } }
  93. store.subscribe(() => { console.log('state =', store.getState()); }); store.dispatch(increment()); store.dispatch(changeColor('green')); //

    state = { counter: 1, car: { color: 'red' } } // state = { counter: 1, car: { color: 'green' } }
  94. store.subscribe(() => { console.log('state =', store.getState()); }); store.dispatch(increment()); store.dispatch(changeColor('green')); //

    state = { counter: 1, car: { color: 'red' } } // state = { counter: 1, car: { color: 'green' } }
  95. Middleware

  96. Reducer View State Actions Middleware

  97. Reducer View State Actions Middleware Intercept

  98. const logMiddleware = api => next => action => {

    console.log('dispatch', action); const result = next(action); console.log('state =', api.getState()); return result; };
  99. const logMiddleware = api => next => action => {

    console.log('dispatch', action); const result = next(action); console.log('state =', api.getState()); return result; };
  100. const logMiddleware = api => next => action => {

    console.log('dispatch', action); const result = next(action); console.log('state =', api.getState()); return result; };
  101. const logMiddleware = api => next => action => {

    console.log('dispatch', action); const result = next(action); console.log('state =', api.getState()); return result; };
  102. const logMiddleware = api => next => action => {

    console.log('dispatch', action); const result = next(action); console.log('state =', api.getState()); return result; };
  103. const logMiddleware = api => next => action => {

    console.log('dispatch', action); const result = next(action); console.log('state =', api.getState()); return result; };
  104. const logMiddleware = api => next => action => {

    console.log('dispatch', action); const result = next(action); console.log('state =', api.getState()); return result; };
  105. const logMiddleware = api => next => action => {

    console.log('dispatch', action); const result = next(action); console.log('state =', api.getState()); return result; };
  106. const logMiddleware = api => next => action => {

    console.log('dispatch', action); const result = next(action); console.log('state =', api.getState()); return result; };
  107. import { applyMiddleware } from 'redux'; const store = createStore(

    reducer, applyMiddleware(logMiddleware) ); store.dispatch(increment()); store.dispatch(changeColor('green')); // dispatch { type: 'INCREMENT' } // state = { counter: 1, car: { color: 'red' } } // dispatch { type: 'CHANGE_COLOR', payload: 'green' } // state = { counter: 1, car: { color: 'green' } }
  108. import { applyMiddleware } from 'redux'; const store = createStore(

    reducer, applyMiddleware(logMiddleware) ); store.dispatch(increment()); store.dispatch(changeColor('green')); // dispatch { type: 'INCREMENT' } // state = { counter: 1, car: { color: 'red' } } // dispatch { type: 'CHANGE_COLOR', payload: 'green' } // state = { counter: 1, car: { color: 'green' } }
  109. import { applyMiddleware } from 'redux'; const store = createStore(

    reducer, applyMiddleware(logMiddleware) ); store.dispatch(increment()); store.dispatch(changeColor('green')); // dispatch { type: 'INCREMENT' } // state = { counter: 1, car: { color: 'red' } } // dispatch { type: 'CHANGE_COLOR', payload: 'green' } // state = { counter: 1, car: { color: 'green' } }
  110. import { applyMiddleware } from 'redux'; const store = createStore(

    reducer, applyMiddleware(logMiddleware) ); store.dispatch(increment()); store.dispatch(changeColor('green')); // dispatch { type: 'INCREMENT' } // state = { counter: 1, car: { color: 'red' } } // dispatch { type: 'CHANGE_COLOR', payload: 'green' } // state = { counter: 1, car: { color: 'green' } }
  111. import { applyMiddleware } from 'redux'; const store = createStore(

    reducer, applyMiddleware(logMiddleware) ); store.dispatch(increment()); store.dispatch(changeColor('green')); // dispatch { type: 'INCREMENT' } // state = { counter: 1, car: { color: 'red' } } // dispatch { type: 'CHANGE_COLOR', payload: 'green' } // state = { counter: 1, car: { color: 'green' } }
  112. Side Effects I/O Mutable State

  113. const initialState = { users: [], isFetching: false, };

  114. function reducer(state = initialState, action) { switch (action.type) { case

    'REQUEST_USERS': return { ...state, isFetching: true }; case 'RECEIVE_USERS': return { ...state, isFetching: false, users: action.payload, }; default: return state; } }
  115. function reducer(state = initialState, action) { switch (action.type) { case

    'REQUEST_USERS': return { ...state, isFetching: true }; case 'RECEIVE_USERS': return { ...state, isFetching: false, users: action.payload, }; default: return state; } }
  116. function reducer(state = initialState, action) { switch (action.type) { case

    'REQUEST_USERS': return { ...state, isFetching: true }; case 'RECEIVE_USERS': return { ...state, isFetching: false, users: action.payload, }; default: return state; } }
  117. const requestUsers = () => ({ type: 'REQUEST_USERS', }); const

    receiveUsers = users => ({ type: 'RECEIVE_USERS', payload: users, });
  118. const thunkMiddleware = api => next => action => {

    if (typeof action === 'function') { return action(api.dispatch); } return next(action); }; const store = createStore( reducer, applyMiddleware(thunkMiddleware) );
  119. const thunkMiddleware = api => next => action => {

    if (typeof action === 'function') { return action(api.dispatch); } return next(action); }; const store = createStore( reducer, applyMiddleware(thunkMiddleware) );
  120. const thunkMiddleware = api => next => action => {

    if (typeof action === 'function') { return action(api.dispatch); } return next(action); }; const store = createStore( reducer, applyMiddleware(thunkMiddleware) );
  121. const thunkMiddleware = api => next => action => {

    if (typeof action === 'function') { return action(api.dispatch); } return next(action); }; const store = createStore( reducer, applyMiddleware(thunkMiddleware) );
  122. function fetchUsers() { return dispatch => { dispatch(requestUsers()); return axios.get('/users')

    .then(({ data }) => { dispatch(receiveUsers(data)); }); }; } store.dispatch(fetchUsers());
  123. function fetchUsers() { return dispatch => { dispatch(requestUsers()); return axios.get('/users')

    .then(({ data }) => { dispatch(receiveUsers(data)); }); }; } store.dispatch(fetchUsers()); Action Creator
  124. function fetchUsers() { return dispatch => { dispatch(requestUsers()); return axios.get('/users')

    .then(({ data }) => { dispatch(receiveUsers(data)); }); }; } store.dispatch(fetchUsers()); Action
  125. function fetchUsers() { return dispatch => { dispatch(requestUsers()); return axios.get('/users')

    .then(({ data }) => { dispatch(receiveUsers(data)); }); }; } store.dispatch(fetchUsers());
  126. None
  127. Reducer View State Actions Middleware Redux Saga

  128. Reducer View State Actions Middleware Redux Saga

  129. Reducer View State Actions Middleware Redux Saga

  130. Reducer View State Actions Middleware Redux Saga

  131. Effect Descriptors take('SOME_ACTION_TYPE') call(someAsyncFunction) put(someAction()) fork(someSagaFunction) Just values

  132. Redux Saga Sagas

  133. Redux Saga Sagas

  134. Redux Saga Sagas

  135. Redux Saga Sagas IO APIs Console DB call, apply

  136. Redux Saga Sagas IO APIs Console DB call, apply call,

    apply, fork, spawn, join, cancel
  137. Redux Saga Sagas IO APIs Console DB call, apply call,

    apply, fork, spawn, join, cancel Redux Store put, select, take
  138. Generator Functions

  139. function* myGenerator() { yield 'hello'; const value = yield 'world';

    return value * 2; }
  140. function* myGenerator() { yield 'hello'; const value = yield 'world';

    return value * 2; }
  141. function* myGenerator() { yield 'hello'; const value = yield 'world';

    return value * 2; }
  142. function* myGenerator() { yield 'hello'; const value = yield 'world';

    return value * 2; }
  143. function* myGenerator() { yield 'hello'; const value = yield 'world';

    return value * 2; } const iterator = myGenerator(); iterator.next(); // { value: 'hello', done: false }
  144. function* myGenerator() { yield 'hello'; const value = yield 'world';

    return value * 2; } const iterator = myGenerator(); iterator.next(); // { value: 'hello', done: false }
  145. function* myGenerator() { yield 'hello'; const value = yield 'world';

    return value * 2; } const iterator = myGenerator(); iterator.next(); // { value: 'hello', done: false }
  146. function* myGenerator() { yield 'hello'; const value = yield 'world';

    return value * 2; } const iterator = myGenerator(); iterator.next(); // { value: 'hello', done: false }
  147. function* myGenerator() { yield 'hello'; const value = yield 'world';

    return value * 2; } const iterator = myGenerator(); iterator.next(); // { value: 'hello', done: false }
  148. function* myGenerator() { yield 'hello'; const value = yield 'world';

    return value * 2; } iterator.next(); // { value: 'world', done: false }
  149. function* myGenerator() { yield 'hello'; const value = yield 'world';

    return value * 2; } iterator.next(); // { value: 'world', done: false }
  150. function* myGenerator() { yield 'hello'; const value = yield 'world';

    return value * 2; } iterator.next(21); // { value: 42, done: true }
  151. function* myGenerator() { yield 'hello'; const value = yield 'world';

    return value * 2; } iterator.next(21); // { value: 42, done: true }
  152. function* myGenerator() { yield 'hello'; const value = yield 'world';

    return value * 2; } iterator.next(21); // { value: 42, done: true }
  153. function* myGenerator() { yield 'hello'; const value = yield 'world';

    return value * 2; } iterator.next(); // { value: undefined, done: true }
  154. function* myGenerator() { yield 'hello'; const value = yield 'world';

    return value * 2; } iterator.next(); // { value: undefined, done: true }
  155. function* myGenerator() { yield 'hello'; const value = yield 'world';

    return value * 2; } iterator.next(); // { value: undefined, done: true }
  156. import { call, put, take } from 'redux-saga/effects'; function* fetchUsersSaga()

    { yield take('REQUEST_USERS'); const response = yield call(axios.get, '/users'); yield put(receiveUsers(response.data)); } store.dispatch(requestUsers());
  157. import { call, put, take } from 'redux-saga/effects'; function* fetchUsersSaga()

    { yield take('REQUEST_USERS'); const response = yield call(axios.get, '/users'); yield put(receiveUsers(response.data)); } store.dispatch(requestUsers());
  158. import { call, put, take } from 'redux-saga/effects'; function* fetchUsersSaga()

    { yield take('REQUEST_USERS'); const response = yield call(axios.get, '/users'); yield put(receiveUsers(response.data)); } store.dispatch(requestUsers());
  159. import { call, put, take } from 'redux-saga/effects'; function* fetchUsersSaga()

    { yield take('REQUEST_USERS'); const response = yield call(axios.get, '/users'); yield put(receiveUsers(response.data)); } store.dispatch(requestUsers());
  160. import { call, put, take } from 'redux-saga/effects'; function* fetchUsersSaga()

    { yield take('REQUEST_USERS'); const response = yield call(axios.get, '/users'); yield put(receiveUsers(response.data)); } store.dispatch(requestUsers());
  161. import { call, put, take } from 'redux-saga/effects'; function* fetchUsersSaga()

    { yield take('REQUEST_USERS'); const response = yield call(axios.get, '/users'); yield put(receiveUsers(response.data)); } store.dispatch(requestUsers());
  162. import { call, put, take } from 'redux-saga/effects'; function* fetchUsersSaga()

    { yield take('REQUEST_USERS'); const response = yield call(axios.get, '/users'); yield put(receiveUsers(response.data)); } store.dispatch(requestUsers());
  163. import { call, put, take } from 'redux-saga/effects'; function* fetchUsersSaga()

    { yield take('REQUEST_USERS'); const response = yield call(axios.get, '/users'); yield put(receiveUsers(response.data)); } store.dispatch(requestUsers());
  164. import { call, put, take } from 'redux-saga/effects'; function* fetchUsersSaga()

    { yield take('REQUEST_USERS'); const response = yield call(axios.get, '/users'); yield put(receiveUsers(response.data)); } store.dispatch(requestUsers());
  165. Saga Redux Saga yield take('REQUEST_USERS'); 1. Wait for action

  166. Saga Redux Saga View Middleware yield take('REQUEST_USERS'); 2. Receive action

  167. Saga Redux Saga yield call(axios.get, '/users'); 3. Call API

  168. const response = yield call(axios.get, '/users'); Saga Redux Saga 4.

    Receive response
  169. Saga Redux Saga Reducer Redux Store yield put( receiveUsers(response.data) );

    5. Dispatch (put) action
  170. Set Up

  171. import createSagaMiddleware from 'redux-saga'; const sagaMiddleware = createSagaMiddleware(); const store

    = createStore( reducer, applyMiddleware(sagaMiddleware) ); sagaMiddleware.run(fetchUsersSaga);
  172. import createSagaMiddleware from 'redux-saga'; const sagaMiddleware = createSagaMiddleware(); const store

    = createStore( reducer, applyMiddleware(sagaMiddleware) ); sagaMiddleware.run(fetchUsersSaga);
  173. import createSagaMiddleware from 'redux-saga'; const sagaMiddleware = createSagaMiddleware(); const store

    = createStore( reducer, applyMiddleware(sagaMiddleware) ); sagaMiddleware.run(fetchUsersSaga);
  174. import createSagaMiddleware from 'redux-saga'; const sagaMiddleware = createSagaMiddleware(); const store

    = createStore( reducer, applyMiddleware(sagaMiddleware) ); sagaMiddleware.run(fetchUsersSaga);
  175. import createSagaMiddleware from 'redux-saga'; const sagaMiddleware = createSagaMiddleware(); const store

    = createStore( reducer, applyMiddleware(sagaMiddleware) ); sagaMiddleware.run(fetchUsersSaga);
  176. Why Sagas?

  177. Business Logic Spread Out Component Thunk Service Service Component Reducer

  178. Business Logic Spread Out Component Thunk Service Service Component ×

    Reducer
  179. Business Logic Consolidated Saga Saga Saga Saga Saga Saga ✓

  180. Testing

  181. function* fetchUsersSaga() { yield take('REQUEST_USERS'); const response = yield call(axios.get,

    '/users'); yield put(receiveUsers(response.data)); } const saga = fetchUsersSaga(); it('waits for REQUEST_USERS', () => { const actual = saga.next().value; const expected = take('REQUEST_USERS'); expect(actual).toEqual(expected); });
  182. function* fetchUsersSaga() { yield take('REQUEST_USERS'); const response = yield call(axios.get,

    '/users'); yield put(receiveUsers(response.data)); } const saga = fetchUsersSaga(); it('waits for REQUEST_USERS', () => { const actual = saga.next().value; const expected = take('REQUEST_USERS'); expect(actual).toEqual(expected); });
  183. function* fetchUsersSaga() { yield take('REQUEST_USERS'); const response = yield call(axios.get,

    '/users'); yield put(receiveUsers(response.data)); } const saga = fetchUsersSaga(); it('waits for REQUEST_USERS', () => { const actual = saga.next().value; const expected = take('REQUEST_USERS'); expect(actual).toEqual(expected); });
  184. function* fetchUsersSaga() { yield take('REQUEST_USERS'); const response = yield call(axios.get,

    '/users'); yield put(receiveUsers(response.data)); } const saga = fetchUsersSaga(); it('waits for REQUEST_USERS', () => { const actual = saga.next().value; const expected = take('REQUEST_USERS'); expect(actual).toEqual(expected); });
  185. function* fetchUsersSaga() { yield take('REQUEST_USERS'); const response = yield call(axios.get,

    '/users'); yield put(receiveUsers(response.data)); } const saga = fetchUsersSaga(); it('waits for REQUEST_USERS', () => { const actual = saga.next().value; const expected = take('REQUEST_USERS'); expect(actual).toEqual(expected); });
  186. function* fetchUsersSaga() { yield take('REQUEST_USERS'); const response = yield call(axios.get,

    '/users'); yield put(receiveUsers(response.data)); } // ... it('fetches users', () => { const actual = saga.next().value; const expected = call(axios.get, '/users'); expect(actual).toEqual(expected); });
  187. function* fetchUsersSaga() { yield take('REQUEST_USERS'); const response = yield call(axios.get,

    '/users'); yield put(receiveUsers(response.data)); } // ... it('fetches users', () => { const actual = saga.next().value; const expected = call(axios.get, '/users'); expect(actual).toEqual(expected); });
  188. function* fetchUsersSaga() { yield take('REQUEST_USERS'); const response = yield call(axios.get,

    '/users'); yield put(receiveUsers(response.data)); } // ... it('fetches users', () => { const actual = saga.next().value; const expected = call(axios.get, '/users'); expect(actual).toEqual(expected); });
  189. function* fetchUsersSaga() { yield take('REQUEST_USERS'); const response = yield call(axios.get,

    '/users'); yield put(receiveUsers(response.data)); } // ... it('fetches users', () => { const actual = saga.next().value; const expected = call(axios.get, '/users'); expect(actual).toEqual(expected); });
  190. function* fetchUsersSaga() { yield take('REQUEST_USERS'); const response = yield call(axios.get,

    '/users'); yield put(receiveUsers(response.data)); } it('dispatches the users', () => { const users = [{ name: 'Bob' }, { name: 'Alice' }]; const response = { data: users }; const actual = saga.next(response).value; const expected = put(receiveUsers(users)); expect(actual).toEqual(expected); });
  191. function* fetchUsersSaga() { yield take('REQUEST_USERS'); const response = yield call(axios.get,

    '/users'); yield put(receiveUsers(response.data)); } it('dispatches the users', () => { const users = [{ name: 'Bob' }, { name: 'Alice' }]; const response = { data: users }; const actual = saga.next(response).value; const expected = put(receiveUsers(users)); expect(actual).toEqual(expected); });
  192. function* fetchUsersSaga() { yield take('REQUEST_USERS'); const response = yield call(axios.get,

    '/users'); yield put(receiveUsers(response.data)); } it('dispatches the users', () => { const users = [{ name: 'Bob' }, { name: 'Alice' }]; const response = { data: users }; const actual = saga.next(response).value; const expected = put(receiveUsers(users)); expect(actual).toEqual(expected); });
  193. function* fetchUsersSaga() { yield take('REQUEST_USERS'); const response = yield call(axios.get,

    '/users'); yield put(receiveUsers(response.data)); } it('dispatches the users', () => { const users = [{ name: 'Bob' }, { name: 'Alice' }]; const response = { data: users }; const actual = saga.next(response).value; const expected = put(receiveUsers(users)); expect(actual).toEqual(expected); });
  194. function* fetchUsersSaga() { yield take('REQUEST_USERS'); const response = yield call(axios.get,

    '/users'); yield put(receiveUsers(response.data)); } it('dispatches the users', () => { const users = [{ name: 'Bob' }, { name: 'Alice' }]; const response = { data: users }; const actual = saga.next(response).value; const expected = put(receiveUsers(users)); expect(actual).toEqual(expected); });
  195. function* fetchUsersSaga() { yield take('REQUEST_USERS'); const response = yield call(axios.get,

    '/users'); yield put(receiveUsers(response.data)); } it('dispatches the users', () => { const users = [{ name: 'Bob' }, { name: 'Alice' }]; const response = { data: users }; const actual = saga.next(response).value; const expected = put(receiveUsers(users)); expect(actual).toEqual(expected); });
  196. Error Handling

  197. const initialState = { users: [], isFetching: false, error: null,

    };
  198. const initialState = { users: [], isFetching: false, error: null,

    };
  199. const failUsers = error => ({ type: 'FAIL_USERS', error, });

  200. function reducer(state = initialState, action) { switch (action.type) { //

    ... case 'FAIL_USERS': return { ...state, error: action.error }; // ... } }
  201. function reducer(state = initialState, action) { switch (action.type) { //

    ... case 'FAIL_USERS': return { ...state, error: action.error }; // ... } }
  202. function fetchUsers() { return dispatch => { dispatch(requestUsers()); return axios.get('/users')

    .then(({ data }) => { dispatch(receiveUsers(data)); }) .catch((e) => { dispatch(failUsers(e)); }); }; }
  203. function fetchUsers() { return dispatch => { dispatch(requestUsers()); return axios.get('/users')

    .then(({ data }) => { dispatch(receiveUsers(data)); }) .catch((e) => { dispatch(failUsers(e)); }); }; } Swallowed without catch
  204. function* fetchUsersSaga() { yield take('REQUEST_USERS'); const response = yield call(axios.get,

    '/users'); yield put(receiveUsers(response.data)); }
  205. function* fetchUsersSaga() { yield take('REQUEST_USERS'); const response = yield call(axios.get,

    '/users'); yield put(receiveUsers(response.data)); } No swallowed errors
  206. function* fetchUsersSaga() { try { yield take('REQUEST_USERS'); const response =

    yield call(axios.get, '/users'); yield put(receiveUsers(response.data)); } catch (e) { yield put(failUsers(e)); } }
  207. function* fetchUsersSaga() { try { yield take('REQUEST_USERS'); const response =

    yield call(axios.get, '/users'); yield put(receiveUsers(response.data)); } catch (e) { yield put(failUsers(e)); } }
  208. function* fetchUsersSaga() { try { yield take('REQUEST_USERS'); const response =

    yield call(axios.get, '/users'); yield put(receiveUsers(response.data)); } catch (e) { yield put(failUsers(e)); } }
  209. function* fetchUsersSaga() { try { yield take('REQUEST_USERS'); const response =

    yield call(axios.get, '/users'); yield put(receiveUsers(response.data)); } catch (e) { yield put(failUsers(e)); } }
  210. Multiple Requests

  211. function* fetchUsersSaga() { yield take('REQUEST_USERS'); const responses = yield [

    call(axios.get, '/users/1'), call(axios.get, '/users/2'), call(axios.get, '/users/3'), ]; const users = responses.map(resp => resp.data); yield put(receiveUsers(users)); }
  212. function* fetchUsersSaga() { yield take('REQUEST_USERS'); const responses = yield [

    call(axios.get, '/users/1'), call(axios.get, '/users/2'), call(axios.get, '/users/3'), ]; const users = responses.map(resp => resp.data); yield put(receiveUsers(users)); }
  213. function* fetchUsersSaga() { yield take('REQUEST_USERS'); const responses = yield [

    call(axios.get, '/users/1'), call(axios.get, '/users/2'), call(axios.get, '/users/3'), ]; const users = responses.map(resp => resp.data); yield put(receiveUsers(users)); }
  214. function* fetchUsersSaga() { yield take('REQUEST_USERS'); const responses = yield [

    call(axios.get, '/users/1'), call(axios.get, '/users/2'), call(axios.get, '/users/3'), ]; const users = responses.map(resp => resp.data); yield put(receiveUsers(users)); }
  215. Saga yield [ call(axios.get, '/users/1'), call(axios.get, '/users/2'), call(axios.get, '/users/3'), ];

    // ...
  216. Saga yield [ call(axios.get, '/users/1'), call(axios.get, '/users/2'), call(axios.get, '/users/3'), ];

    // ... 1 2 3
  217. Saga yield [ call(axios.get, '/users/1'), call(axios.get, '/users/2'), call(axios.get, '/users/3'), ];

    // ... 1 2 3
  218. Saga yield [ call(axios.get, '/users/1'), call(axios.get, '/users/2'), call(axios.get, '/users/3'), ];

    // ... 1 3
  219. Saga yield [ call(axios.get, '/users/1'), call(axios.get, '/users/2'), call(axios.get, '/users/3'), ];

    // ... 3
  220. Saga Helpers

  221. function* fetchUsersSaga() { const response = yield call(axios.get, '/users'); yield

    put(receiveUsers(response.data)); } function* mainSaga() { while (true) { yield take('REQUEST_USERS'); yield call(fetchUsersSaga); } }
  222. function* fetchUsersSaga() { const response = yield call(axios.get, '/users'); yield

    put(receiveUsers(response.data)); } function* mainSaga() { while (true) { yield take('REQUEST_USERS'); yield call(fetchUsersSaga); } }
  223. import { takeEvery } from 'redux-saga/effects'; function* fetchUsersSaga() { const

    response = yield call(axios.get, '/users'); yield put(receiveUsers(response.data)); } function* mainSaga() { yield takeEvery('REQUEST_USERS', fetchUsersSaga); }
  224. import { takeEvery } from 'redux-saga/effects'; function* fetchUsersSaga() { const

    response = yield call(axios.get, '/users'); yield put(receiveUsers(response.data)); } function* mainSaga() { yield takeEvery('REQUEST_USERS', fetchUsersSaga); }
  225. import { takeEvery } from 'redux-saga/effects'; function* fetchUsersSaga() { const

    response = yield call(axios.get, '/users'); yield put(receiveUsers(response.data)); } function* mainSaga() { yield takeEvery('REQUEST_USERS', fetchUsersSaga); }
  226. import { takeEvery } from 'redux-saga/effects'; function* fetchUsersSaga() { const

    response = yield call(axios.get, '/users'); yield put(receiveUsers(response.data)); } function* mainSaga() { yield takeEvery('REQUEST_USERS', fetchUsersSaga); }
  227. import { takeEvery } from 'redux-saga/effects'; function* fetchUsersSaga() { const

    response = yield call(axios.get, '/users'); yield put(receiveUsers(response.data)); } function* mainSaga() { yield takeEvery('REQUEST_USERS', fetchUsersSaga); }
  228. mainSaga fetchUsersSaga yield takeEvery( 'REQUEST_USERS', fetchUsersSaga );

  229. View Middleware store.dispatch( requestUsers() ); mainSaga fetchUsersSaga

  230. mainSaga fetchUsersSaga fork

  231. mainSaga fetchUsersSaga yield call( axios.get, '/users' );

  232. mainSaga fetchUsersSaga const response = yield call( axios.get, '/users' );

  233. mainSaga fetchUsersSaga yield put( receiveUsers( response.data ) ); Reducer Redux

    Store
  234. mainSaga fetchUsersSaga yield takeEvery( 'REQUEST_USERS', fetchUsersSaga ); And repeat…

  235. Data Race Issues Use alternative takeLatest

  236. Forking

  237. function* mainSaga() { yield take('REQUEST_USERS'); yield call(fetchUsersSaga); yield take('LOGOUT'); yield

    call(logoutSaga); } store.dispatch(requestUsers()); store.dispatch(logout());
  238. function* mainSaga() { yield take('REQUEST_USERS'); yield call(fetchUsersSaga); yield take('LOGOUT'); yield

    call(logoutSaga); } store.dispatch(requestUsers()); store.dispatch(logout());
  239. function* mainSaga() { yield take('REQUEST_USERS'); yield call(fetchUsersSaga); yield take('LOGOUT'); yield

    call(logoutSaga); } store.dispatch(requestUsers()); store.dispatch(logout());
  240. function* mainSaga() { yield take('REQUEST_USERS'); yield call(fetchUsersSaga); yield take('LOGOUT'); yield

    call(logoutSaga); } store.dispatch(requestUsers()); store.dispatch(logout());
  241. function* mainSaga() { yield take('REQUEST_USERS'); yield call(fetchUsersSaga); yield take('LOGOUT'); yield

    call(logoutSaga); } store.dispatch(requestUsers()); store.dispatch(logout());
  242. function* mainSaga() { yield take('REQUEST_USERS'); yield call(fetchUsersSaga); yield take('LOGOUT'); yield

    call(logoutSaga); } store.dispatch(requestUsers()); store.dispatch(logout()); Missed
  243. function* mainSaga() { yield take('REQUEST_USERS'); yield call(fetchUsersSaga); yield take('LOGOUT'); yield

    call(logoutSaga); } store.dispatch(requestUsers()); store.dispatch(logout()); Missed Never reached
  244. mainSaga yield take( 'REQUEST_USERS' );

  245. View Middleware store.dispatch( requestUsers() ); mainSaga

  246. mainSaga fetchUsersSaga call

  247. mainSaga fetchUsersSaga call yield call( fetchUsersSaga );

  248. mainSaga fetchUsersSaga call View Middleware store.dispatch( logout() ); ×

  249. import { fork } from 'redux-saga/effects'; function* fetchUsersSaga() { yield

    take('REQUEST_USERS'); const response = yield call(axios.get, '/users'); yield put(receiveUsers(response.data)); } function* mainSaga() { yield fork(fetchUsersSaga); yield take('LOGOUT'); yield call(logoutSaga); }
  250. import { fork } from 'redux-saga/effects'; function* fetchUsersSaga() { yield

    take('REQUEST_USERS'); const response = yield call(axios.get, '/users'); yield put(receiveUsers(response.data)); } function* mainSaga() { yield fork(fetchUsersSaga); yield take('LOGOUT'); yield call(logoutSaga); }
  251. import { fork } from 'redux-saga/effects'; function* fetchUsersSaga() { yield

    take('REQUEST_USERS'); const response = yield call(axios.get, '/users'); yield put(receiveUsers(response.data)); } function* mainSaga() { yield fork(fetchUsersSaga); yield take('LOGOUT'); yield call(logoutSaga); } Nonblocking
  252. import { fork } from 'redux-saga/effects'; function* fetchUsersSaga() { yield

    take('REQUEST_USERS'); const response = yield call(axios.get, '/users'); yield put(receiveUsers(response.data)); } function* mainSaga() { yield fork(fetchUsersSaga); yield take('LOGOUT'); yield call(logoutSaga); }
  253. import { fork } from 'redux-saga/effects'; function* fetchUsersSaga() { yield

    take('REQUEST_USERS'); const response = yield call(axios.get, '/users'); yield put(receiveUsers(response.data)); } function* mainSaga() { yield fork(fetchUsersSaga); yield take('LOGOUT'); yield call(logoutSaga); } Won’t miss now
  254. mainSaga

  255. mainSaga fetchUsersSaga fork yield fork( fetchUsersSaga );

  256. View Middleware store.dispatch( requestUsers() ); mainSaga fetchUsersSaga

  257. mainSaga fetchUsersSaga yield call( axios.get, '/users' );

  258. mainSaga fetchUsersSaga yield take( 'LOGOUT' );

  259. mainSaga fetchUsersSaga View Middleware store.dispatch( logout() );

  260. Miscellaneous Patterns

  261. Miscellaneous Patterns • Autosaving background tasks

  262. Miscellaneous Patterns • Autosaving background tasks • Races for timeouts

  263. Miscellaneous Patterns • Autosaving background tasks • Races for timeouts

    • Task cancellation
  264. Miscellaneous Patterns • Autosaving background tasks • Races for timeouts

    • Task cancellation • Throttling and debouncing
  265. Miscellaneous Patterns • Autosaving background tasks • Races for timeouts

    • Task cancellation • Throttling and debouncing • Hook up to other IO sources
  266. Resources •Redux • redux.js.org • egghead.io/courses/getting-started-with-redux •React • github.com/reactjs/react-redux •Redux

    Saga • redux-saga.github.io/redux-saga • Testing • github.com/jfairbank/redux-saga-test-plan
  267. Thanks! Code: github.com/jfairbank/state-side-effects-and-redux Slides: bit.ly/jazzcon-redux Jeremy Fairbank @elpapapollo / jfairbank