pure-render-callback-component v1.1.1
PureRenderCallbackComponent
npm install --save pure-render-callback-component
# or
yarn add pure-render-callback-componentThe render callback pattern in React allows us to build highly functional components, but it doesn’t play well with how React currently manages re-rendering.
import React, {Component} from 'react'
class CurrentTime extends Component {
state = {currentTime: Date.now()}
render() {
return this.props.children(this.state.currentTime)
}
}
class App extends Component {
render() {
return (
<div>
<CurrentTime>
{// *
currentTime => (
<div>
<p>{this.props.pageTitle}</p>
<p>{currentTime}</p>
</div>
)}
</CurrentTime>
</div>
)
}
}Here, our CurrentTime component will re-render every time our App component renders, even if neither CurrentTime’s state nor its props (in this case, its children function) have changed.
However, changing CurrentTime to inherit from PureComponent doesn’t help. The React docs explain why: PureComponent compares state and props, and only if a property of state or of props has changed, does it re-render. In the above case, every time App re-renders, the render callback supplied to CurrentTime (marked *) is recreated. Two functions which look the same are still two different functions, so CurrentTime#props.children has changed, and CurrentTime re-renders.
We can solve this by, as the React docs put it, defining the function “as an instance method”, in other words, moving the function out of our App component’s render method.
import React, {Component, PureComponent} from 'react'
class CurrentTime extends PureComponent {
state = {currentTime: Date.now()}
render() {
return this.props.children(this.state.currentTime)
}
}
class App extends Component {
currentTimeCallback = currentTime => (
<div>
<p>{this.props.pageTitle}</p>
<p>{currentTime}</p>
</div>
)
render() {
return (
<div>
<CurrentTime>{this.currentTimeCallback}</CurrentTime>
</div>
)
}
}Now, currentTimeCallback is only created once. PureComponent compares props before and after the re-render of App, finds that the children function hasn’t changed, and aborts the re-render of CurrentTime. Performance improved!
But there is a big problem waiting to happen. Our currentTimeCallback doesn’t just depend on the currentTime argument passed down from our CurrentTime component. It also renders App’s props.pageTitle. But with the above setup, when pageTitle changes, currentTimeCallback will not re-render. It will show the old pageTitle.
I struggled with this problem, trying all sorts of horrible hacks, until I came across this Github issue on the React repo, and the suggestion by a React developer of a possible solution. PureRenderCallbackComponent is my implementation of that solution.
Usage
import React, {Component} from 'react'
import PureRenderCallbackComponent from 'pure-render-callback-component'
class CurrentTime extends PureRenderCallbackComponent {
state = {currentTime: Date.now()}
render() {
return this.props.children(this.state.currentTime, this.props.extraProps)
// NOTE: PureRenderCallbackComponent also supports render props ☟
return this.props.render(this.state.currentTime, this.props.extraProps)
}
}
class App extends Component {
render() {
return (
<div>
<CurrentTime extraProps={{pageTitle: this.props.pageTitle}}>
{(currentTime, extraProps) => (
<div>
<p>{extraProps.pageTitle}</p>
<p>{currentTime}</p>
</div>
)}
</CurrentTime>
{
// NOTE: PureRenderCallbackComponent also supports render props (instead of children) ☟
}
<CurrentTime
extraProps={{pageTitle: this.props.pageTitle}}
render={(currentTime, extraProps) => (
<div>
<p>{extraProps.pageTitle}</p>
<p>{currentTime}</p>
</div>
)}
/>
</div>
)
}
}Now, our render callback will always re-render when, and only when, CurrentTime#state.currentTime or App#props.pageTitle change.
You can also pass other props into your render callback component and they will be treated in the same way.
import React, {Component} from 'react'
import PureRenderCallbackComponent from 'pure-render-callback-component'
class CurrentTime extends PureRenderCallbackComponent {
state = {currentTime: Date.now()}
format(timestamp) {
return String(new Date(timestamp))
}
render() {
const time = this.props.format
? this.format(this.state.currentTime)
: this.state.currentTime
return this.props.children(time, this.props.extraProps)
}
}
class App extends Component {
render() {
return (
<div>
<CurrentTime
format={true}
extraProps={{pageTitle: this.props.pageTitle}}>
{(currentTime, extraProps) => (
<div>
<p>{extraProps.pageTitle}</p>
<p>{currentTime}</p>
</div>
)}
</CurrentTime>
</div>
)
}
}Here, our render callback will also re-render when the boolean passed into CurrentTime’s format prop changes.
Caveats & Assumptions
PureRenderCallbackComponentassumes you will either use aprops.childrencallback:<RenderCallbackComponent> {(val, extraProps) => <Node />} <RenderCallbackComponent>or a “render prop”:
<RenderCallbackComponent render={(val, extraProps) => <Node />} />Using either one for a purpose other than the render callback pattern will lead to unexpected behaviour, including but not limited to a stale UI due to missed renders.
How does it work?
shouldComponentUpdate(nextProps, nextState) {
const {props, state} = this
const omitKeys = ['extraProps', 'children', 'render']
return (
!shallowEqual(state, nextState) ||
!shallowEqual(omit(props, omitKeys), omit(nextProps, omitKeys)) ||
!shallowEqual(props.extraProps, nextProps.extraProps)
)
}