Connect.Tech 2017: Get Started with Redux

Connect.Tech 2017: Get Started with Redux

94bd558238b69c45d3d3e15797ae94f7?s=128

Jeremy Fairbank

September 21, 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, 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 = { status: 'READY', user: null, };

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

    'REQUEST_USER': return { ...state, status: 'FETCHING' }; case 'RECEIVE_USER': return { ...state, status: 'SUCCESS', user: action.payload, }; default: return state; } }
  148. function reducer(state = initialState, action) { switch (action.type) { case

    'REQUEST_USER': return { ...state, status: 'FETCHING' }; case 'RECEIVE_USER': return { ...state, status: 'SUCCESS', user: action.payload, }; default: return state; } }
  149. function reducer(state = initialState, action) { switch (action.type) { case

    'REQUEST_USER': return { ...state, status: 'FETCHING' }; case 'RECEIVE_USER': return { ...state, status: 'SUCCESS', user: action.payload, }; default: return state; } }
  150. const requestUser = id => ({ type: 'REQUEST_USER', payload: id,

    }); const receiveUser = user => ({ type: 'RECEIVE_USER', payload: user, });
  151. const store = createStore(reducer); function fetchUser(id) { store.dispatch(requestUser(id)); axios.get(`/users/${id}`) .then(({

    data: user }) => { store.dispatch(receiveUser(user)); }); } fetchUser(1);
  152. const store = createStore(reducer); function fetchUser(id) { store.dispatch(requestUser(id)); axios.get(`/users/${id}`) .then(({

    data: user }) => { store.dispatch(receiveUser(user)); }); } fetchUser(1);
  153. const store = createStore(reducer); function fetchUser(id) { store.dispatch(requestUser(id)); axios.get(`/users/${id}`) .then(({

    data: user }) => { store.dispatch(receiveUser(user)); }); } fetchUser(1);
  154. const thunkMiddleware = api => next => action => {

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

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

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

    if (typeof action === 'function') { return action(api.dispatch); } return next(action); }; const store = createStore( reducer, applyMiddleware(thunkMiddleware) );
  158. function fetchUser(id) { return dispatch => { dispatch(requestUser(id)); return axios.get(`/users/${id}`)

    .then(({ data: user }) => { dispatch(receiveUser(user)); }); }; } store.dispatch(fetchUser(1));
  159. function fetchUser(id) { return dispatch => { dispatch(requestUser(id)); return axios.get(`/users/${id}`)

    .then(({ data: user }) => { dispatch(receiveUser(user)); }); }; } store.dispatch(fetchUser(1)); Action Creator
  160. function fetchUser(id) { return dispatch => { dispatch(requestUser(id)); return axios.get(`/users/${id}`)

    .then(({ data: user }) => { dispatch(receiveUser(user)); }); }; } store.dispatch(fetchUser(1)); Action
  161. function fetchUser(id) { return dispatch => { dispatch(requestUser(id)); return axios.get(`/users/${id}`)

    .then(({ data: user }) => { dispatch(receiveUser(user)); }); }; } store.dispatch(fetchUser(1));
  162. Alternative Async Middleware

  163. redux-saga.js.org

  164. • Uses ES6 generator functions redux-saga.js.org

  165. • Uses ES6 generator functions • Write asynchronous code in

    synchronous manner redux-saga.js.org
  166. • Uses ES6 generator functions • Write asynchronous code in

    synchronous manner • Great for coordinating multiple API calls redux-saga.js.org
  167. • Uses ES6 generator functions • Write asynchronous code in

    synchronous manner • Great for coordinating multiple API calls • Great for forking background tasks redux-saga.js.org
  168. redux-observable.js.org

  169. redux-observable.js.org • Uses RxJS observables

  170. redux-observable.js.org • Uses RxJS observables • Write asynchronous code with

    declarative observable chains
  171. redux-observable.js.org • Uses RxJS observables • Write asynchronous code with

    declarative observable chains • Great for composing asynchronous operations
  172. redux-observable.js.org • Uses RxJS observables • Write asynchronous code with

    declarative observable chains • Great for composing asynchronous operations • Very concise code
  173. And plenty more options…

  174. • redux-logic And plenty more options…

  175. • redux-logic • redux-ship And plenty more options…

  176. • redux-logic • redux-ship • redux-promise And plenty more options…

  177. • redux-logic • redux-ship • redux-promise • redux-api-middleware And plenty

    more options…
  178. × ✓ Testing

  179. 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!
  180. 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.
  181. However, you should test async action creators. function fetchUser(id) {

    return dispatch => { dispatch(requestUser(id)); return axios.get(`/users/${id}`) .then(({ data: user }) => { dispatch(receiveUser(user)); }); }; }
  182. import td from 'testdouble'; const axios = td.replace('axios'); it('fetches a

    user', () => { // Arrange const dispatch = td.function(); td.when(axios.get('/users/1')).thenResolve({ data: 'fake user' }); // Act return fetchUser(1)(dispatch).then(() => { const subject = td.matchers.captor(); td.verify(dispatch(subject.capture())); // Assert expect(subject.values[0]).toEqual(requestUser(1)); expect(subject.values[1]).toEqual(receiveUser('fake user’)); }); }); Unit test with test doubles
  183. import td from 'testdouble'; const axios = td.replace('axios'); it('fetches a

    user', () => { // Arrange const dispatch = td.function(); td.when(axios.get('/users/1')).thenResolve({ data: 'fake user' }); // Act return fetchUser(1)(dispatch).then(() => { const subject = td.matchers.captor(); td.verify(dispatch(subject.capture())); // Assert expect(subject.values[0]).toEqual(requestUser(1)); expect(subject.values[1]).toEqual(receiveUser('fake user’)); }); });
  184. import td from 'testdouble'; const axios = td.replace('axios'); it('fetches a

    user', () => { // Arrange const dispatch = td.function(); td.when(axios.get('/users/1')).thenResolve({ data: 'fake user' }); // Act return fetchUser(1)(dispatch).then(() => { const subject = td.matchers.captor(); td.verify(dispatch(subject.capture())); // Assert expect(subject.values[0]).toEqual(requestUser(1)); expect(subject.values[1]).toEqual(receiveUser('fake user’)); }); });
  185. import td from 'testdouble'; const axios = td.replace('axios'); it('fetches a

    user', () => { // Arrange const dispatch = td.function(); td.when(axios.get('/users/1')).thenResolve({ data: 'fake user' }); // Act return fetchUser(1)(dispatch).then(() => { const subject = td.matchers.captor(); td.verify(dispatch(subject.capture())); // Assert expect(subject.values[0]).toEqual(requestUser(1)); expect(subject.values[1]).toEqual(receiveUser('fake user’)); }); });
  186. import td from 'testdouble'; const axios = td.replace('axios'); it('fetches a

    user', () => { // Arrange const dispatch = td.function(); td.when(axios.get('/users/1')).thenResolve({ data: 'fake user' }); // Act return fetchUser(1)(dispatch).then(() => { const subject = td.matchers.captor(); td.verify(dispatch(subject.capture())); // Assert expect(subject.values[0]).toEqual(requestUser(1)); expect(subject.values[1]).toEqual(receiveUser('fake user’)); }); });
  187. import td from 'testdouble'; const axios = td.replace('axios'); it('fetches a

    user', () => { // Arrange const dispatch = td.function(); td.when(axios.get('/users/1')).thenResolve({ data: 'fake user' }); // Act return fetchUser(1)(dispatch).then(() => { const subject = td.matchers.captor(); td.verify(dispatch(subject.capture())); // Assert expect(subject.values[0]).toEqual(requestUser(1)); expect(subject.values[1]).toEqual(receiveUser('fake user’)); }); });
  188. import td from 'testdouble'; const axios = td.replace('axios'); it('fetches a

    user', () => { // Arrange const dispatch = td.function(); td.when(axios.get('/users/1')).thenResolve({ data: 'fake user' }); // Act return fetchUser(1)(dispatch).then(() => { const subject = td.matchers.captor(); td.verify(dispatch(subject.capture())); // Assert expect(subject.values[0]).toEqual(requestUser(1)); expect(subject.values[1]).toEqual(receiveUser('fake user’)); }); });
  189. import td from 'testdouble'; const axios = td.replace('axios'); it('fetches a

    user', () => { // Arrange const dispatch = td.function(); td.when(axios.get('/users/1')).thenResolve({ data: 'fake user' }); // Act return fetchUser(1)(dispatch).then(() => { const subject = td.matchers.captor(); td.verify(dispatch(subject.capture())); // Assert expect(subject.values[0]).toEqual(requestUser(1)); expect(subject.values[1]).toEqual(receiveUser('fake user’)); }); });
  190. Use integration tests to ensure all pieces work together. Allow

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

    = createStore(reducer, applyMiddleware(thunkMiddleware)); td.replace(axios, 'get'); td.when(axios.get('/users/1')).thenResolve({ data: 'fake user' }); // Act const promise = store.dispatch(fetchUser(1)); const subject = store.getState; // Assert expect(subject()).toEqual({ status: 'FETCHING', user: null }); return promise.then(() => { expect(subject()).toEqual({ status: 'SUCCESS', user: 'fake user' }); }); });
  192. it('fetches a user', () => { // Arrange const store

    = createStore(reducer, applyMiddleware(thunkMiddleware)); td.replace(axios, 'get'); td.when(axios.get('/users/1')).thenResolve({ data: 'fake user' }); // Act const promise = store.dispatch(fetchUser(1)); const subject = store.getState; // Assert expect(subject()).toEqual({ status: 'FETCHING', user: null }); return promise.then(() => { expect(subject()).toEqual({ status: 'SUCCESS', user: 'fake user' }); }); });
  193. it('fetches a user', () => { // Arrange const store

    = createStore(reducer, applyMiddleware(thunkMiddleware)); td.replace(axios, 'get'); td.when(axios.get('/users/1')).thenResolve({ data: 'fake user' }); // Act const promise = store.dispatch(fetchUser(1)); const subject = store.getState; // Assert expect(subject()).toEqual({ status: 'FETCHING', user: null }); return promise.then(() => { expect(subject()).toEqual({ status: 'SUCCESS', user: 'fake user' }); }); });
  194. it('fetches a user', () => { // Arrange const store

    = createStore(reducer, applyMiddleware(thunkMiddleware)); td.replace(axios, 'get'); td.when(axios.get('/users/1')).thenResolve({ data: 'fake user' }); // Act const promise = store.dispatch(fetchUser(1)); const subject = store.getState; // Assert expect(subject()).toEqual({ status: 'FETCHING', user: null }); return promise.then(() => { expect(subject()).toEqual({ status: 'SUCCESS', user: 'fake user' }); }); });
  195. it('fetches a user', () => { // Arrange const store

    = createStore(reducer, applyMiddleware(thunkMiddleware)); td.replace(axios, 'get'); td.when(axios.get('/users/1')).thenResolve({ data: 'fake user' }); // Act const promise = store.dispatch(fetchUser(1)); const subject = store.getState; // Assert expect(subject()).toEqual({ status: 'FETCHING', user: null }); return promise.then(() => { expect(subject()).toEqual({ status: 'SUCCESS', user: 'fake user' }); }); });
  196. Resources • Redux • redux.js.org • egghead.io/courses/getting-started-with- redux • React

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