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. 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
  2. 6.

    View Model Model Model Model Model View Model View Model

    View Model View Model Model Two-way Data Binding
  3. 12.

    Predictable Old State REDUCER New State • State changes in

    one place • State changes in well-defined ways
  4. 13.

    Predictable Old State REDUCER New State • State changes in

    one place • State changes in well-defined ways • Changes are serialized
  5. 20.

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

    decrementBtn.addEventListener('click', () => { counter.innerText = Number(counter.innerText) - 1; });
  6. 21.

    let state = 0; function render() { counter.innerText = state;

    } incrementBtn.addEventListener('click', () => { state += 1; render(); }); decrementBtn.addEventListener('click', () => { state -= 1; render(); });
  7. 22.

    let state = 0; function render() { counter.innerText = state;

    } incrementBtn.addEventListener('click', () => { state += 1; render(); }); decrementBtn.addEventListener('click', () => { state -= 1; render(); });
  8. 23.

    let state = 0; function render() { counter.innerText = state;

    } incrementBtn.addEventListener('click', () => { state += 1; render(); }); decrementBtn.addEventListener('click', () => { state -= 1; render(); });
  9. 24.

    let state = 0; function render() { counter.innerText = state;

    } incrementBtn.addEventListener('click', () => { state += 1; render(); }); decrementBtn.addEventListener('click', () => { state -= 1; render(); });
  10. 26.

    state += 1 state -= 1 { type: 'INCREMENT' }

    { type: 'DECREMENT' } Tokens/descriptors that describe a type of change. Actions
  11. 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
  12. 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
  13. 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
  14. 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
  15. 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
  16. 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
  17. 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
  18. 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
  19. 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
  20. 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.
  21. 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.
  22. 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.
  23. 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.
  24. 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.
  25. 41.
  26. 47.

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

    () => { store.dispatch({ type: 'DECREMENT' }); }); store.subscribe(() => { counter.innerText = store.getState(); });
  27. 48.

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

    () => { store.dispatch({ type: 'DECREMENT' }); }); store.subscribe(() => { counter.innerText = store.getState(); });
  28. 49.

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

    () => { store.dispatch({ type: 'DECREMENT' }); }); store.subscribe(() => { counter.innerText = store.getState(); });
  29. 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
  30. 61.

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

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

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

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

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

    decrement = () => ({ type: 'DECREMENT', }); Reusable functions that create actions Action Creators
  33. 64.

    incrementBtn.addEventListener('click', () => { store.dispatch(increment()); }); decrementBtn.addEventListener('click', () => {

    store.dispatch(decrement()); }); Problems: • Creating actions are cumbersome • Requires direct access to store
  34. 65.

    const actions = { increment: () => store.dispatch(increment()), decrement: ()

    => store.dispatch(decrement()), }; Automatically dispatch when invoked Bound Action Creators
  35. 66.

    const actions = { increment: () => store.dispatch(increment()), decrement: ()

    => store.dispatch(decrement()), }; Automatically dispatch when invoked Bound Action Creators Manually created
  36. 67.

    const actions = { increment: () => store.dispatch(increment()), decrement: ()

    => store.dispatch(decrement()), }; Automatically dispatch when invoked Bound Action Creators
  37. 68.

    import { bindActionCreators } from 'redux'; const actions = bindActionCreators(

    { increment, decrement }, store.dispatch ); Automatically dispatch when invoked Bound Action Creators
  38. 69.

    import { bindActionCreators } from 'redux'; const actions = bindActionCreators(

    { increment, decrement }, store.dispatch ); Automatically dispatch when invoked Bound Action Creators
  39. 70.

    import { bindActionCreators } from 'redux'; const actions = bindActionCreators(

    { increment, decrement }, store.dispatch ); Automatically dispatch when invoked Bound Action Creators Automatically created
  40. 71.

    Problems: • Creating actions are cumbersome • Requires direct access

    to store incrementBtn.addEventListener('click', actions.increment); decrementBtn.addEventListener('click', actions.decrement);
  41. 72.
  42. 80.

    import React from 'react'; import { render } from 'react-dom';

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

    import React from 'react'; import { render } from 'react-dom';

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

    import React from 'react'; import { render } from 'react-dom';

    import { Provider } from 'react-redux'; render(( <Provider store={store}> <MyAppContainer /> </Provider> ), document.getElementById('main'));
  45. 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);
  46. 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);
  47. 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);
  48. 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);
  49. 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);
  50. 88.

    const MyApp = (props) => ( <div> <button onClick={props.onDecrement}> -

    </button> <div>{props.counter}</div> <button onClick={props.onIncrement}> + </button> </div> );
  51. 89.

    const MyApp = (props) => ( <div> <button onClick={props.onDecrement}> -

    </button> <div>{props.counter}</div> <button onClick={props.onIncrement}> + </button> </div> ); Store state mapStateToProps
  52. 90.

    const MyApp = (props) => ( <div> <button onClick={props.onDecrement}> -

    </button> <div>{props.counter}</div> <button onClick={props.onIncrement}> + </button> </div> ); Bound action creators mapDispatchToProps
  53. 95.

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

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

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

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

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

    decrement = () => ({ type: 'DECREMENT', }); const changeColor = color => ({ type: 'CHANGE_COLOR', payload: color, });
  56. 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; } }
  57. 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; } }
  58. 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; } }
  59. 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; } }
  60. 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; } }
  61. 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; } }
  62. 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' } }
  63. 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' } }
  64. 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' } }
  65. 110.

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

    'INCREMENT': return state + 1; case 'DECREMENT': return state - 1; default: return state; } }
  66. 111.

    const initialState = { color: 'red' }; function carReducer(state =

    initialState, action) { switch (action.type) { case 'CHANGE_COLOR': return { ...state, color: action.payload }; default: return state; } }
  67. 112.

    const initialState = { color: 'red' }; function carReducer(state =

    initialState, action) { switch (action.type) { case 'CHANGE_COLOR': return { ...state, color: action.payload }; default: return state; } }
  68. 113.

    const initialState = { color: 'red' }; function carReducer(state =

    initialState, action) { switch (action.type) { case 'CHANGE_COLOR': return { ...state, color: action.payload }; default: return state; } }
  69. 118.
  70. 119.
  71. 120.
  72. 129.
  73. 132.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    console.log('dispatch', action); const result = next(action); console.log('state =', api.getState()); return result; };
  82. 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' } }
  83. 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' } }
  84. 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' } }
  85. 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' } }
  86. 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' } }
  87. 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; } }
  88. 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; } }
  89. 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; } }
  90. 150.

    const requestUser = id => ({ type: 'REQUEST_USER', payload: id,

    }); const receiveUser = user => ({ type: 'RECEIVE_USER', payload: user, });
  91. 154.

    const thunkMiddleware = api => next => action => {

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

    const thunkMiddleware = api => next => action => {

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

    const thunkMiddleware = api => next => action => {

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

    const thunkMiddleware = api => next => action => {

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

    function fetchUser(id) { return dispatch => { dispatch(requestUser(id)); return axios.get(`/users/${id}`)

    .then(({ data: user }) => { dispatch(receiveUser(user)); }); }; } store.dispatch(fetchUser(1));
  96. 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
  97. 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
  98. 161.

    function fetchUser(id) { return dispatch => { dispatch(requestUser(id)); return axios.get(`/users/${id}`)

    .then(({ data: user }) => { dispatch(receiveUser(user)); }); }; } store.dispatch(fetchUser(1));
  99. 165.
  100. 166.

    • Uses ES6 generator functions • Write asynchronous code in

    synchronous manner • Great for coordinating multiple API calls redux-saga.js.org
  101. 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
  102. 171.

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

    declarative observable chains • Great for composing asynchronous operations
  103. 172.

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

    declarative observable chains • Great for composing asynchronous operations • Very concise code
  104. 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!
  105. 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.
  106. 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)); }); }; }
  107. 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
  108. 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’)); }); });
  109. 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’)); }); });
  110. 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’)); }); });
  111. 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’)); }); });
  112. 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’)); }); });
  113. 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’)); }); });
  114. 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’)); }); });
  115. 190.

    Use integration tests to ensure all pieces work together. Allow

    store, reducer, and actions to all interact .
  116. 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' }); }); });
  117. 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' }); }); });
  118. 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' }); }); });
  119. 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' }); }); });
  120. 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' }); }); });