Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Children don't re-render when parent component is optimised #6515

Closed
benoitgrelard opened this issue Apr 14, 2016 · 12 comments
Closed

Children don't re-render when parent component is optimised #6515

benoitgrelard opened this issue Apr 14, 2016 · 12 comments

Comments

@benoitgrelard
Copy link

I am seeing something that looks like a bug and have written a simple test case for it.

Please refer to this WebpackBin as it contains the test scenario:
http://www.webpackbin.com/EJpabsuJW

@jimfb
Copy link
Contributor

jimfb commented Apr 14, 2016

This is a usage question, rather than a bug in React. Your shouldComponentUpdate function is explicitly telling React that it should not update, so obviously it does not update, this is expected behavior.

Optimizing components that can take in arbitrary children is somewhat non-trivial. In general, it will require walking/diffing the children in some way, and (for completely general children) this is technically not possible for the same reasons that componentWillReceiveProps does not guarantee props will be different (http://facebook.github.io/react/blog/2016/01/08/A-implies-B-does-not-imply-B-implies-A.html).

Also, keep in mind that shouldComponentUpdate should be used somewhat sparingly in critical parts of your application. Diffing values (especially a tree of values, like children is likely to be) can be expensive, and if your shouldComponentUpdate is ultimately going to return true anyway, than React will need to do that same diffing again during reconciliation, so you're just wasting CPU cycles (ie. it might actually be cheaper as a whole to just skip implementing that function).

Anyway, I'm going to close out the issue, since it's not a bug in React. Feel free to continue the discussion on this thread, or move the discussion to StackOverflow.

@gaearon
Copy link
Collaborator

gaearon commented Apr 14, 2016

This might be an inconvenience or even a conceptual problem, but definitely not a bug.
As you note here:

  shouldComponentUpdate(nextProps) {
    // naïve, only for demo purpose
    return nextProps.color !== this.props.color;

    // how to optimize this for children??
    // it doesn't make sense for it to have to check for its
    // children, as it's simple composition, it shouldn't
    // have to care about what is passed through it.
  }

Indeed, just nextProps.color !== this.props.color will bail out for children changes so it is not enough. Usually people compare props shallowly so children are included in the comparison.

This begs the question: isn’t this negating the benefits of shouldComponentUpdate? If your children are static, hoisting constant elements in production would help but this doesn’t apply to cases where you rely on this.state inside the child element.

There was some discussion about taking children off of props so you might want to have a look at it for some historical context: #4694.

@benoitgrelard
Copy link
Author

@gaearon I realise this implementation is naïve, and it does make sense when thinking about the fact that even though children are passed from the outside, they are only rendered within the parent's render function, wherever you're going to do {children}.

To be clear, this example was obviously very contrived and doesn't really make sense on its own as you probably wouldn't try to optimise things in this case.

In my real scenario, I'm however having to deal with trying to optimise things on hundreds of these components.

To give a bit more insight into what I'm building:

I am building a low level performant grid component (similar to FixedDataTable ) that can handle thousands of rows and columns, as well as handling lots of realtime updates. I am using Immutable.js for the data so that it's easier and more efficient to handle.

My problem appeared when I changed the top level Grid API from passing value getters for each cell value, (columnHeaderValueGetter, rowHeaderValueGetter and cellValueGetter) to wanting to let users render their own cells the way they want.

Using getters at the top level component meant it was easy to optimise my Cell component using shouldComponentUpdate because it would end up receiving a value prop (easy to check for changes). But this API wasn't really flexible and made it hard to implement anything else (say the user wants to have a customisable helper text on hover – using title for example, you'd have to pass all the way through another columnHeaderHelperTextGetter, etc)

From there came the idea to be able to pass your own custom components in the same fashion as FixedDataTable.

But I can't seem to be able to stop everything re-rendering even when not needed (especially for example when scrolling as setState is basically being called on requestAnimationFrame during scroll, meaning a lots of updates, for a lot of cells).

After all your comments regarding how technically difficult it would be to optimise shouldComponentUpdate for arbitrary children, I wonder what to do. And even if it was possible, it still feels ugly to me that the parent component should have to check for changes on its children's props. It feels like it breaks encapsulation and composition.

To me the whole point of being able to pass children from outside, or even as props is that you can easily compose things and separate concerns and responsibilities.

@gaearon, you mention this:

Indeed, just nextProps.color !== this.props.color will bail out for children changes so it is not enough. Usually people compare props shallowly so children are included in the comparison.

How would you do this in practice? I understand shallowCompare on the parent props (which include children) but wouldn't that only basically check for nextProps.children === this.props.children? In which case, I feel like it would be different every render...

It does feel like it negates the benefits of shouldComponentUpdate if React also offers the ability to pass arbitrary components as children or even as props.

@gaearon
Copy link
Collaborator

gaearon commented Apr 14, 2016

For your use case, you can take the same approach (I believe?) that FixedDataTable takes. Rather than accept children, accept renderChildren. For example,

render() {
  return (
    <MyTable
      renderRow={this.renderRow}
      renderHeader={this.renderHeader}
    />
  )
}

Make sure the function references don’t change on every render (just don’t bind them in render()) and you’ll be fine. Sure, closing over this.state won’t bring updates in this case:

renderRow() {
  // won’t update
  return <CustomRow data={this.state.data} />
}

To work around it, make any necessary data props to your table so that it can pass them back to the render* props whenever it needs:

render() {
  return (
    <MyTable
      renderRow={this.renderRow}
      renderHeader={this.renderHeader}
      data={this.state.data}
    />
  )
}

renderRow(data) {
  return <CustomRow data={data} />
}

This way, unless data changes, re-rendering <MyTable /> won’t re-render every row.

How would you do this in practice? I understand shallowCompare on the parent props (which include children) but wouldn't that only basically check for nextProps.children === this.props.children? In which case, I feel like it would be different every render...

Yes, however, most components in my experience don’t take custom children so this only comes up in limited cases, and usually shallowCompare works fine. When it doesn’t, you can make it work good by using the workaround I suggest above.

@benoitgrelard
Copy link
Author

So you mean passing a render callback? Isn't that considered an anti-pattern?

@gaearon
Copy link
Collaborator

gaearon commented Apr 14, 2016

Anti-pattern is a loaded word. It’s a tool; like any tool it has upsides and downsides. In this particular case I think it’s very appropriate.

@jimfb
Copy link
Contributor

jimfb commented Apr 14, 2016

Well, some anti-patterns are just bad, always. But I wouldn't classify passing a render callback as an anti-pattern, I'd just classify it as a pattern.

The pattern is used pretty commonly when a parent needs to pass necessary info to an unowned child. I think it's at least as good as cloning elements or using context.

@benoitgrelard
Copy link
Author

What I meant was, doesn't passing a render component makes the component inherently impure? As now the render callback has access to all sorts of outside variables the function has closed on?

Meaning shouldComponentUpdate wouldn't help here?

@jimfb
Copy link
Contributor

jimfb commented Apr 14, 2016

As long as the callback is pure, everything remains pure. The component is still a function of props (even though the prop happens to be a pure function).

shouldComponentUpdate can check equality of the function, to see if the function is referentially equal to the old function.

@benoitgrelard
Copy link
Author

@gaearon I've updated the example here with the suggested technique: http://www.webpackbin.com/EJtN1ctk-

Although it works, I feel like now we've basically broken encapsulation and made Box couple to its children because it has to account for data it needs to pass through explicitly via renderChildren.

I understand that if you were to render arbitrary children in the render callback now it's somewhat practical, but considering this current example of just a string, how is it different from making the Box component accept a counter prop anyway and render it itself?

What I mean is that now the Box component is responsible for more than it should.

@gaearon
Copy link
Collaborator

gaearon commented Apr 15, 2016

The way I see it, you don’t get the render thousands of rows and keep perfect encapsulation 😄 . Something’s gotta give.

That said I don’t see where the encapsulation is breaking. If you rename it from counter to rowData, it’s pretty abstract and can work for different kinds of children. The fact that you pass a function rather than children themselves—well, that’s an optimization. Maybe you want to support a million rows in the future, and creating all of them is a waste anyway, so you build support for lazy creation right in your API. I don’t really see an issue with this.

@Tzook
Copy link

Tzook commented Sep 24, 2017

We came across this issue as well and we decided to solve it!
So we built an HOC that takes any component and makes its performance much better (by implementing a wiser shouldComponentUpdate)!
Take a look here: https://github.com/NoamELB/shouldComponentUpdate-Children

poteto pushed a commit that referenced this issue Sep 26, 2024
Bumps [axios](https://github.com/axios/axios) from 1.7.1 to 1.7.4.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/axios/axios/releases">axios's
releases</a>.</em></p>
<blockquote>
<h2>Release v1.7.4</h2>
<h2>Release notes:</h2>
<h3>Bug Fixes</h3>
<ul>
<li><strong>sec:</strong> CVE-2024-39338 (<a
href="https://redirect.github.com/axios/axios/issues/6539">#6539</a>)
(<a
href="https://redirect.github.com/axios/axios/issues/6543">#6543</a>)
(<a
href="https://github.com/axios/axios/commit/6b6b605eaf73852fb2dae033f1e786155959de3a">6b6b605</a>)</li>
<li><strong>sec:</strong> disregard protocol-relative URL to remediate
SSRF (<a
href="https://redirect.github.com/axios/axios/issues/6539">#6539</a>)
(<a
href="https://github.com/axios/axios/commit/07a661a2a6b9092c4aa640dcc7f724ec5e65bdda">07a661a</a>)</li>
</ul>
<h3>Contributors to this release</h3>
<ul>
<li><!-- raw HTML omitted --> <a href="https://github.com/levpachmanov"
title="+47/-11 ([#6543](axios/axios#6543)
)">Lev Pachmanov</a></li>
<li><!-- raw HTML omitted --> <a href="https://github.com/hainenber"
title="+49/-4 ([#6539](axios/axios#6539) )">Đỗ
Trọng Hải</a></li>
</ul>
<h2>Release v1.7.3</h2>
<h2>Release notes:</h2>
<h3>Bug Fixes</h3>
<ul>
<li><strong>adapter:</strong> fix progress event emitting; (<a
href="https://redirect.github.com/axios/axios/issues/6518">#6518</a>)
(<a
href="https://github.com/axios/axios/commit/e3c76fc9bdd03aa4d98afaf211df943e2031453f">e3c76fc</a>)</li>
<li><strong>fetch:</strong> fix withCredentials request config (<a
href="https://redirect.github.com/axios/axios/issues/6505">#6505</a>)
(<a
href="https://github.com/axios/axios/commit/85d4d0ea0aae91082f04e303dec46510d1b4e787">85d4d0e</a>)</li>
<li><strong>xhr:</strong> return original config on errors from XHR
adapter (<a
href="https://redirect.github.com/axios/axios/issues/6515">#6515</a>)
(<a
href="https://github.com/axios/axios/commit/8966ee7ea62ecbd6cfb39a905939bcdab5cf6388">8966ee7</a>)</li>
</ul>
<h3>Contributors to this release</h3>
<ul>
<li><!-- raw HTML omitted --> <a
href="https://github.com/DigitalBrainJS" title="+211/-159
([#6518](axios/axios#6518)
[#6519](axios/axios#6519) )">Dmitriy
Mozgovoy</a></li>
<li><!-- raw HTML omitted --> <a href="https://github.com/ValeraS"
title="+3/-3 ([#6515](axios/axios#6515)
)">Valerii Sidorenko</a></li>
<li><!-- raw HTML omitted --> <a href="https://github.com/prianyu"
title="+2/-2 ([#6505](axios/axios#6505)
)">prianYu</a></li>
</ul>
<h2>Release v1.7.2</h2>
<h2>Release notes:</h2>
<h3>Bug Fixes</h3>
<ul>
<li><strong>fetch:</strong> enhance fetch API detection; (<a
href="https://redirect.github.com/axios/axios/issues/6413">#6413</a>)
(<a
href="https://github.com/axios/axios/commit/4f79aef81b7c4644328365bfc33acf0a9ef595bc">4f79aef</a>)</li>
</ul>
<h3>Contributors to this release</h3>
<ul>
<li><!-- raw HTML omitted --> <a
href="https://github.com/DigitalBrainJS" title="+3/-3
([#6413](axios/axios#6413) )">Dmitriy
Mozgovoy</a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/axios/axios/blob/v1.x/CHANGELOG.md">axios's
changelog</a>.</em></p>
<blockquote>
<h2><a
href="https://github.com/axios/axios/compare/v1.7.3...v1.7.4">1.7.4</a>
(2024-08-13)</h2>
<h3>Bug Fixes</h3>
<ul>
<li><strong>sec:</strong> CVE-2024-39338 (<a
href="https://redirect.github.com/axios/axios/issues/6539">#6539</a>)
(<a
href="https://redirect.github.com/axios/axios/issues/6543">#6543</a>)
(<a
href="https://github.com/axios/axios/commit/6b6b605eaf73852fb2dae033f1e786155959de3a">6b6b605</a>)</li>
<li><strong>sec:</strong> disregard protocol-relative URL to remediate
SSRF (<a
href="https://redirect.github.com/axios/axios/issues/6539">#6539</a>)
(<a
href="https://github.com/axios/axios/commit/07a661a2a6b9092c4aa640dcc7f724ec5e65bdda">07a661a</a>)</li>
</ul>
<h3>Contributors to this release</h3>
<ul>
<li><!-- raw HTML omitted --> <a href="https://github.com/levpachmanov"
title="+47/-11 ([#6543](axios/axios#6543)
)">Lev Pachmanov</a></li>
<li><!-- raw HTML omitted --> <a href="https://github.com/hainenber"
title="+49/-4 ([#6539](axios/axios#6539) )">Đỗ
Trọng Hải</a></li>
</ul>
<h2><a
href="https://github.com/axios/axios/compare/v1.7.2...v1.7.3">1.7.3</a>
(2024-08-01)</h2>
<h3>Bug Fixes</h3>
<ul>
<li><strong>adapter:</strong> fix progress event emitting; (<a
href="https://redirect.github.com/axios/axios/issues/6518">#6518</a>)
(<a
href="https://github.com/axios/axios/commit/e3c76fc9bdd03aa4d98afaf211df943e2031453f">e3c76fc</a>)</li>
<li><strong>fetch:</strong> fix withCredentials request config (<a
href="https://redirect.github.com/axios/axios/issues/6505">#6505</a>)
(<a
href="https://github.com/axios/axios/commit/85d4d0ea0aae91082f04e303dec46510d1b4e787">85d4d0e</a>)</li>
<li><strong>xhr:</strong> return original config on errors from XHR
adapter (<a
href="https://redirect.github.com/axios/axios/issues/6515">#6515</a>)
(<a
href="https://github.com/axios/axios/commit/8966ee7ea62ecbd6cfb39a905939bcdab5cf6388">8966ee7</a>)</li>
</ul>
<h3>Contributors to this release</h3>
<ul>
<li><!-- raw HTML omitted --> <a
href="https://github.com/DigitalBrainJS" title="+211/-159
([#6518](axios/axios#6518)
[#6519](axios/axios#6519) )">Dmitriy
Mozgovoy</a></li>
<li><!-- raw HTML omitted --> <a href="https://github.com/ValeraS"
title="+3/-3 ([#6515](axios/axios#6515)
)">Valerii Sidorenko</a></li>
<li><!-- raw HTML omitted --> <a href="https://github.com/prianyu"
title="+2/-2 ([#6505](axios/axios#6505)
)">prianYu</a></li>
</ul>
<h2><a
href="https://github.com/axios/axios/compare/v1.7.1...v1.7.2">1.7.2</a>
(2024-05-21)</h2>
<h3>Bug Fixes</h3>
<ul>
<li><strong>fetch:</strong> enhance fetch API detection; (<a
href="https://redirect.github.com/axios/axios/issues/6413">#6413</a>)
(<a
href="https://github.com/axios/axios/commit/4f79aef81b7c4644328365bfc33acf0a9ef595bc">4f79aef</a>)</li>
</ul>
<h3>Contributors to this release</h3>
<ul>
<li><!-- raw HTML omitted --> <a
href="https://github.com/DigitalBrainJS" title="+3/-3
([#6413](axios/axios#6413) )">Dmitriy
Mozgovoy</a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/axios/axios/commit/abd24a7367726616e60dfc04cb394b4be37cf597"><code>abd24a7</code></a>
chore(release): v1.7.4 (<a
href="https://redirect.github.com/axios/axios/issues/6544">#6544</a>)</li>
<li><a
href="https://github.com/axios/axios/commit/6b6b605eaf73852fb2dae033f1e786155959de3a"><code>6b6b605</code></a>
fix(sec): CVE-2024-39338 (<a
href="https://redirect.github.com/axios/axios/issues/6539">#6539</a>)
(<a
href="https://redirect.github.com/axios/axios/issues/6543">#6543</a>)</li>
<li><a
href="https://github.com/axios/axios/commit/07a661a2a6b9092c4aa640dcc7f724ec5e65bdda"><code>07a661a</code></a>
fix(sec): disregard protocol-relative URL to remediate SSRF (<a
href="https://redirect.github.com/axios/axios/issues/6539">#6539</a>)</li>
<li><a
href="https://github.com/axios/axios/commit/c6cce43cd94489f655f4488c5a50ecaf781c94f2"><code>c6cce43</code></a>
chore(release): v1.7.3 (<a
href="https://redirect.github.com/axios/axios/issues/6521">#6521</a>)</li>
<li><a
href="https://github.com/axios/axios/commit/e3c76fc9bdd03aa4d98afaf211df943e2031453f"><code>e3c76fc</code></a>
fix(adapter): fix progress event emitting; (<a
href="https://redirect.github.com/axios/axios/issues/6518">#6518</a>)</li>
<li><a
href="https://github.com/axios/axios/commit/85d4d0ea0aae91082f04e303dec46510d1b4e787"><code>85d4d0e</code></a>
fix(fetch): fix withCredentials request config (<a
href="https://redirect.github.com/axios/axios/issues/6505">#6505</a>)</li>
<li><a
href="https://github.com/axios/axios/commit/92cd8ed94362f929d3d0ed85ca84296c0ac8fd6d"><code>92cd8ed</code></a>
chore(github): update ISSUE_TEMPLATE.md (<a
href="https://redirect.github.com/axios/axios/issues/6519">#6519</a>)</li>
<li><a
href="https://github.com/axios/axios/commit/8966ee7ea62ecbd6cfb39a905939bcdab5cf6388"><code>8966ee7</code></a>
fix(xhr): return original config on errors from XHR adapter (<a
href="https://redirect.github.com/axios/axios/issues/6515">#6515</a>)</li>
<li><a
href="https://github.com/axios/axios/commit/0e4f9fa29077ebee4499facea6be1492b42e8a26"><code>0e4f9fa</code></a>
chore(release): v1.7.2 (<a
href="https://redirect.github.com/axios/axios/issues/6414">#6414</a>)</li>
<li><a
href="https://github.com/axios/axios/commit/4f79aef81b7c4644328365bfc33acf0a9ef595bc"><code>4f79aef</code></a>
fix(fetch): enhance fetch API detection; (<a
href="https://redirect.github.com/axios/axios/issues/6413">#6413</a>)</li>
<li>See full diff in <a
href="https://github.com/axios/axios/compare/v1.7.1...v1.7.4">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=axios&package-manager=npm_and_yarn&previous-version=1.7.1&new-version=1.7.4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

You can trigger a rebase of this PR by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/facebook/react/network/alerts).

</details>

> **Note**
> Automatic rebases have been disabled on this pull request as it has
been open for over 30 days.

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants