kentcdodds / advanced-react-patterns-v2

Created with CodeSandbox
https://codesandbox.io/s/github/kentcdodds/advanced-react-patterns-v2
1.51k stars 567 forks source link

Consumers still re-rendering after passing this.state to provider #48

Closed amkoehler closed 6 years ago

amkoehler commented 6 years ago

Hi Kent - I'm having some trouble seeing the optimization discussed in exercise 3 for preventing unnecessary re-renders of the consumers. It could be due to a misunderstanding by me of how those consumers work. To test it out I added a simple counter button that updates the state of the Usage component to trigger a re-render. I also added a console.log within Toggle.Button to check when a rendering happens.

// Flexible Compound Components with context

import React from 'react'
import {Switch} from '../switch'

const ToggleContext = React.createContext();

function ToggleConsumer(props) {
  return (
    <ToggleContext.Consumer>
      {
        (context) => {
          if (!context) {
            throw new Error('Toggle compound components must be rendered within the Toggle component');
          }

          return props.children(context);
        }
      }
    </ToggleContext.Consumer>
  );
}

class Toggle extends React.Component {
  static On = (props) => (
    <ToggleConsumer>
      {({ on }) => on ? props.children : null}
    </ToggleConsumer>
  );
  static Off = (props) => (
    <ToggleConsumer>
      {({ on }) => on ? null : props.children}
    </ToggleConsumer>
  );
  static Button = (props) => {
    return (
      <ToggleConsumer>
        {({ on, toggle }) => {
          console.log('rendering button');

          return <Switch on={on} onClick={toggle} {...props} />
        }}
      </ToggleConsumer>
    );
  }

  toggle = () =>
  this.setState(
    ({on}) => ({on: !on}),
    () => this.props.onToggle(this.state.on),
    )

  state = { on: false, toggle: this.toggle }
  render() {
    // UN-OPTIMIZED
    // return (
    //   <ToggleContext.Provider value={{on: this.state.on, toggle: this.toggle }}>
    //     {this.props.children}
    //   </ToggleContext.Provider>
    // );

    // OPTIMIZED
    return (
      <ToggleContext.Provider value={this.state}>
        {this.props.children}
      </ToggleContext.Provider>
    );
  }
}

class Usage extends React.Component {
  static title = 'Flexible Compound Components';
  state = { count: 0 };

  increment = () => {
    this.setState((prevState) => ({ count: prevState.count + 1}));
  }

  onToggle = (...args) => console.log('onToggle', ...args);

  render() {
    const onToggle = this.props.onToggle || this.onToggle;

    return (
      <React.Fragment>
        <div>{this.state.count}</div>
        <button onClick={this.increment}>+</button>
        <Toggle onToggle={onToggle}>
          <div>
            <Toggle.Button />
          </div>
          <Toggle.On>The button is on</Toggle.On>
          <Toggle.Off>The button is off</Toggle.Off>
        </Toggle>
      </React.Fragment>

    );
  }
}

export {Toggle, Usage as default}

My expectation was that with the optimization of passing this.state I'd no longer see rendering button logged every time I increment my counter. But it still is getting logged. If there is a better forum for this question please let me know!

kentcdodds commented 6 years ago

I believe that if you make the button a pure component (either via extending PureComponent, or using the upcoming React.pure API) then that will get you what you're looking for :)

Thanks!