Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Connect.Tech 2017: Get Started with Redux

Connect.Tech 2017: Get Started with Redux

Jeremy Fairbank

September 21, 2017
Tweet

More Decks by Jeremy Fairbank

Other Decks in Programming

Transcript

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

    View full-size slide

  2. Software is broken.
    We are here to fix it.
    Say [email protected]

    View full-size slide

  3. The Wild West of State

    View full-size slide


  4. ...

    var $el = $('[data-id="42"]');
    var currentName = $el.data('name');
    $el.data('name', currentName.toUpperCase());
    Data in the DOM

    View full-size slide

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

    View full-size slide

  6. View
    Model
    Model
    Model
    Model
    Model
    View
    Model
    View
    Model
    View
    Model
    View
    Model
    Model
    Two-way Data Binding

    View full-size slide

  7. The Wild West of State
    State is everywhere and
    anyone can change it!

    View full-size slide

  8. Predictable
    State Container

    View full-size slide

  9. All application
    state in one place
    State Container

    View full-size slide

  10. Predictable
    Old
    State
    REDUCER
    New
    State

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  14. Reducer
    View
    State
    Actions

    View full-size slide

  15. Reducer
    View
    State
    Actions

    View full-size slide

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

    View full-size slide

  17. Reducer
    View
    State
    Actions
    Dispatch

    View full-size slide

  18. Reducer
    View
    State
    Actions

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  24. state += 1;
    state -= 1;
    Anyone can access and
    mutate state

    View full-size slide

  25. state += 1
    state -= 1
    { type: 'INCREMENT' }
    { type: 'DECREMENT' }
    Tokens/descriptors that describe a type of change.
    Actions

    View full-size slide

  26. 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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  33. 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

    View full-size slide

  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
    Unhandled types are ignored

    View full-size slide

  35. 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.

    View full-size slide

  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.

    View full-size slide

  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.

    View full-size slide

  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.

    View full-size slide

  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.

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  48. Store
    Reducer
    State
    View
    0

    View full-size slide

  49. Store
    Reducer
    State
    View
    0

    View full-size slide

  50. Store
    Reducer
    State
    View
    0
    getState

    View full-size slide

  51. Store
    Reducer
    View
    State
    0

    View full-size slide

  52. Store
    Reducer
    View
    State
    0
    INCREMENT
    dispatch

    View full-size slide

  53. Store
    Reducer
    View
    State
    0
    INCREMENT
    dispatch

    View full-size slide

  54. Store
    Reducer
    View
    State
    0

    View full-size slide

  55. Store
    Reducer
    View
    State
    1

    View full-size slide

  56. Store
    Reducer
    View
    State
    1

    View full-size slide

  57. Store
    Reducer
    View
    State
    1
    1
    subscribe
    getState

    View full-size slide

  58. incrementBtn.addEventListener('click', () => {
    store.dispatch({ type: 'INCREMENT' });
    });
    decrementBtn.addEventListener('click', () => {
    store.dispatch({ type: 'DECREMENT' });
    });
    Problems:
    • Creating actions are cumbersome
    • Requires direct access to store

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  62. incrementBtn.addEventListener('click', () => {
    store.dispatch(increment());
    });
    decrementBtn.addEventListener('click', () => {
    store.dispatch(decrement());
    });
    Problems:
    • Creating actions are cumbersome
    • Requires direct access to store

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  69. Problems:
    • Creating actions are cumbersome
    • Requires direct access to store
    incrementBtn.addEventListener('click', actions.increment);
    decrementBtn.addEventListener('click', actions.decrement);

    View full-size slide

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

    View full-size slide

  71. Reducer
    State
    Actions
    React
    Redux

    View full-size slide

  72. React
    Redux
    React
    Application

    View full-size slide

  73. React
    Redux
    React
    Application
    Provider

    View full-size slide

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

    View full-size slide

  75. Component
    Child
    React
    Redux
    React
    Application
    Provider
    connect
    State
    Child
    Action
    Creators

    View full-size slide

  76. const MyApp = () => (

    -
    0
    +

    );

    View full-size slide

  77. import React from 'react';
    import { render } from 'react-dom';
    import { Provider } from 'react-redux';
    render((



    ), document.getElementById('main'));

    View full-size slide

  78. import React from 'react';
    import { render } from 'react-dom';
    import { Provider } from 'react-redux';
    render((



    ), document.getElementById('main'));

    View full-size slide

  79. import React from 'react';
    import { render } from 'react-dom';
    import { Provider } from 'react-redux';
    render((



    ), document.getElementById('main'));

    View full-size slide

  80. 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);

    View full-size slide

  81. 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);

    View full-size slide

  82. 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);

    View full-size slide

  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);

    View full-size slide

  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);

    View full-size slide

  85. const MyApp = (props) => (


    -

    {props.counter}

    +


    );

    View full-size slide

  86. const MyApp = (props) => (


    -

    {props.counter}

    +


    );
    Store state
    mapStateToProps

    View full-size slide

  87. const MyApp = (props) => (


    -

    {props.counter}

    +


    );
    Bound action creators
    mapDispatchToProps

    View full-size slide

  88. Immutable
    Object State

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  95. 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;
    }
    }

    View full-size slide

  96. 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;
    }
    }

    View full-size slide

  97. 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;
    }
    }

    View full-size slide

  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;
    }
    }

    View full-size slide

  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;
    }
    }

    View full-size slide

  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;
    }
    }

    View full-size slide

  101. 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' } }

    View full-size slide

  102. 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' } }

    View full-size slide

  103. 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' } }

    View full-size slide

  104. function reducer(state = initialState, action) {
    }

    View full-size slide

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

    View full-size slide

  106. Root
    Reducer
    Counter
    Reducer
    Car
    Reducer

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  121. Interact
    with APIs?

    View full-size slide

  122. Middleware
    Enhance Redux applications

    View full-size slide

  123. • Logging
    Middleware
    Enhance Redux applications

    View full-size slide

  124. • Logging
    • Debugging
    Middleware
    Enhance Redux applications

    View full-size slide

  125. • Logging
    • Debugging
    • API interaction
    Middleware
    Enhance Redux applications

    View full-size slide

  126. • Logging
    • Debugging
    • API interaction
    • Custom actions
    Middleware
    Enhance Redux applications

    View full-size slide

  127. Reducer
    View
    State
    Actions
    Middleware

    View full-size slide

  128. Reducer
    View
    State
    Actions
    Middleware
    Intercept

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  138. 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' } }

    View full-size slide

  139. 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' } }

    View full-size slide

  140. 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' } }

    View full-size slide

  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' } }

    View full-size slide

  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' } }

    View full-size slide

  143. const initialState = {
    status: 'READY',
    user: null,
    };

    View full-size slide

  144. 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;
    }
    }

    View full-size slide

  145. 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;
    }
    }

    View full-size slide

  146. 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;
    }
    }

    View full-size slide

  147. const requestUser = id => ({
    type: 'REQUEST_USER',
    payload: id,
    });
    const receiveUser = user => ({
    type: 'RECEIVE_USER',
    payload: user,
    });

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  156. 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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  159. Alternative
    Async Middleware

    View full-size slide

  160. redux-saga.js.org

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  164. • 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

    View full-size slide

  165. redux-observable.js.org

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  170. And plenty more options…

    View full-size slide

  171. • redux-logic
    And plenty more options…

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  174. • redux-logic
    • redux-ship
    • redux-promise
    • redux-api-middleware
    And plenty more options…

    View full-size slide

  175. ×
    ✓ Testing

    View full-size slide

  176. 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!

    View full-size slide

  177. 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.

    View full-size slide

  178. 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));
    });
    };
    }

    View full-size slide

  179. 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

    View full-size slide

  180. 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’));
    });
    });

    View full-size slide

  181. 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’));
    });
    });

    View full-size slide

  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’));
    });
    });

    View full-size slide

  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’));
    });
    });

    View full-size slide

  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’));
    });
    });

    View full-size slide

  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’));
    });
    });

    View full-size slide

  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’));
    });
    });

    View full-size slide

  187. Use integration tests to ensure all
    pieces work together.
    Allow store, reducer, and
    actions to all interact .

    View full-size slide

  188. 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' });
    });
    });

    View full-size slide

  189. 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' });
    });
    });

    View full-size slide

  190. 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' });
    });
    });

    View full-size slide

  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' });
    });
    });

    View full-size slide

  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' });
    });
    });

    View full-size slide

  193. Resources
    • Redux
    • redux.js.org
    • egghead.io/courses/getting-started-with-
    redux
    • React
    • github.com/reactjs/react-redux

    View full-size slide

  194. Thanks!
    Slides: bit.ly/redux-connect
    Jeremy Fairbank
    @elpapapollo / jfairbank

    View full-size slide