Using selectors effectively

  • Selectors recap
  • How React renders work
  • How redux works
  • How to use selectors

Topics

Selectors recap

const mapStateToProps = state => ({
  continuousPlay: selectContinuousPlay(state)
})

export default connect(mapStateToProps)(HeroPlayer)

Selectors

export const selectContinuousPlay = createSelector(
  [selectPlayer, getDials],
  (player, dials) => {
    if (
      dials.enableContinuousPlay &&
      dials.enableContinuousPlay !== 'false'
    ) {
      return player.continuousPlay;
    }

    return false;
  }
);

Why selectors?

  • Pattern for calculating values from multiple state values
  • Avoids expensive re-calculations of state
  • Avoids expensive React re-renders

How React renders work

React

update (e.g. setState)

render

update DOM (if needed)

<RContainer />

<Modal />

<Button />

<R />

<Provider />

<Link />

<Button />

setState(newState)

A re-render of <EpisodeContainer /> without DOM updates takes ~10ms on the playback app*

*on my Mac

 Avoid re-renders if possible 

Takeaway

How Redux works

Redux store

export default function createStore(reducer) {
  let currentState

  const getState = () => currentState

  const dispatch = action => {
    currentState = reducer(currentState, action)
  }

  // ..

  return {
    dispatch,
    getState
  }
}
import { createStore } from 'redux'
import reducer from './reducer'

const store = createStore(reducer)

combineReducers

export default function combineReducers(reducers) {
  return function combination(state = {}, action) {
    let nextState = {}

    Object.keys(reducers).forEach(key => {
      let reducer = reducers[key]
      let previousStateForKey = state[key]

      nextState[key] = reducer(previousStateForKey, action)
    })

    return nextState
  }
}
import { combineReducers } from 'redux'
import todos from './todosReducer'
import modal from './modalReducer'

const rootReducer = combineReducers({
  todos,
  modal
})

Reducer

const initialState = {
  items: []
};

export default function todosReducer(
  state = initialState,
  action
) {
  if (action.type === "ADD_TODO") {
    return {
      ...state,
      items: [...state.items, action.payload]
    };
  }
  return state;
}

Recap

export default function createStore(reducer) {
  let currentState

  const getState = () => currentState

  const dispatch = action => {
    currentState = reducer(currentState, action)
    // ..
  }

  // ..

  return {
    dispatch,
    getState
  }
}
export default function createStore(reducer) {
  let currentState

  const getState = () => currentState

  const dispatch = action => {
    currentState = reducer(currentState, action)
    // ..
  }

  dispatch({ type: '@@redux/INIT$' })

  return {
    dispatch,
    getState
  }
}

connect/Provider

import { connect } from 'react-redux'

// ..

const ModalContainer = connect(mapStateToProps)(Modal)
import store from './store/store'

const App = () => (
  <Provider store={store}>
    <ModalContainer />
  </Provider>
)

export default App
export default class Provider extends Component {
  constructor(props) {
    super(props)
    this.state = {
      storeState: props.store.getState()
    }
  }

  // ..

  render() {
    return (
      <Context.Provider value={this.state}>
        {this.props.children}
      </Context.Provider>
    )
  }
}
export default class Provider extends Component {
  // ..
  componentDidMount() {
    const store = this.props.store

    store.subscribe(() => {
      this.setState({
        storeState: store.getState()
      })
    })
  }
  // ..
}
export default function createStore(reducer) {
  // ..
  let listeners = []

  // ..

  const subscribe = listener => {
    listeners.push(listener)
  }

  const dispatch = action => {
    // ..
    listeners.forEach(l => l())
  }

  // ..
}

<RContainer />

<Modal />

<Button />

<R />

<Provider />

<Link />

<Button />

this.setState({
  storeState: store.getState()
})

Connect

export default function connectHOC(mapStateToProps, mapDispatchToProps) {
  return function wrapWithConnect(WrappedComponent) {
    let prevProps
    let prevComponent

    return () => {
      const renderWrappedComponent = ({ storeState }) => {
        const props = mapStateToProps(storeState)
        if(!shallowEquals(prevProps, props) {
          prevComponent = <WrappedComponent {...props} />
        }
        return prevComponent
      }
      return <Context.Consumer>{renderWrappedComponent}</Context.Consumer>
    }
  }
}
export default function connectHOC(mapStateToProps, mapDispatchToProps) {
  return function wrapWithConnect(WrappedComponent) {
    let prevProps
    let prevComponent

    return () => {
      const renderWrappedComponent = ({ storeState }) => {
        const props = mapStateToProps(storeState)
        if(!shallowEquals(prevProps, props) {
          prevComponent = React.createElement(WrappedComponent, props)
        }
        return prevComponent
      }
      return <Context.Consumer>{renderWrappedComponent}</Context.Consumer>
    }
  }
}
import { connect } from 'react-redux'

// ..

const ModalContainer = connect(mapStateToProps)(Modal)

shallowEqual

function shallowEqual(objA, objB) {
  const keysA = Object.keys(objA)
  const keysB = Object.keys(objB)

  for (let i = 0; i < keysA.length; i++) {
    if (objA[keysA[i]] !== objB[keysA[i]]) {
      return false
    }
  }

  return true
}

Redux checks that each new prop strict equals (===) the previous prop

Redux re-renders the component if any prop does not strict equal the previous prop

Strict equals

const obj = {}

obj === obj // true
obj === {} // false
const arr = []

arr === arr // true
arr === [] // false
const str = 'string'

str === str // true
str === 'string' // true
const num = 1

num === num // true
num === 1 // true
const bool = true

bool === bool // true
bool === true // true

Takeaways

Redux re-renders based on strict equality checks

If mapStateToProps returns a prop as a new object, the component tree will re-render each time dispatch is called!

How to use selectors

Problem

const mapStateToProps = state => ({
  subtitles: {
    text: state.subtitle.long,
    color: state.theme.color
  }
})

Reselect

subtitlesSelector(oldState) === subtitlesSelector(newState)

The solution is to memoize objects using a selector library like Reselect

Reselect

Reselect works by memoizing (caching) the last call of a function

Reselect

const memoizeFn = fn => {
  let prevResult
  let prevArg

   
      
    




  
}
const memoizeFn = fn => {
  let prevResult
  let prevArg
  return arg => {







  }
}
const memoizeFn = fn => {
  let prevResult
  let prevArg
  return arg => {
    if (prevArg === arg) {
      return prevResult
    }
    const result = fn(arg)
    
    
    
  }
}
const memoizeFn = fn => {
  let prevResult
  let prevArg
  return arg => {
    if (prevArg === arg) {
      return prevResult
    }
    
    
   
    
  }
}
const memoizeFn = fn => {
  let prevResult
  let prevArg
  return arg => {
    if (prevArg === arg) {
      return prevResult
    }
    const result = fn(arg)
    prevArg = arg
    prevResult = result

  }
}
const memoizeFn = fn => {
  let prevResult
  let prevArg
  return arg => {
    if (prevArg === arg) {
      return prevResult
    }
    const result = fn(arg)
    prevArg = arg
    prevResult = result
    return result
  }
}
export const selectUpNextType = createSelector(
  [getUpNext],
  (upNext) => {
    if (!upNext) {
      return null
    }

    return upNext.source
  }
)

Dependency

Effective selectors

Return a primitive value

OR

Ensure new object isn't created if dependencies don't change

Return a primitive value

export const selectUpNextType = createSelector(
  [getUpNext],
  (upNext) => {
    if (!upNext) {
      return null
    }

    return { 
      type: upNext.source 
    }
  }
)
export const selectUpNextType = createSelector(
  [getUpNext],
  (upNext) => {
    if (!upNext) {
      return null
    }

    return upNext.source
  }
)

Ensure new object isn't created if dependencies don't change

const getUpNextSource = state => upNext && upNext.source

export const selectUpNextType = createSelector(
  [getUpNextSource],
  (upNext) => {
    return { 
      type: upNext.source 
    }
  }
)
export const selectUpNextType = createSelector(
  [getUpNext],
  (upNext) => {
    if (!upNext) {
      return null
    }

    return { 
      type: upNext.source 
    }
  }
)

Considerations

  • How often will reducer change? (e.g. translations never change)
  • Is reducer liable to update often in future?
  • Is value primitive and can it be calculated without a selector (e.g. state.episode.id)

Questions?

Using reselect effectively

By Edd Yerburgh

Using reselect effectively

Using reselect-effectively

  • 1,889