React @ Scale

React @ Scale

6696617169722009ed1ec8c52496c6da?s=128

Daniel Cousineau

May 04, 2019
Tweet

Transcript

  1. React @ Scale

  2. @dcousineau

  3. None
  4. None
  5. None
  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
  7. Don’t avoid local state

  8. onClick(e) { this.setState({val: e.target.value.toUpperCase()}); } // ...or... const Component =

    () => { const [val, setVal] = useState(''); return ( <input value={val} onChange={e => setVal(e.target.value.toUpperCase())} /> ); }
  9. onClick() { this.setState((prevState, currentProps) => { return { show: !prevState.show,

    }; }); } // ...or... const Component = () => { const [show, setShow] = useState(false); return ( <a onClick={setShow(prevShow => !prevShow)}>Hello</a> ); }
  10. onClick(e) { this.setState({value: e.target.value}); this.props.onChange(this.state.value); }

  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 ( <input value={val} onChange={e => setVal(e.target.value)} /> ); }
  12. Hoist local state early and often

  13. this.state.checked = true;

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

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

    = true; checked: true
  16. Use hooks (on new components)

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

  18. useState useEffect useRef useContext this.setState(…) componentDidUpdate() {…} React.createRef() contextTypes: {…}

    https://reactjs.org/docs/hooks-reference.html
  19. eslint-plugin-react-hooks https://www.npmjs.com/package/eslint-plugin-react-hooks

  20. Use Suspense (and React.lazy)

  21. const lazyComponent = (lazyImport, fallback = null) => { const

    LazyComponent = React.lazy(lazyImport); return props => ( <React.Suspense fallback={fallback}> <LazyComponent {...props} /> </React.Suspense> ); }; const AsyncComponent = lazyComponent(() => import('./async.js')); const Component = ({ show }) => { return ( <div> {show && <AsyncComponent any={props} you={want} />} </div> ); }
  22. Don’t avoid Redux (hooks are not a replacement)

  23. Be judicious about what you store in Redux

  24. • API Responses (“shared cache”) • Data required at deeply

    nested levels • Truly global layout state (menu open, etc)
  25. Use “selector” functions to read Redux store

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

    return store.users.currentUserId; }
  27. const mapStateToProps = (store) => { const currentUserId = selectCurrentUserId(store);

    return { user: selectUserById(store, currentUserId), }; }; export default connect(mapStateToProps)(YourComponent);
  28. https://github.com/reduxjs/reselect

  29. Normalize API responses based on Domain model

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

  31. { data: { ...entities }, namespaces: [`${ns}`], [ns]: { ids:

    ['id0', ..., 'idN'], ...meta } }
  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 } }
  33. function selectUserById(store, userId) { return store.users.data[userId]; } function selectUsersByNamespace(store, ns)

    { return store.users[ns].ids.map(userId => selectUserById(store, userId)); }
  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 }; }
  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 } }; } }
  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 } }; } }
  37. function selectUsersAreFetching(store, ns) { return !!store.users[ns].isFetching; } function selectManagersAreFetching(store) {

    return selectUsersAreFetching(store, 'allManagers'); }
  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 } }; } }
  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 } }; } }
  40. function selectUserById(store, userId) { return store.users.draftsById[userId] || store.users.data[userId]; }

  41. function reducer(state = defaultState, action) { switch(action.type) { case UNDO_UPDATE_USER:

    return { ...state, draftsById: { ...omit(state.draftsById, action.user.id), } }; } }
  42. Use Webpack!

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

  44. const path = require('path'); module.exports = { mode: 'development', entry:

    './index.js', output: { path: path.resolve('./dist'), filename: 'index.bundle.js' } }; webpack.config.js
  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

  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]
  47. entry output module optimization plugins

  48. Use webpack-dev-server

  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
  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]
  51. None
  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
  53. Optimize fresh starts for new developers

  54. How fast can you deploy?

  55. None
  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
  57. Use feature flags

  58. <Feature name="new-feature" fallback={<OldFeatureComponent />}> <NewFeatureComponent /> </Feature>

  59. None
  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
  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
  62. Can you optimize your directory structure around team responsibilities? If

    teams are organized by “product domain”, Can you organize code around product domain?
  63. Final Thoughts

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

    performance later.
  65. You will not get it perfect the first time. Optimize

    your code for easier refactoring.
  66. Questions?