React.PureComponent and childs
Asked Answered
P

1

6

Someone please explain me this last paragraph of the React documentation with a simple example, it's about React.PureComponent , all the examples I've seen are advanced and I'm just starting to know this concept and I can't see exactly what it refers to. Precisely the point that children should also be "pure". To the best of my knowledge, I believe that this statement is over-stated, because if the father does not re-render himself, then neither do the children, or, is there something that escapes me and I cannot visualize it? That is why I need a simple example, I already looked at all the similar questions here but they are advanced examples and do not cover what I am looking for.

"Furthermore, React.PureComponent's shouldComponentUpdate() skips prop updates for the whole component subtree. Make sure all the children components are also "pure""

Pomerleau answered 27/5, 2022 at 18:20 Comment(3)
See also github.com/facebook/react/issues/10610.Corbitt
If it is somewhat ambiguous, what I deduce in my humble opinion, is that the React documentation says it simply to REAFFIRM that the PureComponent itself will be Pure, the children have a "separate" treaty, you decide if you make them pure or not, that is, that the parent is pure does not make the children automatically pure, I think that's what the documentation means and that's why it warns that.Pomerleau
That's why I said in the question that I think it's redundant that statement in the documentation, but hopefully someone else will see this post and tell us what we're missing.Pomerleau
P
0

I made something to play with.

I think the important part to realize, is that there is only one difference, it is the shouldComponentUpdate implementation on PureComponent.

I am also confused about Make sure all the children components are also “pure”. From my understanding, it is meant as a warning, as a guideline, not as an absolute don't. When you put a parent as PureComponent, all the childrens become impacted. But someone not used to shouldComponentUpdate (PureComponent) may be surprised by childrens behavior (not updating) when mixing both in the tree.

Take for example in the second snippet this.state.quotes[0].text = "StackOverflow save the day";. When using Component on both <List/> and <ListElement/>, any setState would update the view. But when using PureComponent on only <List/>, someone may expect <ListElement/> to update, but it won't.

Imagine that, that instead of writing <List/> as pure, and <ListElement/> as unpure, what they warn NOT to do. You would have written both as pure. Would you even think of doing something like this.state.quotes[0].text = "StackOverflow save the day"; ? No you would start with this.state.quotes[0] = {...}. And then you would take a look at which component handle the data above, the quotes array, the component handling the array, is it pure ? Yes it is, then write this.state = [{...}] instead. You do this "recreating references/deep copy" until you find the first non-pure component. If you followed their advice, at the first non-pure component, you know there won't be any other stale reference blocking your rendering.

If using deep comparison, you would never have that issue. I am using hooks components, they are much more friendly. I recommend you skip class component and use hooks. In hooks to implement shouldComponentUpdate you use React.memo. Since YOU decide how you compare props, it becomes quite obvious that if you compare previousList === newList (shallow comparison), you will miss any update/add/delete on the list. And naturally you will do a deep comparison (use deep-equal lib or (avoid) JSON.stringify(previousList) === JSON.stringify(newList). Note that React.Memo is better for another reason, it only handle props comparison, not both state and props comparison as PureComponent.

I don't think you need every children to use shallow comparison, if the component render fast enough, using shouldComponentUpdate (or React.memo) can make it worse (remember it is a performance optimization).

This bellow demonstrate the issues we can get with shallow props comparison (first snippet) and shallow state comparison (second snippet):

import { Component, PureComponent } from 'react';

interface Quote {
  author: string;
  text: string;
}

export class Playground extends Component {
  state = {
    quotes: [
      {
        author: 'Ambroise',
        text: 'I like React.'
      }
    ]
  }

  onClickOK () {
    this.setState({
      quotes: [
        {
          author: 'Ariel',
          text: 'Prefer VueJS.'
        }
      ]
    });
  }

  // https://reactjs.org/docs/react-api.html#reactpurecomponent
  // React.PureComponent implements it with a shallow prop and state comparison
  // "prop comparison"
  onClickIssue() {
    if (window['dontMix']) {
      alert("Please don't mix on click issues. Reload page.")
      return;
    }
    window['dontMix'] = true;

    // This won't work if <List/> is a PureComponent
    // Note that we ALSO CHANGED the props for <ListElement/> here, different object, differents strings,
    // but <ListElement/> will NOT re-render, regardless of <ListElement/> being pure or not.
    const quotesUpdated = this.state.quotes;
    quotesUpdated.pop();
    quotesUpdated.push({
      author: 'Thomas',
      text: 'Prefer Angular.'
    });

    // Shallow props comparison, <List/> will compare array references
    // `quotesUpdated === this.state.quotes` and because it is true,
    // it will not re-render, or any of the childrens.
    //
    // The quote object is different, so <ListElement/>
    // would be expected to re-render.
    this.setState({
      quotes: quotesUpdated
    });
  }

  onClickIssue2() {
    if (window['dontMix']) {
      alert("Please don't mix on click issues. Reload page.")
      return;
    }
    window['dontMix'] = true;

    // This won't work if <ListElement/> is a PureComponent since the object in first index stay the same.
    // Since we recreate the array, <List/> will always re-render regardless of <List/> being pure.
    this.state.quotes[0].author = "Thomas";
    this.state.quotes[0].text = "Prefer Angular.";
    this.setState({
      quotes: [
        this.state.quotes[0],
      ]
    });
  }

  render() {
    return (
      <section>
        <button onClick={this.onClickOK.bind(this)}>Get updated</button>
        <button onClick={this.onClickIssue.bind(this)}>Problem</button>
        <button onClick={this.onClickIssue2.bind(this)}>Problem2</button>
        <List quotes={this.state.quotes}/>
      </section>
    );
  }
}

// Change PureComponent to Component, no problem with shallow comparison anymore.
class List extends PureComponent<{ quotes: Quote[]; }>{
  render() {
    return (
      <ul>
        {
          this.props.quotes.map(
            (e, i) => <ListElement key={i} quote={e}/>
          )
        }
      </ul>
    );
  }
}

class ListElement extends PureComponent<{quote: Quote}> {
  render() {
    return (
      <li>
        <h3>{this.props.quote.author}</h3>
        <p>{this.props.quote.text}</p>
      </li>
    );
  }
}

A second snippet, focusing on the state shallow comparison, and also <ListElement/> start as a Component this time instead of a PureComponent.

import { Component, PureComponent } from 'react';

interface Quote {
  author: string;
  text: string;
}

export class Playground2 extends Component {
  state = {
    quotes: [
      {
        author: 'Ambroise',
        text: 'I like React.'
      }
    ]
  };

  render() {
    return (
      <section>
        <List quotes={this.state.quotes}/>
      </section>
    );
  }
}

// Note: Careful not to confuse state.quotes and props.quote
class List extends PureComponent<{ quotes: Quote[]; }> {
  state = {
    quotes: this.props.quotes
  };

  // https://reactjs.org/docs/react-api.html#reactpurecomponent
  // React.PureComponent implements it with a shallow prop and state comparison
  // "state comparison"
  fail() {
    for (let i = 0; i < this.state.quotes.length; i++) {
      this.state.quotes.pop();
    }

    this.setState({
      quotes: this.state.quotes
    });
  }

  success() {
    // This will never work, because `previousState === newState` (both are this.state)
    // this.state.quotes = [];
    // this.setState(this.state);

    this.setState({
      quotes: []
    });
  }

  // It work if you change this class to Component
  changeChild() {
    // if you clicked another button reload the page to get initial state.
    if (this.state.quotes.length === 0) {
      alert("You clicked another button. Please reload page to test this one.");
    }

    // previously "I like React."
    this.state.quotes[0].text = "StackOverflow save the day";

    // NOTICE: Choose only one setState bellow, comment the others.

    // Won't work
    // this.setState({
    //   quotes: this.state.quotes
    // });

    // This will never work, because `previousState === newState` (both are this.state)
    // this.state.quotes = [this.state.quotes[0]];
    // this.setState(this.state);

    // Won't work
    this.setState(this.state);

    // This will work when <List/> is a PureComponent and <ListElement/> a Component,
    // both this.state and this.state.quotes are different
    // this.setState({
    //   quotes: [this.state.quotes[0]]
    // });
  }

  render() {
    return (
      <div>
        <button onClick={this.fail.bind(this)}>Empty the list (FAIL if PureComponent)</button>
        <button onClick={this.success.bind(this)}>Empty the list (SUCCESS)</button>
        <button onClick={this.changeChild.bind(this)}>Change child (FAIL if PureComponent)</button>
        <ul>
          {
            this.state.quotes.map(
              (e, i) => <ListElement key={i} quote={e}/>
            )
          }
        </ul>
      </div>
    );
  }
}

// ATTENTION: This one start as a Component this time, instead of a PureComponent
class ListElement extends Component<{quote: Quote}, {author: string}> {
  state = {
    author: this.props.quote.author,
  };

  // Yep this work.
  takeOwnership() {
    this.setState({
      author: "Constantin"
    })
  }

  render() {
    return (
      <li>
        <button onClick={this.takeOwnership.bind(this)}>Take ownership !</button>
        <h3>{this.state.author}</h3>
        <p>{this.props.quote.text}</p>
      </li>
    );
  }
}

TLDR; I also find it confusing, it seems more like and advice, I recommend not putting too much attention on it, and to use hooks components, where you have more fined grained control, over props/state comparison and shallow/deep comparison.

Pneumogastric answered 28/5, 2022 at 0:14 Comment(2)
Ambroise Rabier, When you say " When you put a parent as PureComponent, all the childrens become impacted." do you mean that because the parent does not re-render as a consequence neither do the children, right?Pomerleau
@DanielTinajeroDíaz Yes I am referring to "skips prop updates for the whole component subtree". It is demonstrated in onClickIssue function. Although the quote object reference changed, because the array containing it stay the same reference, and List is pure, it won't re-render ListElement.Pneumogastric

© 2022 - 2024 — McMap. All rights reserved.