React + Redux @ Scale @dcousineau Rules Rules Scalability is the - - PowerPoint PPT Presentation

react redux scale dcousineau rules rules scalability is
SMART_READER_LITE
LIVE PREVIEW

React + Redux @ Scale @dcousineau Rules Rules Scalability is the - - PowerPoint PPT Presentation

React + Redux @ Scale @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


slide-1
SLIDE 1

React+Redux @ Scale

slide-2
SLIDE 2

@dcousineau

slide-3
SLIDE 3
slide-4
SLIDE 4
slide-5
SLIDE 5
slide-6
SLIDE 6
slide-7
SLIDE 7

Rules

slide-8
SLIDE 8
slide-9
SLIDE 9

“Rules”

slide-10
SLIDE 10
slide-11
SLIDE 11
slide-12
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

slide-13
SLIDE 13

Part 1: React

slide-14
SLIDE 14

Rule: Components should be stateless

slide-15
SLIDE 15

Reality: State is the enemy, but also inevitable

slide-16
SLIDE 16
  • nClick(e) {

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

slide-17
SLIDE 17
  • nClick() {

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

slide-18
SLIDE 18
  • nClick(e) {

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

slide-19
SLIDE 19
  • nClick(e) {

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

slide-20
SLIDE 20

Rule: Don’t use Context, it hides complexity

slide-21
SLIDE 21

Reality: Sometimes complexity should be hidden

slide-22
SLIDE 22
slide-23
SLIDE 23
slide-24
SLIDE 24

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

slide-25
SLIDE 25

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

slide-26
SLIDE 26

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

slide-27
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 <CardComponent cardData={cardData} /> } }

slide-28
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 <CardComponent cardData={cardData} /> } }

slide-29
SLIDE 29

Part 2: Redux

slide-30
SLIDE 30

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

slide-31
SLIDE 31

Reality: You can have multiple “single sources”

slide-32
SLIDE 32 this.state.checked = true;
slide-33
SLIDE 33 this.props.checked = true; this.props.checked = true; this.props.checked = true; this.state.checked = true;
slide-34
SLIDE 34 this.props.checked = true; this.props.checked = true; this.props.checked = true; this.props.checked = true; checked: true connect()();
slide-35
SLIDE 35

window.location.*

slide-36
SLIDE 36

Rule: Side effects should happen outside the Redux cycle

slide-37
SLIDE 37

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

slide-38
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 {

  • nClickSubmit() {

const {dispatch} = this.props; const {post} = this.state; dispatch(persistPostAction(post, () => this.displaySuccessBanner())); } }

slide-39
SLIDE 39

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

slide-40
SLIDE 40

Rule: Redux stores must be normalized for performance

slide-41
SLIDE 41

Reality: You must normalize to reduce complexity

slide-42
SLIDE 42

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

slide-43
SLIDE 43

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

slide-44
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 } }

slide-45
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)); }

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

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

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

slide-49
SLIDE 49

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

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

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

slide-52
SLIDE 52

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

slide-53
SLIDE 53

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

slide-54
SLIDE 54

Part 3: Scale

slide-55
SLIDE 55

Rule: Keep dependencies low to keep the application fast

slide-56
SLIDE 56

Reality: Use bundling to increase PERCEIVED performance

slide-57
SLIDE 57

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

slide-58
SLIDE 58

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

slide-59
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 <Component {...this.props} />; } else { return <div>Is Loading!</div>; } } };

slide-60
SLIDE 60
slide-61
SLIDE 61

Rule: Render up-to-date data

slide-62
SLIDE 62

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

slide-63
SLIDE 63
slide-64
SLIDE 64
slide-65
SLIDE 65
slide-66
SLIDE 66
slide-67
SLIDE 67
slide-68
SLIDE 68
slide-69
SLIDE 69

Epilog: Scale?

slide-70
SLIDE 70

Rule: Scale is bytes served, users concurrent

slide-71
SLIDE 71

Reality: Scale is responding to bytes served and users concurrent

slide-72
SLIDE 72

How fast can you deploy?

slide-73
SLIDE 73
slide-74
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

slide-75
SLIDE 75

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

slide-76
SLIDE 76
slide-77
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

slide-78
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

slide-79
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?

slide-80
SLIDE 80

Final Thoughts

slide-81
SLIDE 81

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

slide-82
SLIDE 82

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

slide-83
SLIDE 83

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

slide-84
SLIDE 84

Questions?