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. 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_USERS': return { ...state, isFetching: true }; case 'RECEIVE_USERS': return { ...state, isFetching: false, users: action.payload, }; default: return state; } }
  88. 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; } }
  89. 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; } }
  90. 150.

    const requestUsers = () => ({ type: 'REQUEST_USERS', }); const

    receiveUsers = users => ({ type: 'RECEIVE_USERS', payload: users, });
  91. 151.

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

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

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

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

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

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

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

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

    function fetchUsers() { return dispatch => { dispatch(requestUsers()); return axios.get('/users')

    .then(({ data }) => { dispatch(receiveUsers(data)); }); }; } store.dispatch(fetchUsers());
  96. 156.

    function fetchUsers() { return dispatch => { dispatch(requestUsers()); return axios.get('/users')

    .then(({ data }) => { dispatch(receiveUsers(data)); }); }; } store.dispatch(fetchUsers()); Action Creator
  97. 157.

    function fetchUsers() { return dispatch => { dispatch(requestUsers()); return axios.get('/users')

    .then(({ data }) => { dispatch(receiveUsers(data)); }); }; } store.dispatch(fetchUsers()); Action
  98. 158.

    function fetchUsers() { return dispatch => { dispatch(requestUsers()); return axios.get('/users')

    .then(({ data }) => { dispatch(receiveUsers(data)); }); }; } store.dispatch(fetchUsers());
  99. 161.

    • Appropriate for more complex business logic and async flows.

    • Middleware-based like thunk middleware. Alternatives for async code.
  100. 162.

    • Appropriate for more complex business logic and async flows.

    • Middleware-based like thunk middleware. • redux-saga Alternatives for async code.
  101. 163.

    • Appropriate for more complex business logic and async flows.

    • Middleware-based like thunk middleware. • redux-saga • redux-observable Alternatives for async code.
  102. 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.
  103. 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!
  104. 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.
  105. 168.

    function fetchUsers() { return dispatch => { dispatch(requestUsers()); return axios.get('/users')

    .then(({ data }) => { dispatch(receiveUsers(data)); }); }; } However, you should test async action creators.
  106. 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
  107. 170.

    Use integration tests to ensure all pieces work together. Allow

    store, reducer, and actions to all interact .
  108. 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' }); }); });
  109. 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' }); }); });
  110. 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' }); }); });
  111. 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' }); }); });