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

React+Redux @ Scale

React+Redux @ Scale

Daniel Cousineau

June 26, 2017
Tweet

More Decks by Daniel Cousineau

Other Decks in Programming

Transcript

  1. React+Redux @ Scale

    View Slide

  2. @dcousineau

    View Slide

  3. View Slide

  4. View Slide

  5. View Slide

  6. View Slide

  7. Rules

    View Slide

  8. View Slide

  9. “Rules”

    View Slide

  10. View Slide

  11. View Slide

  12. Scalability is the capability of a system, network, or process
    to handle a growing amount of work, or its potential to be
    enlarged to accommodate that growth.
    – Wikipedia

    View Slide

  13. Part 1: React

    View Slide

  14. Rule: Components should be stateless

    View Slide

  15. Reality: State is the enemy, but also inevitable

    View Slide

  16. onClick(e) {
    const value = e.target.value;
    const formatted = value.toUpperCase();
    this.setState({value: formatted});
    }

    View Slide

  17. onClick() {
    this.setState((previousState, currentProps) => {
    return {
    show: !previousState.show,
    };
    });
    }

    View Slide

  18. onClick(e) {
    this.setState({value: e.target.value});
    this.props.onChange(this.state.value);
    }

    View Slide

  19. onClick(e) {
    this.setState({value: e.target.value}, () => {
    this.props.onChange(this.state.value);
    });
    }

    View Slide

  20. Rule: Don’t use Context, it hides complexity

    View Slide

  21. Reality: Sometimes complexity should be hidden

    View Slide

  22. View Slide

  23. View Slide

  24. class TextCard extends React.Component {
    static contextTypes = {
    metatypes: React.PropTypes.object,
    };
    render() {
    const {cardData} = this.props;
    const {metatypes} = this.context;
    return (

    The following is either editable or displayed:


    )
    }
    }
    function selectCardComponent(cardData) {
    switch (cardData.type) {
    case 'text': return TextCard;
    default: throw new Error(`Invalid card type ${cardData.type}`);
    }
    }

    View Slide

  25. class TextCard extends React.Component {
    static contextTypes = {
    metatypes: React.PropTypes.object,
    };
    render() {
    const {cardData} = this.props;
    const {metatypes} = this.context;
    return (

    The following is either editable or displayed:


    )
    }
    }
    function selectCardComponent(cardData) {
    switch (cardData.type) {
    case 'text': return TextCard;
    default: throw new Error(`Invalid card type ${cardData.type}`);
    }
    }

    View Slide

  26. const metatypesEdit = {
    text: class extends React.Component {
    render() {
    return ;
    }
    }
    }
    const metatypesView = {
    text: class extends React.Component {
    render() {
    return {this.props.value};
    }
    }
    }

    View Slide

  27. class CardViewer extends React.Component {
    static childContextTypes = {
    metatypes: React.PropTypes.object
    };
    getChildContext() {
    return {metatypes: metatypesView};
    }
    render() {
    const {cardData} = this.props;
    const CardComponent = selectCardComponent(cardData);
    return
    }
    }

    View Slide

  28. class CardEditor extends React.Component {
    static childContextTypes = {
    metatypes: React.PropTypes.object
    };
    getChildContext() {
    return {metatypes: metatypesEdit};
    }
    render() {
    const {cardData} = this.props;
    const CardComponent = selectCardComponent(cardData);
    return
    }
    }

    View Slide

  29. Part 2: Redux

    View Slide

  30. Rule: “Single source of truth” means all state in the store

    View Slide

  31. Reality: You can have multiple “single sources”

    View Slide

  32. this.state.checked = true;

    View Slide

  33. this.props.checked = true; this.props.checked = true; this.props.checked = true;
    this.state.checked = true;

    View Slide

  34. this.props.checked = true; this.props.checked = true; this.props.checked = true;
    this.props.checked = true; checked: true
    connect()();

    View Slide

  35. window.location.*

    View Slide

  36. Rule: Side effects should happen outside the Redux cycle

    View Slide

  37. Reality: This doesn’t mean you can’t have callbacks

    View Slide

  38. function persistPostAction(post, callback = () => {}) {
    return {
    type: 'PERSIST_POST',
    post,
    callback
    };
    }
    function *fetchPostsSaga(action) {
    const status = yield putPostAPI(action.post);
    yield put(persistPostCompleteAction(status));
    yield call(action.callback, status);
    }
    class ComposePost extends React.Component {
    onClickSubmit() {
    const {dispatch} = this.props;
    const {post} = this.state;
    dispatch(persistPostAction(post, () => this.displaySuccessBanner()));
    }
    }

    View Slide

  39. class ViewPostPage extends React.Component {
    componentWillMount() {
    const {dispatch, postId} = this.props;
    dispatch(fetchPostAction(postId, () => this.logPageLoadComplete()));
    }
    }

    View Slide

  40. Rule: Redux stores must be normalized for performance

    View Slide

  41. Reality: You must normalize to reduce complexity

    View Slide

  42. https://medium.com/@dcousineau/advanced-redux-entity-normalization-f5f1fe2aefc5

    View Slide

  43. {
    byId: {
    ...entities
    },
    keyWindows: [`${keyWindowName}`],
    [keyWindowName]: {
    ids: ['id0', ..., 'idN'],
    ...meta
    }
    }

    View Slide

  44. {
    byId: {
    'a': userA, 'b': userB, 'c': userC, 'd': userD
    },
    keyWindows: ['browseUsers', 'allManagers'],
    browseUsers: {
    ids: ['a', 'b', 'c'],
    isFetching: false,
    page: 1,
    totalPages: 10,
    next: '/users?page=2',
    last: '/users?page=10'
    },
    allManagers: {
    ids: ['d', 'a'],
    isFetching: false
    }
    }

    View Slide

  45. function selectUserById(store, userId) {
    return store.users.byId[userId];
    }
    function selectUsersByKeyWindow(store, keyWindow) {
    return store.users[keyWindow].ids.map(userId => selectUserById(store, userId));
    }

    View Slide

  46. function fetchUsers({query}, keyWindow) {
    return {
    type: FETCH_USERS,
    query,
    keyWindow
    };
    }
    function fetchManagers() {
    return fetchUsers({query: {isManager: true}}, 'allManager');
    }
    function receiveEntities(entities, keyWindow) {
    return {
    type: RECEIVE_ENTITIES,
    entities,
    keyWindow
    };
    }

    View Slide

  47. function reducer(state = defaultState, action) {
    switch(action.type) {
    case FETCH_USERS:
    return {
    ...state,
    keyWindows: uniq([...state.keyWindows, action.keyWindow]),
    [action.keyWindow]: {
    ...state[action.keyWindow],
    isFetching: true,
    query: action.query
    }
    };
    case RECEIVE_ENTITIES:
    return {
    ...state,
    byId: {
    ...state.byId,
    ...action.entities.users.byId
    },
    keyWindows: uniq([...state.keyWindows, action.keyWindow]),
    [action.keyWindow]: {
    ...state[action.keyWindow],
    isFetching: false,
    ids: action.entities.users.ids
    }
    };
    }
    }

    View Slide

  48. function reducer(state = defaultState, action) {
    switch(action.type) {
    case FETCH_USERS:
    return {
    ...state,
    keyWindows: uniq([...state.keyWindows, action.keyWindow]),
    [action.keyWindow]: {
    ...state[action.keyWindow],
    isFetching: true,
    query: action.query
    }
    };
    case RECEIVE_ENTITIES:
    return {
    ...state,
    byId: {
    ...state.byId,
    ...action.entities.users.byId
    },
    keyWindows: uniq([...state.keyWindows, action.keyWindow]),
    [action.keyWindow]: {
    ...state[action.keyWindow],
    isFetching: false,
    ids: action.entities.users.ids
    }
    };
    }
    }

    View Slide

  49. function selectUsersAreFetching(store, keyWindow) {
    return !!store.users[keyWindow].isFetching;
    }
    function selectManagersAreFetching(store) {
    return selectUsersAreFetching(store, 'allManagers');
    }

    View Slide

  50. function reducer(state = defaultState, action) {
    switch(action.type) {
    case UPDATE_USER:
    return {
    ...state,
    draftsById: {
    ...state.draftsById,
    [action.user.id]: action.user
    }
    };
    case RECEIVE_ENTITIES:
    return {
    ...state,
    byId: {
    ...state.byId,
    ...action.entities.users.byId
    },
    draftsById: {
    ...omit(state.draftsById, action.entities.users.byId)
    },
    keyWindows: uniq([...state.keyWindows, action.keyWindow]),
    [action.keyWindow]: {
    ...state[action.keyWindow],
    isFetching: false,
    ids: action.entities.users.ids
    }
    };
    }
    }

    View Slide

  51. function reducer(state = defaultState, action) {
    switch(action.type) {
    case UPDATE_USER:
    return {
    ...state,
    draftsById: {
    ...state.draftsById,
    [action.user.id]: action.user
    }
    };
    case RECEIVE_ENTITIES:
    return {
    ...state,
    byId: {
    ...state.byId,
    ...action.entities.users.byId
    },
    draftsById: {
    ...omit(state.draftsById, action.entities.users.byId)
    },
    keyWindows: uniq([...state.keyWindows, action.keyWindow]),
    [action.keyWindow]: {
    ...state[action.keyWindow],
    isFetching: false,
    ids: action.entities.users.ids
    }
    };
    }
    }

    View Slide

  52. function selectUserById(store, userId) {
    return store.users.draftsById[userId] || store.users.byId[userId];
    }

    View Slide

  53. function reducer(state = defaultState, action) {
    switch(action.type) {
    case UNDO_UPDATE_USER:
    return {
    ...state,
    draftsById: {
    ...omit(state.draftsById, action.user.id),
    }
    };
    }
    }

    View Slide

  54. Part 3: Scale

    View Slide

  55. Rule: Keep dependencies low to keep the application fast

    View Slide

  56. Reality: Use bundling to increase PERCEIVED performance

    View Slide

  57. class Routes extends React.Component {
    render() {
    return (

    component={require(‘../home').default} />
    component={lazy(require(‘bundle-loader?lazy&name=admin!../admin’))} />


    );
    }
    }

    View Slide

  58. require('bundle-loader?lazy&name=admin!../admin’)

    View Slide

  59. const lazy = loader => class extends React.Component {
    componentWillMount() {
    loader(mod =>
    this.setState({
    Component: mod.default ? mod.default : mod
    })
    );
    }
    render() {
    const { Component } = this.state;
    if (Component !== null) {
    return ;
    } else {
    return Is Loading!;
    }
    }
    };

    View Slide

  60. View Slide

  61. Rule: Render up-to-date data

    View Slide

  62. Reality: If you got something render it, update it later

    View Slide

  63. View Slide

  64. View Slide

  65. View Slide

  66. View Slide

  67. View Slide

  68. View Slide

  69. Epilog: Scale?

    View Slide

  70. Rule: Scale is bytes served, users concurrent

    View Slide

  71. Reality: Scale is responding to bytes served and users concurrent

    View Slide

  72. How fast can you deploy?

    View Slide

  73. View Slide

  74. Pre: Clear homebrew & yarn caches
    1. Reinstall node & yarn via brew
    2. Clone repo
    3. Run yarn install
    4. Run production build
    1. Compile & Minify CSS
    2. Compile Server via Babel
    3. Compile, Minify, & Gzip via Webpack
    190.64s
    ~3 min

    View Slide

  75. }>


    View Slide

  76. View Slide

  77. Team 1
    Team 2
    Merge Feature A
    Merge Feature B
    Deploy
    Deploy
    OMG
    ROLLBACK
    DEPLOY!!!
    Merge Feature C
    Merge Bugfix for A
    Deploy
    Deploy BLOCKED!!!
    Deploy

    View Slide

  78. Team 1
    Team 2
    Merge Feature A
    Merge Feature B
    Deploy
    Deploy
    Rollout Flag A
    Rollout Flag B
    OMG
    ROLLBACK
    FLAG
    A!!!
    Merge Feature C
    Deploy
    Merge Bugfix for A
    Deploy
    Rollout Flag A
    Rollout Flag C

    View Slide

  79. Can you optimize your directory structure around team responsibilities?
    If teams are organized by “product domain”,
    Can you organize code around product domain?

    View Slide

  80. Final Thoughts

    View Slide

  81. Strict rules rarely 100% apply to your application.
    Remembering the purpose behind the rules is valuable.

    View Slide

  82. Code behavior should be predictable and intuitable.
    Be realistic about the problem you’re actually solving.

    View Slide

  83. You will not get it perfect the first time.
    Optimize your processes for refactoring.

    View Slide

  84. Questions?

    View Slide