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