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

Scenic City Summit 2017: Get Started with Redux

Scenic City Summit 2017: Get Started with Redux

94bd558238b69c45d3d3e15797ae94f7?s=128

Jeremy Fairbank

July 28, 2017
Tweet

Transcript

  1. Jeremy Fairbank @elpapapollo / jfairbank Get Started with Redux

  2. Software is broken. We are here to fix it. Say

    hi@testdouble.com
  3. The Wild West of State

  4. <div data-id="42" data-name="Tucker"> ... </div> var $el = $('[data-id="42"]'); var

    currentName = $el.data('name'); $el.data('name', currentName.toUpperCase()); Data in the DOM
  5. Controller View Model Model Model Model Model View View View

    View MVC
  6. View Model Model Model Model Model View Model View Model

    View Model View Model Model Two-way Data Binding
  7. The Wild West of State State is everywhere and anyone

    can change it!
  8. Predictable State Container

  9. All application state in one place State Container

  10. Predictable Old State REDUCER New State

  11. Predictable Old State REDUCER New State • State changes in

    one place
  12. Predictable Old State REDUCER New State • State changes in

    one place • State changes in well-defined ways
  13. Predictable Old State REDUCER New State • State changes in

    one place • State changes in well-defined ways • Changes are serialized
  14. Reducer View State Actions

  15. Reducer View State Actions

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

  17. Reducer View State Actions Dispatch

  18. Reducer View State Actions

  19. <button id="decrement">-</button> <div id="counter">0</div> <button id="increment">+</button>

  20. incrementBtn.addEventListener('click', () => { counter.innerText = Number(counter.innerText) + 1; });

    decrementBtn.addEventListener('click', () => { counter.innerText = Number(counter.innerText) - 1; });
  21. let state = 0; function render() { counter.innerText = state;

    } incrementBtn.addEventListener('click', () => { state += 1; render(); }); decrementBtn.addEventListener('click', () => { state -= 1; render(); });
  22. let state = 0; function render() { counter.innerText = state;

    } incrementBtn.addEventListener('click', () => { state += 1; render(); }); decrementBtn.addEventListener('click', () => { state -= 1; render(); });
  23. let state = 0; function render() { counter.innerText = state;

    } incrementBtn.addEventListener('click', () => { state += 1; render(); }); decrementBtn.addEventListener('click', () => { state -= 1; render(); });
  24. let state = 0; function render() { counter.innerText = state;

    } incrementBtn.addEventListener('click', () => { state += 1; render(); }); decrementBtn.addEventListener('click', () => { state -= 1; render(); });
  25. state += 1; state -= 1; Anyone can access and

    mutate state
  26. state += 1 state -= 1 { type: 'INCREMENT' }

    { type: 'DECREMENT' } Tokens/descriptors that describe a type of change. Actions
  27. function reducer(state = 0, action) { switch (action.type) { case

    'INCREMENT': return state + 1; case 'DECREMENT': return state - 1; default: return state; } } Returns new state from current state and action. Reducer
  28. function reducer(state = 0, action) { switch (action.type) { case

    'INCREMENT': return state + 1; case 'DECREMENT': return state - 1; default: return state; } } Returns new state from current state and action. Reducer
  29. function reducer(state = 0, action) { switch (action.type) { case

    'INCREMENT': return state + 1; case 'DECREMENT': return state - 1; default: return state; } } Returns new state from current state and action. Reducer
  30. function reducer(state = 0, action) { switch (action.type) { case

    'INCREMENT': return state + 1; case 'DECREMENT': return state - 1; default: return state; } } Returns new state from current state and action. Reducer
  31. function reducer(state = 0, action) { switch (action.type) { case

    'INCREMENT': return state + 1; case 'DECREMENT': return state - 1; default: return state; } } Returns new state from current state and action. Reducer
  32. function reducer(state = 0, action) { switch (action.type) { case

    'INCREMENT': return state + 1; case 'DECREMENT': return state - 1; default: return state; } } Returns new state from current state and action. Reducer
  33. function reducer(state = 0, action) { switch (action.type) { case

    'INCREMENT': return state + 1; case 'DECREMENT': return state - 1; default: return state; } } Returns new state from current state and action. Reducer
  34. Updating state is just a function call now let state

    = 0; state = reducer(state, { type: 'INCREMENT' }); // 1 state = reducer(state, { type: 'INCREMENT' }); // 2 state = reducer(state, { type: 'DECREMENT' }); // 1 state = reducer(state, { type: 'ADD_2' }); // 1 console.log(state); // 1
  35. Updating state is just a function call now let state

    = 0; state = reducer(state, { type: 'INCREMENT' }); // 1 state = reducer(state, { type: 'INCREMENT' }); // 2 state = reducer(state, { type: 'DECREMENT' }); // 1 state = reducer(state, { type: 'ADD_2' }); // 1 console.log(state); // 1 Unhandled types are ignored
  36. function dispatch(action) { state = reducer(state, action); render(); } incrementBtn.addEventListener('click',

    () => { dispatch({ type: 'INCREMENT' }); }); decrementBtn.addEventListener('click', () => { dispatch({ type: 'DECREMENT' }); }); Decouple event from state change.
  37. function dispatch(action) { state = reducer(state, action); render(); } incrementBtn.addEventListener('click',

    () => { dispatch({ type: 'INCREMENT' }); }); decrementBtn.addEventListener('click', () => { dispatch({ type: 'DECREMENT' }); }); Decouple event from state change.
  38. function dispatch(action) { state = reducer(state, action); render(); } incrementBtn.addEventListener('click',

    () => { dispatch({ type: 'INCREMENT' }); }); decrementBtn.addEventListener('click', () => { dispatch({ type: 'DECREMENT' }); }); Decouple event from state change.
  39. function dispatch(action) { state = reducer(state, action); render(); } incrementBtn.addEventListener('click',

    () => { dispatch({ type: 'INCREMENT' }); }); decrementBtn.addEventListener('click', () => { dispatch({ type: 'DECREMENT' }); }); Decouple event from state change.
  40. function dispatch(action) { state = reducer(state, action); render(); } incrementBtn.addEventListener('click',

    () => { dispatch({ type: 'INCREMENT' }); }); decrementBtn.addEventListener('click', () => { dispatch({ type: 'DECREMENT' }); }); Decouple event from state change.
  41. STORE

  42. import { createStore } from 'redux'; const store = createStore(reducer,

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

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

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

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

    0); store.getState(); // 0
  47. incrementBtn.addEventListener('click', () => { store.dispatch({ type: 'INCREMENT' }); }); decrementBtn.addEventListener('click',

    () => { store.dispatch({ type: 'DECREMENT' }); }); store.subscribe(() => { counter.innerText = store.getState(); });
  48. incrementBtn.addEventListener('click', () => { store.dispatch({ type: 'INCREMENT' }); }); decrementBtn.addEventListener('click',

    () => { store.dispatch({ type: 'DECREMENT' }); }); store.subscribe(() => { counter.innerText = store.getState(); });
  49. incrementBtn.addEventListener('click', () => { store.dispatch({ type: 'INCREMENT' }); }); decrementBtn.addEventListener('click',

    () => { store.dispatch({ type: 'DECREMENT' }); }); store.subscribe(() => { counter.innerText = store.getState(); });
  50. Store Reducer State View 0

  51. Store Reducer State View 0

  52. Store Reducer State View 0 getState

  53. Store Reducer View State 0

  54. Store Reducer View State 0 INCREMENT dispatch

  55. Store Reducer View State 0 INCREMENT dispatch

  56. Store Reducer View State 0

  57. Store Reducer View State 1

  58. Store Reducer View State 1

  59. Store Reducer View State 1 1 subscribe getState

  60. incrementBtn.addEventListener('click', () => { store.dispatch({ type: 'INCREMENT' }); }); decrementBtn.addEventListener('click',

    () => { store.dispatch({ type: 'DECREMENT' }); }); Problems: • Creating actions are cumbersome • Requires direct access to store
  61. const increment = () => ({ type: 'INCREMENT', }); const

    decrement = () => ({ type: 'DECREMENT', }); Reusable functions that create actions Action Creators
  62. const increment = () => ({ type: 'INCREMENT', }); const

    decrement = () => ({ type: 'DECREMENT', }); Reusable functions that create actions Action Creators
  63. const increment = () => ({ type: 'INCREMENT', }); const

    decrement = () => ({ type: 'DECREMENT', }); Reusable functions that create actions Action Creators
  64. incrementBtn.addEventListener('click', () => { store.dispatch(increment()); }); decrementBtn.addEventListener('click', () => {

    store.dispatch(decrement()); }); Problems: • Creating actions are cumbersome • Requires direct access to store
  65. const actions = { increment: () => store.dispatch(increment()), decrement: ()

    => store.dispatch(decrement()), }; Automatically dispatch when invoked Bound Action Creators
  66. const actions = { increment: () => store.dispatch(increment()), decrement: ()

    => store.dispatch(decrement()), }; Automatically dispatch when invoked Bound Action Creators Manually created
  67. const actions = { increment: () => store.dispatch(increment()), decrement: ()

    => store.dispatch(decrement()), }; Automatically dispatch when invoked Bound Action Creators
  68. import { bindActionCreators } from 'redux'; const actions = bindActionCreators(

    { increment, decrement }, store.dispatch ); Automatically dispatch when invoked Bound Action Creators
  69. import { bindActionCreators } from 'redux'; const actions = bindActionCreators(

    { increment, decrement }, store.dispatch ); Automatically dispatch when invoked Bound Action Creators
  70. import { bindActionCreators } from 'redux'; const actions = bindActionCreators(

    { increment, decrement }, store.dispatch ); Automatically dispatch when invoked Bound Action Creators Automatically created
  71. Problems: • Creating actions are cumbersome • Requires direct access

    to store incrementBtn.addEventListener('click', actions.increment); decrementBtn.addEventListener('click', actions.decrement);
  72. React +

  73. github.com/reactjs/react-redux npm install --save react-redux Official React bindings for Redux

    React Redux Library
  74. Reducer State Actions React Redux

  75. React Redux React Application

  76. React Redux React Application Provider

  77. Component React Redux React Application Provider connect State Action Creators

  78. Component Child React Redux React Application Provider connect State Child

    Action Creators
  79. const MyApp = () => ( <div> <button>-</button> <div>0</div> <button>+</button>

    </div> );
  80. import React from 'react'; import { render } from 'react-dom';

    import { Provider } from 'react-redux'; render(( <Provider store={store}> <MyAppContainer /> </Provider> ), document.getElementById('main'));
  81. import React from 'react'; import { render } from 'react-dom';

    import { Provider } from 'react-redux'; render(( <Provider store={store}> <MyAppContainer /> </Provider> ), document.getElementById('main'));
  82. import React from 'react'; import { render } from 'react-dom';

    import { Provider } from 'react-redux'; render(( <Provider store={store}> <MyAppContainer /> </Provider> ), document.getElementById('main'));
  83. import { connect } from 'react-redux'; const mapStateToProps = counter

    => ({ counter }); function mapDispatchToProps(dispatch) { return bindActionCreators({ onIncrement: increment, onDecrement: decrement, }, dispatch); } const MyAppContainer = connect( mapStateToProps, mapDispatchToProps )(MyApp);
  84. import { connect } from 'react-redux'; const mapStateToProps = counter

    => ({ counter }); function mapDispatchToProps(dispatch) { return bindActionCreators({ onIncrement: increment, onDecrement: decrement, }, dispatch); } const MyAppContainer = connect( mapStateToProps, mapDispatchToProps )(MyApp);
  85. import { connect } from 'react-redux'; const mapStateToProps = counter

    => ({ counter }); function mapDispatchToProps(dispatch) { return bindActionCreators({ onIncrement: increment, onDecrement: decrement, }, dispatch); } const MyAppContainer = connect( mapStateToProps, mapDispatchToProps )(MyApp);
  86. import { connect } from 'react-redux'; const mapStateToProps = counter

    => ({ counter }); function mapDispatchToProps(dispatch) { return bindActionCreators({ onIncrement: increment, onDecrement: decrement, }, dispatch); } const MyAppContainer = connect( mapStateToProps, mapDispatchToProps )(MyApp);
  87. import { connect } from 'react-redux'; const mapStateToProps = counter

    => ({ counter }); function mapDispatchToProps(dispatch) { return bindActionCreators({ onIncrement: increment, onDecrement: decrement, }, dispatch); } const MyAppContainer = connect( mapStateToProps, mapDispatchToProps )(MyApp);
  88. const MyApp = (props) => ( <div> <button onClick={props.onDecrement}> -

    </button> <div>{props.counter}</div> <button onClick={props.onIncrement}> + </button> </div> );
  89. const MyApp = (props) => ( <div> <button onClick={props.onDecrement}> -

    </button> <div>{props.counter}</div> <button onClick={props.onIncrement}> + </button> </div> ); Store state mapStateToProps
  90. const MyApp = (props) => ( <div> <button onClick={props.onDecrement}> -

    </button> <div>{props.counter}</div> <button onClick={props.onIncrement}> + </button> </div> ); Bound action creators mapDispatchToProps
  91. Immutable Object State

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

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

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

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

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

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

    decrement = () => ({ type: 'DECREMENT', }); const changeColor = color => ({ type: 'CHANGE_COLOR', payload: color, });
  98. 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; } }
  99. 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; } }
  100. 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; } }
  101. 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; } }
  102. 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; } }
  103. 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; } }
  104. 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' } }
  105. 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' } }
  106. 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' } }
  107. function reducer(state = initialState, action) { }

  108. Reducer Composition Create modular reducers for better organization and readability

  109. Root Reducer Counter Reducer Car Reducer

  110. function counterReducer(state = 0, action) { switch (action.type) { case

    'INCREMENT': return state + 1; case 'DECREMENT': return state - 1; default: return state; } }
  111. const initialState = { color: 'red' }; function carReducer(state =

    initialState, action) { switch (action.type) { case 'CHANGE_COLOR': return { ...state, color: action.payload }; default: return state; } }
  112. const initialState = { color: 'red' }; function carReducer(state =

    initialState, action) { switch (action.type) { case 'CHANGE_COLOR': return { ...state, color: action.payload }; default: return state; } }
  113. const initialState = { color: 'red' }; function carReducer(state =

    initialState, action) { switch (action.type) { case 'CHANGE_COLOR': return { ...state, color: action.payload }; default: return state; } }
  114. function reducer(state = {}, action) { return { counter: counterReducer(state.counter,

    action), car: carReducer(state.car, action), }; }
  115. function reducer(state = {}, action) { return { counter: counterReducer(state.counter,

    action), car: carReducer(state.car, action), }; }
  116. function reducer(state = {}, action) { return { counter: counterReducer(state.counter,

    action), car: carReducer(state.car, action), }; }
  117. function reducer(state = {}, action) { return { counter: counterReducer(state.counter,

    action), car: carReducer(state.car, action), }; }
  118. import { combineReducers } from 'redux'; const reducer = combineReducers({

    counter: counterReducer, car: carReducer, });
  119. import { combineReducers } from 'redux'; const reducer = combineReducers({

    counter: counterReducer, car: carReducer, });
  120. import { combineReducers } from 'redux'; const reducer = combineReducers({

    counter: counterReducer, car: carReducer, });
  121. Root Reducer Counter Reducer Car Reducer Engine Reducer Tire Reducer

    … … …
  122. Root Reducer Counter Reducer Car Reducer Engine Reducer Tire Reducer

    … … …
  123. Root Reducer Counter Reducer Car Reducer Engine Reducer Tire Reducer

    … … …
  124. Interact with APIs?

  125. Middleware Enhance Redux applications

  126. • Logging Middleware Enhance Redux applications

  127. • Logging • Debugging Middleware Enhance Redux applications

  128. • Logging • Debugging • API interaction Middleware Enhance Redux

    applications
  129. • Logging • Debugging • API interaction • Custom actions

    Middleware Enhance Redux applications
  130. Reducer View State Actions Middleware

  131. Reducer View State Actions Middleware Intercept

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

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

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

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

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

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

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

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

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

    console.log('dispatch', action); const result = next(action); console.log('state =', api.getState()); return result; };
  141. 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' } }
  142. 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' } }
  143. 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' } }
  144. 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' } }
  145. 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' } }
  146. const initialState = { users: [], isFetching: false, };

  147. 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; } }
  148. 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; } }
  149. 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; } }
  150. const requestUsers = () => ({ type: 'REQUEST_USERS', }); const

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

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

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

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

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

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

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

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

    .then(({ data }) => { dispatch(receiveUsers(data)); }); }; } store.dispatch(fetchUsers());
  159. Alternatives for async code.

  160. • Appropriate for more complex business logic and async flows.

    Alternatives for async code.
  161. • Appropriate for more complex business logic and async flows.

    • Middleware-based like thunk middleware. Alternatives for async code.
  162. • Appropriate for more complex business logic and async flows.

    • Middleware-based like thunk middleware. • redux-saga Alternatives for async code.
  163. • Appropriate for more complex business logic and async flows.

    • Middleware-based like thunk middleware. • redux-saga • redux-observable Alternatives for async code.
  164. • Appropriate for more complex business logic and async flows.

    • Middleware-based like thunk middleware. • redux-saga • redux-observable • redux-logic Alternatives for async code.
  165. × ✓ Testing

  166. const state = { counter: 0, car: { color: 'red'

    }, }; it('returns initial state', () => { expect(reducer(undefined, {})).toEqual(state); }); it('increments the number', () => { const subject = reducer(state, increment()).counter; expect(subject).toBe(1); }); it('changes the car color', () => { const subject = reducer(state, changeColor('green')).car.color; expect(subject).toBe('green'); }); Easy reducer unit tests!
  167. it('creates an INCREMENT action', () => { expect(increment()).toEqual({ type: 'INCREMENT'

    }); }); it('creates a CHANGE_COLOR action', () => { expect(changeColor('blue')).toEqual({ type: 'CHANGE_COLOR', payload: 'blue', }); }); You can test action creators, but not really necessary.
  168. function fetchUsers() { return dispatch => { dispatch(requestUsers()); return axios.get('/users')

    .then(({ data }) => { dispatch(receiveUsers(data)); }); }; } However, you should test async action creators.
  169. import td from 'testdouble'; const axios = td.replace('axios'); it('fetches users',

    () => { // Arrange const dispatch = td.function(); td.when(axios.get('/users')).thenResolve({ data: 'success' }); // Act return fetchUsers()(dispatch).then(() => { const subject = td.matchers.captor(); td.verify(dispatch(subject.capture())); // Assert expect(subject.values[0]).toEqual(requestUsers()); expect(subject.values[1]).toEqual(receiveUsers('success')); }); }); Unit test with test doubles
  170. Use integration tests to ensure all pieces work together. Allow

    store, reducer, and actions to all interact .
  171. it('fetches users', () => { // Arrange const store =

    createStore(reducer, applyMiddleware(thunkMiddleware)); td.replace(axios, 'get'); td.when(axios.get('/users')).thenResolve({ data: 'success' }); // Act const promise = store.dispatch(fetchUsers()); const subject = store.getState; // Assert expect(subject()).toEqual({ isFetching: true, users: [] }); return promise.then(() => { expect(subject()).toEqual({ isFetching: false, users: 'success' }); }); });
  172. it('fetches users', () => { // Arrange const store =

    createStore(reducer, applyMiddleware(thunkMiddleware)); td.replace(axios, 'get'); td.when(axios.get('/users')).thenResolve({ data: 'success' }); // Act const promise = store.dispatch(fetchUsers()); const subject = store.getState; // Assert expect(subject()).toEqual({ isFetching: true, users: [] }); return promise.then(() => { expect(subject()).toEqual({ isFetching: false, users: 'success' }); }); });
  173. it('fetches users', () => { // Arrange const store =

    createStore(reducer, applyMiddleware(thunkMiddleware)); td.replace(axios, 'get'); td.when(axios.get('/users')).thenResolve({ data: 'success' }); // Act const promise = store.dispatch(fetchUsers()); const subject = store.getState; // Assert expect(subject()).toEqual({ isFetching: true, users: [] }); return promise.then(() => { expect(subject()).toEqual({ isFetching: false, users: 'success' }); }); });
  174. it('fetches users', () => { // Arrange const store =

    createStore(reducer, applyMiddleware(thunkMiddleware)); td.replace(axios, 'get'); td.when(axios.get('/users')).thenResolve({ data: 'success' }); // Act const promise = store.dispatch(fetchUsers()); const subject = store.getState; // Assert expect(subject()).toEqual({ isFetching: true, users: [] }); return promise.then(() => { expect(subject()).toEqual({ isFetching: false, users: 'success' }); }); });
  175. Resources • Redux • redux.js.org • egghead.io/courses/getting-started-with- redux • React

    • github.com/reactjs/react-redux
  176. Thanks! Slides: bit.ly/scs-redux Jeremy Fairbank @elpapapollo / jfairbank