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

React @ Scale

React @ Scale

Daniel Cousineau

May 04, 2019
Tweet

More Decks by Daniel Cousineau

Other Decks in Programming

Transcript

  1. React @ Scale

    View Slide

  2. @dcousineau

    View Slide

  3. View Slide

  4. View Slide

  5. View Slide

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

  7. Don’t avoid local state

    View Slide

  8. onClick(e) {
    this.setState({val: e.target.value.toUpperCase()});
    }
    // ...or...
    const Component = () => {
    const [val, setVal] = useState('');
    return (
    value={val}
    onChange={e => setVal(e.target.value.toUpperCase())}
    />
    );
    }

    View Slide

  9. onClick() {
    this.setState((prevState, currentProps) => {
    return {
    show: !prevState.show,
    };
    });
    }
    // ...or...
    const Component = () => {
    const [show, setShow] = useState(false);
    return (
    !prevShow)}>Hello
    );
    }

    View Slide

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

    View Slide

  11. onClick(e) {
    this.setState({value: e.target.value}, () => {
    this.props.onChange(this.state.value);
    });
    }
    // ... or ...
    const Component = ({ onChange }) => {
    const [val, setVal] = useState('');
    useEffect(() => {
    onChange(val);
    }, [val]);
    return (
    value={val}
    onChange={e => setVal(e.target.value)}
    />
    );
    }

    View Slide

  12. Hoist local state early and often

    View Slide

  13. this.state.checked = true;

    View Slide

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

    View Slide

  15. this.props.checked = true; this.props.checked = true; this.props.checked = true;
    this.props.checked = true; checked: true

    View Slide

  16. Use hooks (on new components)

    View Slide

  17. https://reactjs.org/docs/hooks-intro.html
    Don’t rewrite everything in hooks

    View Slide

  18. useState
    useEffect
    useRef
    useContext
    this.setState(…)
    componentDidUpdate() {…}
    React.createRef()
    contextTypes: {…}
    https://reactjs.org/docs/hooks-reference.html

    View Slide

  19. eslint-plugin-react-hooks
    https://www.npmjs.com/package/eslint-plugin-react-hooks

    View Slide

  20. Use Suspense (and React.lazy)

    View Slide

  21. const lazyComponent = (lazyImport, fallback = null) => {
    const LazyComponent = React.lazy(lazyImport);
    return props => (



    );
    };
    const AsyncComponent = lazyComponent(() => import('./async.js'));
    const Component = ({ show }) => {
    return (

    {show && }

    );
    }

    View Slide

  22. Don’t avoid Redux (hooks are not a replacement)

    View Slide

  23. Be judicious about what you store in Redux

    View Slide

  24. • API Responses (“shared cache”)
    • Data required at deeply nested levels
    • Truly global layout state (menu open, etc)

    View Slide

  25. Use “selector” functions to read Redux store

    View Slide

  26. function selectUserById(store, userId) {
    return store.users.data[userId];
    }
    function selectCurrentUserId(store) {
    return store.users.currentUserId;
    }

    View Slide

  27. const mapStateToProps = (store) => {
    const currentUserId = selectCurrentUserId(store);
    return {
    user: selectUserById(store, currentUserId),
    };
    };
    export default connect(mapStateToProps)(YourComponent);

    View Slide

  28. https://github.com/reduxjs/reselect

    View Slide

  29. Normalize API responses based on Domain model

    View Slide

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

    View Slide

  31. {
    data: {
    ...entities
    },
    namespaces: [`${ns}`],
    [ns]: {
    ids: ['id0', ..., 'idN'],
    ...meta
    }
    }

    View Slide

  32. {
    data: {
    'a': userA, 'b': userB, 'c': userC, 'd': userD
    },
    namespaces: ['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

  33. function selectUserById(store, userId) {
    return store.users.data[userId];
    }
    function selectUsersByNamespace(store, ns) {
    return store.users[ns].ids.map(userId => selectUserById(store, userId));
    }

    View Slide

  34. function fetchUsers({query}, ns) {
    return {
    type: FETCH_USERS,
    query,
    ns
    };
    }
    function fetchManagers() {
    return fetchUsers({query: {isManager: true}}, 'allManagers');
    }
    function receiveEntities(entities, ns) {
    return {
    type: RECEIVE_ENTITIES,
    entities,
    ns
    };
    }

    View Slide

  35. function reducer(state = defaultState, action) {
    switch(action.type) {
    case FETCH_USERS:
    return {
    ...state,
    namespaces: uniq([...state.namespaces, action.ns]),
    [action.ns]: {
    ...state[action.ns],
    isFetching: true,
    query: action.query
    }
    };
    case RECEIVE_ENTITIES:
    return {
    ...state,
    data: {
    ...state.data,
    ...action.entities.users.data
    },
    namespaces: uniq([...state.namespaces, action.ns]),
    [action.ns]: {
    ...state[action.ns],
    isFetching: false,
    ids: action.entities.users.ids
    }
    };
    }
    }

    View Slide

  36. function reducer(state = defaultState, action) {
    switch(action.type) {
    case FETCH_USERS:
    return {
    ...state,
    namespaces: uniq([...state.namespaces, action.ns]),
    [action.ns]: {
    ...state[action.ns],
    isFetching: true,
    query: action.query
    }
    };
    case RECEIVE_ENTITIES:
    return {
    ...state,
    data: {
    ...state.data,
    ...action.entities.users.data
    },
    namespaces: uniq([...state.namespaces, action.ns]),
    [action.ns]: {
    ...state[action.ns],
    isFetching: false,
    ids: action.entities.users.ids
    }
    };
    }
    }

    View Slide

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

    View Slide

  38. 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,
    data: {
    ...state.data,
    ...action.entities.users.data
    },
    draftsById: {
    ...omit(state.draftsById, keys(action.entities.users.data))
    },
    namespaces: uniq([...state.namespaces, action.ns]),
    [action.ns]: {
    ...state[action.ns],
    isFetching: false,
    ids: action.entities.users.ids
    }
    };
    }
    }

    View Slide

  39. 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,
    data: {
    ...state.data,
    ...action.entities.users.data
    },
    draftsById: {
    ...omit(state.draftsById, keys(action.entities.users.data))
    },
    namespaces: uniq([...state.namespaces, action.ns]),
    [action.ns]: {
    ...state[action.ns],
    isFetching: false,
    ids: action.entities.users.ids
    }
    };
    }
    }

    View Slide

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

    View Slide

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

    View Slide

  42. Use Webpack!

    View Slide

  43. Use Webpack! Have a discrete (FE) asset build pipeline

    View Slide

  44. const path = require('path');
    module.exports = {
    mode: 'development',
    entry: './index.js',
    output: {
    path: path.resolve('./dist'),
    filename: 'index.bundle.js'
    }
    };
    webpack.config.js

    View Slide

  45. import('./async.js').then(() => /* Finished loading async module! */);
    https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#Dynamic_Imports
    index.js

    View Slide

  46. Hash: 27b16bd3779b96e0ae26
    Version: webpack 4.30.0
    Time: 51ms
    Built at: 05/01/2019 6:31:57 PM
    Asset Size Chunks Chunk Names
    0.index.bundle.js 325 bytes 0 [emitted]
    index.bundle.js 8.21 KiB main [emitted] main
    Entrypoint main = index.bundle.js
    [./async.js] 33 bytes {0} [built]
    [./index.js] 36 bytes {main} [built]

    View Slide

  47. entry output
    module optimization
    plugins

    View Slide

  48. Use webpack-dev-server

    View Slide

  49. const path = require('path');
    const webpack = require('webpack');
    module.exports = {
    mode: 'development',
    entry: {
    index: [
    require.resolve('webpack-dev-server/client') + '?http://localhost:3002/',
    require.resolve('webpack/hot/dev-server'),
    './index.js',
    ]
    },
    output: {
    path: path.resolve('./dist'),
    publicPath: '/dist/',
    filename: 'index.bundle.js'
    },
    plugins: [
    new webpack.HotModuleReplacementPlugin(),
    ]
    };
    webpack.config.js

    View Slide

  50. > webpack-dev-server --disable-host-check --progress --color --port 3002 --inline=false
    ℹ 「wds」: Project is running at http://localhost:3002/webpack-dev-server/
    ℹ 「wds」: webpack output is served from /dist/
    ℹ 「wdm」: Hash: 39da216c3734d73fa85c
    Version: webpack 4.30.0
    Time: 278ms
    Built at: 05/01/2019 6:37:44 PM
    Asset Size Chunks Chunk Names
    0.index.bundle.js 325 bytes 0 [emitted]
    index.bundle.js 382 KiB index [emitted] index
    Entrypoint index = index.bundle.js
    [0] multi (webpack)-dev-server/client?http://localhost:3002/ (webpack)/hot/dev-server.js ./index.js 52
    bytes {index} [built]
    [./async.js] 33 bytes {0} [built]
    [./index.js] 36 bytes {index} [built]
    [./node_modules/loglevel/lib/loglevel.js] 7.68 KiB {index} [built]
    [./node_modules/querystring-es3/index.js] 127 bytes {index} [built]
    [./node_modules/url/url.js] 22.8 KiB {index} [built]
    [./node_modules/webpack-dev-server/client/index.js?http://localhost:3002/] (webpack)-dev-server/client?
    http://localhost:3002/ 8.26 KiB {index} [built]
    [./node_modules/webpack-dev-server/client/overlay.js] (webpack)-dev-server/client/overlay.js 3.59 KiB
    {index} [built]
    [./node_modules/webpack-dev-server/client/socket.js] (webpack)-dev-server/client/socket.js 1.05 KiB
    {index} [built]

    View Slide

  51. View Slide

  52. const express = require('express');
    const app = express();
    //...
    app.use(
    /(^\/dist|^\/dist\/.*\.hot-update\.js(on)?|__webpack_hmr)/i,
    streamingHttpProxy('http://localhost:3002', { forwardPath: req =># req.originalUrl })
    );
    Proxy your dev server
    Or use https://github.com/jantimon/html-webpack-plugin

    View Slide

  53. Optimize fresh starts for new developers

    View Slide

  54. How fast can you deploy?

    View Slide

  55. View Slide

  56. Pre: Clear homebrew & npm caches
    1. Clone repo
    2. Run npm install
    3. Run production build
    1. Compile & Minify CSS
    2. Compile, Minify, & Gzip via Webpack
    138.42s
    ~2 min

    View Slide

  57. Use feature flags

    View Slide

  58. }>


    View Slide

  59. View Slide

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

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

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

  63. Final Thoughts

    View Slide

  64. If you’re struggling, they’re struggling.
    Focus on developer ergonomics first, performance later.

    View Slide

  65. You will not get it perfect the first time.
    Optimize your code for easier refactoring.

    View Slide

  66. Questions?

    View Slide