npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

pure-render-prop-component

v2.0.1

Published

Use the render prop pattern without sacrificing component purity

Downloads

7

Readme

PureRenderPropComponent

npm version Build Status Greenkeeper badge

npm install --save pure-render-prop-component
# or
yarn add pure-render-prop-component

The render prop 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 prop 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. PureRenderPropComponent is my implementation of that solution.

Usage

import React, {Component} from 'react'
import PureRenderPropComponent from 'pure-render-prop-component'

class CurrentTime extends PureRenderPropComponent {
  state = {currentTime: Date.now()}
  render() {
    return this.props.children(this.state.currentTime, this.props.extraProps)
    // NOTE: PureRenderPropComponent also supports a prop named 'render' ☟
    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: PureRenderPropComponent also supports a prop named 'render'
          // (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 prop 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 prop component and they will be treated in the same way.

import React, {Component} from 'react'
import PureRenderPropComponent from 'pure-render-prop-component'

class CurrentTime extends PureRenderPropComponent {
  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 prop will also re-render when the boolean passed into CurrentTime’s format prop changes.

Caveats & Assumptions

  • PureRenderPropComponent assumes you will either use a props.children prop:

    <RenderCallbackComponent>
      {(val, extraProps) => <Node />}
    <RenderCallbackComponent>

    or a “render prop”:

    <RenderCallbackComponent render={(val, extraProps) => <Node />} />

    Using either one for a purpose other than the render prop 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)
  )
}