React+Redux @ Scale
InfoQ.com: News & Community Site • 750,000 unique visitors/month • Published in 4 languages (English, Chinese, Japanese and Brazilian Portuguese) • Post content from our QCon conferences • News 15-20 / week • Articles 3-4 / week • Presentations (videos) 12-15 / week • Interviews 2-3 / week • Books 1 / month Watch the video with slide synchronization on InfoQ.com! https://www.infoq.com/presentations/ react-redux-scale
Presented at QCon New York www.qconnewyork.com Purpose of QCon - to empower software development by facilitating the spread of knowledge and innovation Strategy - practitioner-driven conference designed for YOU: influencers of change and innovation in your teams - speakers and topics driving the evolution and innovation - connecting and catalyzing the influencers and innovators Highlights - attended by more than 12,000 delegates since 2007 - held in 9 cities worldwide
@dcousineau
Rules
“Rules”
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
Part 1: React
Rule: Components should be stateless
Reality: State is the enemy, but also inevitable
onClick(e) { const value = e.target.value; const formatted = value.toUpperCase(); this.setState({value: formatted}); }
onClick() { this.setState((previousState, currentProps) => { return { show: !previousState.show, }; }); }
onClick(e) { this.setState({value: e.target.value}); this.props.onChange(this.state.value); }
onClick(e) { this.setState({value: e.target.value}, () => { this.props.onChange(this.state.value); }); }
Rule: Don’t use Context, it hides complexity
Reality: Sometimes complexity should be hidden
class TextCard extends React.Component { static contextTypes = { metatypes: React.PropTypes.object, }; render() { const {cardData} = this.props; const {metatypes} = this.context; return ( <div> The following is either editable or displayed: <metatypes.text value={cardData.text} onChange={this.props.onChange} /> </div> ) } } function selectCardComponent(cardData) { switch (cardData.type) { case 'text': return TextCard; default: throw new Error(`Invalid card type ${cardData.type}`); } }
class TextCard extends React.Component { static contextTypes = { metatypes: React.PropTypes.object, }; render() { const {cardData} = this.props; const {metatypes} = this.context; return ( <div> The following is either editable or displayed: <metatypes.text value={cardData.text} onChange={this.props.onChange} /> </div> ) } } function selectCardComponent(cardData) { switch (cardData.type) { case 'text': return TextCard; default: throw new Error(`Invalid card type ${cardData.type}`); } }
const metatypesEdit = { text: class extends React.Component { render() { return <input type="text" {...this.props} />; } } } const metatypesView = { text: class extends React.Component { render() { return <span>{this.props.value}</span>; } } }
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 <CardComponent cardData={cardData} /> } }
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 <CardComponent cardData={cardData} /> } }
Part 2: Redux
Rule: “Single source of truth” means all state in the store
Reality: You can have multiple “single sources”
this.state.checked = true;
this.props.checked = true; this.props.checked = true; this.props.checked = true; this.state.checked = true;
this.props.checked = true; this.props.checked = true; this.props.checked = true; this.props.checked = true; checked: true connect()();
window.location.*
Rule: Side effects should happen outside the Redux cycle
Reality: This doesn’t mean you can’t have callbacks
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())); } }
class ViewPostPage extends React.Component { componentWillMount() { const {dispatch, postId} = this.props; dispatch(fetchPostAction(postId, () => this.logPageLoadComplete())); } }
Rule: Redux stores must be normalized for performance
Reality: You must normalize to reduce complexity
https://medium.com/@dcousineau/advanced-redux-entity-normalization-f5f1fe2aefc5
{ byId: { ...entities }, keyWindows: [`${keyWindowName}`], [keyWindowName]: { ids: ['id0', ..., 'idN'], ...meta } }
{ 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 } }
function selectUserById(store, userId) { return store.users.byId[userId]; } function selectUsersByKeyWindow(store, keyWindow) { return store.users[keyWindow].ids.map(userId => selectUserById(store, userId)); }
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 }; }
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 } }; } }
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 } }; } }
function selectUsersAreFetching(store, keyWindow) { return !!store.users[keyWindow].isFetching; } function selectManagersAreFetching(store) { return selectUsersAreFetching(store, 'allManagers'); }
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 } }; } }
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 } }; } }
function selectUserById(store, userId) { return store.users.draftsById[userId] || store.users.byId[userId]; }
function reducer(state = defaultState, action) { switch(action.type) { case UNDO_UPDATE_USER: return { ...state, draftsById: { ...omit(state.draftsById, action.user.id), } }; } }
Part 3: Scale
Rule: Keep dependencies low to keep the application fast
Reality: Use bundling to increase PERCEIVED performance
class Routes extends React.Component { render() { return ( <Switch> <Route exact path="/" component={require(‘../home').default} /> <Route path="/admin" component={lazy(require(‘bundle-loader?lazy&name=admin!../admin’))} /> <Route component={PageNotFound} /> </Switch> ); } }
require('bundle-loader?lazy&name=admin!../admin’)
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 <Component {...this.props} />; } else { return <div>Is Loading!</div>; } } };
Rule: Render up-to-date data
Reality: If you got something render it, update it later
Epilog: Scale?
Rule: Scale is bytes served, users concurrent
Reality: Scale is responding to bytes served and users concurrent
How fast can you deploy?
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
<Feature name="new-feature" fallback={<OldFeatureComponent />}> <NewFeatureComponent /> </Feature>
Team 1 Team 2 MergeFeatureA MergeFeatureB Deploy Deploy OMG ROLLBACK DEPLOY!!! MergeFeatureC MergeBugfixforA Deploy DeployBLOCKED!!! Deploy
Team 1 Team 2 MergeFeatureA MergeFeatureB Deploy Deploy RolloutFlagARolloutFlagBOMG ROLLBACK FLAG A!!! MergeFeatureC Deploy MergeBugfixforA Deploy RolloutFlagA RolloutFlagC
Can you optimize your directory structure around team responsibilities? If teams are organized by “product domain”, Can you organize code around product domain?
Final Thoughts
Strict rules rarely 100% apply to your application. Remembering the purpose behind the rules is valuable.
Code behavior should be predictable and intuitable. Be realistic about the problem you’re actually solving.
You will not get it perfect the first time. Optimize your processes for refactoring.
Questions?
Watch the video with slide synchronization on InfoQ.com! https://www.infoq.com/presentations/react- redux-scale

React+Redux at Scale

  • 1.
  • 2.
    InfoQ.com: News &Community Site • 750,000 unique visitors/month • Published in 4 languages (English, Chinese, Japanese and Brazilian Portuguese) • Post content from our QCon conferences • News 15-20 / week • Articles 3-4 / week • Presentations (videos) 12-15 / week • Interviews 2-3 / week • Books 1 / month Watch the video with slide synchronization on InfoQ.com! https://www.infoq.com/presentations/ react-redux-scale
  • 3.
    Presented at QConNew York www.qconnewyork.com Purpose of QCon - to empower software development by facilitating the spread of knowledge and innovation Strategy - practitioner-driven conference designed for YOU: influencers of change and innovation in your teams - speakers and topics driving the evolution and innovation - connecting and catalyzing the influencers and innovators Highlights - attended by more than 12,000 delegates since 2007 - held in 9 cities worldwide
  • 4.
  • 9.
  • 11.
  • 14.
    Scalability is the capabilityof a system, network, or process to handle a growing amount of work, or its potential to be enlarged to accommodate that growth. – Wikipedia
  • 15.
  • 16.
  • 17.
    Reality: State isthe enemy, but also inevitable
  • 18.
    onClick(e) { const value= e.target.value; const formatted = value.toUpperCase(); this.setState({value: formatted}); }
  • 19.
    onClick() { this.setState((previousState, currentProps)=> { return { show: !previousState.show, }; }); }
  • 20.
  • 21.
    onClick(e) { this.setState({value: e.target.value},() => { this.props.onChange(this.state.value); }); }
  • 22.
    Rule: Don’t useContext, it hides complexity
  • 23.
  • 26.
    class TextCard extendsReact.Component { static contextTypes = { metatypes: React.PropTypes.object, }; render() { const {cardData} = this.props; const {metatypes} = this.context; return ( <div> The following is either editable or displayed: <metatypes.text value={cardData.text} onChange={this.props.onChange} /> </div> ) } } function selectCardComponent(cardData) { switch (cardData.type) { case 'text': return TextCard; default: throw new Error(`Invalid card type ${cardData.type}`); } }
  • 27.
    class TextCard extendsReact.Component { static contextTypes = { metatypes: React.PropTypes.object, }; render() { const {cardData} = this.props; const {metatypes} = this.context; return ( <div> The following is either editable or displayed: <metatypes.text value={cardData.text} onChange={this.props.onChange} /> </div> ) } } function selectCardComponent(cardData) { switch (cardData.type) { case 'text': return TextCard; default: throw new Error(`Invalid card type ${cardData.type}`); } }
  • 28.
    const metatypesEdit ={ text: class extends React.Component { render() { return <input type="text" {...this.props} />; } } } const metatypesView = { text: class extends React.Component { render() { return <span>{this.props.value}</span>; } } }
  • 29.
    class CardViewer extendsReact.Component { static childContextTypes = { metatypes: React.PropTypes.object }; getChildContext() { return {metatypes: metatypesView}; } render() { const {cardData} = this.props; const CardComponent = selectCardComponent(cardData); return <CardComponent cardData={cardData} /> } }
  • 30.
    class CardEditor extendsReact.Component { static childContextTypes = { metatypes: React.PropTypes.object }; getChildContext() { return {metatypes: metatypesEdit}; } render() { const {cardData} = this.props; const CardComponent = selectCardComponent(cardData); return <CardComponent cardData={cardData} /> } }
  • 31.
  • 32.
    Rule: “Single sourceof truth” means all state in the store
  • 33.
    Reality: You canhave multiple “single sources”
  • 34.
  • 35.
    this.props.checked = true;this.props.checked = true; this.props.checked = true; this.state.checked = true;
  • 36.
    this.props.checked = true;this.props.checked = true; this.props.checked = true; this.props.checked = true; checked: true connect()();
  • 37.
  • 38.
    Rule: Side effectsshould happen outside the Redux cycle
  • 39.
    Reality: This doesn’tmean you can’t have callbacks
  • 40.
    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())); } }
  • 41.
    class ViewPostPage extendsReact.Component { componentWillMount() { const {dispatch, postId} = this.props; dispatch(fetchPostAction(postId, () => this.logPageLoadComplete())); } }
  • 42.
    Rule: Redux storesmust be normalized for performance
  • 43.
    Reality: You mustnormalize to reduce complexity
  • 44.
  • 45.
  • 46.
    { 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 } }
  • 47.
    function selectUserById(store, userId){ return store.users.byId[userId]; } function selectUsersByKeyWindow(store, keyWindow) { return store.users[keyWindow].ids.map(userId => selectUserById(store, userId)); }
  • 48.
    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 }; }
  • 49.
    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 } }; } }
  • 50.
    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 } }; } }
  • 51.
    function selectUsersAreFetching(store, keyWindow){ return !!store.users[keyWindow].isFetching; } function selectManagersAreFetching(store) { return selectUsersAreFetching(store, 'allManagers'); }
  • 52.
    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 } }; } }
  • 53.
    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 } }; } }
  • 54.
    function selectUserById(store, userId){ return store.users.draftsById[userId] || store.users.byId[userId]; }
  • 55.
    function reducer(state =defaultState, action) { switch(action.type) { case UNDO_UPDATE_USER: return { ...state, draftsById: { ...omit(state.draftsById, action.user.id), } }; } }
  • 56.
  • 57.
    Rule: Keep dependencieslow to keep the application fast
  • 58.
    Reality: Use bundlingto increase PERCEIVED performance
  • 59.
    class Routes extendsReact.Component { render() { return ( <Switch> <Route exact path="/" component={require(‘../home').default} /> <Route path="/admin" component={lazy(require(‘bundle-loader?lazy&name=admin!../admin’))} /> <Route component={PageNotFound} /> </Switch> ); } }
  • 60.
  • 61.
    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 <Component {...this.props} />; } else { return <div>Is Loading!</div>; } } };
  • 63.
  • 64.
    Reality: If yougot something render it, update it later
  • 71.
  • 72.
    Rule: Scale isbytes served, users concurrent
  • 73.
    Reality: Scale isresponding to bytes served and users concurrent
  • 74.
    How fast canyou deploy?
  • 76.
    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
  • 77.
    <Feature name="new-feature" fallback={<OldFeatureComponent/>}> <NewFeatureComponent /> </Feature>
  • 79.
  • 80.
  • 81.
    Can you optimizeyour directory structure around team responsibilities? If teams are organized by “product domain”, Can you organize code around product domain?
  • 82.
  • 83.
    Strict rules rarely100% apply to your application. Remembering the purpose behind the rules is valuable.
  • 84.
    Code behavior shouldbe predictable and intuitable. Be realistic about the problem you’re actually solving.
  • 85.
    You will notget it perfect the first time. Optimize your processes for refactoring.
  • 86.
  • 87.
    Watch the videowith slide synchronization on InfoQ.com! https://www.infoq.com/presentations/react- redux-scale