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

forwardRef() components should not re-render on deep setState() #12690

Merged
merged 3 commits into from
Apr 26, 2018

Conversation

gaearon
Copy link
Collaborator

@gaearon gaearon commented Apr 25, 2018

Fixes #12688. This is not just related to legacy context: as far as I can tell from the test I added, we're currently re-rendering all forwardRef components on a deeper setState. This seems bad.

I changed memoization to store the props instead of the returned child. Not sure if it matters. Maybe for DevTools?

I'm also not sure if what I'm doing here is safe. Can ref provided by React ever be different in this scenario and "justify" re-render?

if (hasLegacyContextChanged()) {
// Normally we can bail out on props equality but if context has changed
// we don't do the bailout and we have to reuse existing props instead.
} else if (workInProgress.memoizedProps === nextProps) {
Copy link
Contributor

@bvaughn bvaughn Apr 25, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should also compare workInProgress.ref

Copy link
Collaborator Author

@gaearon gaearon Apr 25, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With what?

(Sorry if I'm being dense)

Also, is it possible for a deep update to influence the value of a ref above it?

Copy link
Contributor

@bvaughn bvaughn Apr 25, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, is it possible for a deep update to influence the value of a ref above it?

I'm not sure, but it seems conceivable that a ref could be the only changed "prop" (if it's an inline arrow function for a non-PureComponent) and we might bail out when we shouldn't here?

Although in that case it probably wouldn't matter anyway.

Copy link
Contributor

@bvaughn bvaughn Apr 25, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm... maybe Foo gets new properties that causes it to recreate the ref, but only a filtered set of those props are passed through to the ForwardRef component, so from it's POV nothing has changed?

Maybe this is too contrived.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe Foo gets new properties that causes it to recreate the ref, but only a filtered set of those props are passed through to the ForwardRef component, so from it's POV nothing has changed?

OK, that sounds potentially plausible. If you find any free time to hack on this I'd appreciate a test.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think maybe this is too contrived to be a real concern.

Ref callbacks would be re-invoked if the underlying instance/ref changed. If the ref callback itself changed, most likely this just means it's an arrow functions and has an identical implementation.

createRef would be more problematic, but it is only expected to be created once in the first place, so in practice shouldn't be a problem.

Maybe we should just add an inline comment saying this is technically possible but probably not a concern in practice?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the ref callback itself changed, most likely this just means it's an arrow functions and has an identical implementation.

I don’t think we can rely on something like this. Otherwise we might as well never update refs.

We handled this correctly so far so I don’t think we should regress in this particular case.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to write a contrived test to catch the case we're talking about, and it looks like I was mistaken to begin with. Even if props are shallowly equal, the memoizedProps and pendingProps objects themselves won't be equal- so we won't bail out- which side steps the issue I mentioned.

Sorry for the dead end. 😄

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We chatted a bit with @acdlite and he proposed to check against current.ref. Seems like it wouldn’t hurt but I’ll think about it more tomorrow.

Copy link
Contributor

@bvaughn bvaughn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's interesting how a ref owned by Foo can change without a subsequent did-update lifecycle being called on Foo(but that isn't new from this PR, and was also already the case for refs "forwarded" via props so maybe it's fine).

@pull-bot
Copy link

pull-bot commented Apr 25, 2018

ReactDOM: size: 🔺+0.1%, gzip: 🔺+0.1%

Details of bundled changes.

Comparing: 9c77ffb...60ddc00

react-dom

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-dom.development.js +0.1% 0.0% 611.19 KB 611.66 KB 141.28 KB 141.32 KB UMD_DEV
react-dom.production.min.js 🔺+0.1% 🔺+0.1% 100.3 KB 100.37 KB 31.85 KB 31.87 KB UMD_PROD
react-dom.development.js +0.1% 0.0% 595.56 KB 596.03 KB 137.14 KB 137.18 KB NODE_DEV
react-dom.production.min.js 🔺+0.1% 0.0% 98.74 KB 98.81 KB 31.05 KB 31.07 KB NODE_PROD
ReactDOM-dev.js +0.1% 0.0% 619.99 KB 620.46 KB 139.85 KB 139.88 KB FB_WWW_DEV
ReactDOM-prod.js 🔺+0.1% 0.0% 283.99 KB 284.32 KB 51.93 KB 51.93 KB FB_WWW_PROD

react-art

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-art.development.js +0.1% 0.0% 414.1 KB 414.57 KB 90.15 KB 90.19 KB UMD_DEV
react-art.production.min.js 🔺+0.1% 🔺+0.1% 90.62 KB 90.69 KB 27.51 KB 27.54 KB UMD_PROD
react-art.development.js +0.1% +0.1% 339.94 KB 340.41 KB 71.51 KB 71.55 KB NODE_DEV
react-art.production.min.js 🔺+0.1% 0.0% 55.14 KB 55.22 KB 16.76 KB 16.77 KB NODE_PROD
ReactART-dev.js +0.1% 0.0% 348.2 KB 348.67 KB 71.06 KB 71.09 KB FB_WWW_DEV
ReactART-prod.js 🔺+0.2% 0.0% 166.73 KB 167.06 KB 27.41 KB 27.42 KB FB_WWW_PROD

react-test-renderer

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-test-renderer.development.js +0.1% +0.1% 348.03 KB 348.5 KB 73.21 KB 73.25 KB UMD_DEV
react-test-renderer.production.min.js 🔺+0.1% 🔺+0.3% 55.16 KB 55.23 KB 16.69 KB 16.73 KB UMD_PROD
react-test-renderer.development.js +0.1% +0.1% 338.86 KB 339.33 KB 70.53 KB 70.58 KB NODE_DEV
react-test-renderer.production.min.js 🔺+0.1% -0.2% 54.39 KB 54.46 KB 16.33 KB 16.3 KB NODE_PROD
ReactTestRenderer-dev.js +0.1% 0.0% 347.4 KB 347.86 KB 70.15 KB 70.19 KB FB_WWW_DEV

react-reconciler

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-reconciler.development.js +0.1% +0.1% 319.2 KB 319.67 KB 65.98 KB 66.03 KB NODE_DEV
react-reconciler.production.min.js 🔺+0.2% 🔺+0.1% 47.24 KB 47.32 KB 14.23 KB 14.24 KB NODE_PROD
react-reconciler-persistent.development.js +0.1% +0.1% 318.53 KB 319 KB 65.75 KB 65.79 KB NODE_DEV
react-reconciler-persistent.production.min.js 🔺+0.2% 🔺+0.1% 46.16 KB 46.23 KB 14.11 KB 14.12 KB NODE_PROD

react-native-renderer

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
ReactNativeRenderer-dev.js +0.1% 0.0% 459.01 KB 459.48 KB 98.2 KB 98.23 KB RN_FB_DEV
ReactNativeRenderer-prod.js 🔺+0.2% 0.0% 217.69 KB 218.02 KB 36.39 KB 36.4 KB RN_FB_PROD
ReactNativeRenderer-dev.js +0.1% 0.0% 458.76 KB 459.23 KB 98.14 KB 98.17 KB RN_OSS_DEV
ReactNativeRenderer-prod.js 🔺+0.2% 0.0% 216.91 KB 217.24 KB 36.26 KB 36.27 KB RN_OSS_PROD
ReactFabric-dev.js +0.1% 0.0% 441.43 KB 441.9 KB 93.8 KB 93.83 KB RN_FB_DEV
ReactFabric-prod.js 🔺+0.2% 0.0% 202.5 KB 202.83 KB 33.65 KB 33.66 KB RN_FB_PROD
ReactFabric-dev.js +0.1% 0.0% 441.46 KB 441.93 KB 93.82 KB 93.85 KB RN_OSS_DEV
ReactFabric-prod.js 🔺+0.2% 0.0% 202.54 KB 202.87 KB 33.67 KB 33.67 KB RN_OSS_PROD

Generated by 🚫 dangerJS

if (hasLegacyContextChanged()) {
// Normally we can bail out on props equality but if context has changed
// we don't do the bailout and we have to reuse existing props instead.
} else if (workInProgress.memoizedProps === nextProps) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to write a contrived test to catch the case we're talking about, and it looks like I was mistaken to begin with. Even if props are shallowly equal, the memoizedProps and pendingProps objects themselves won't be equal- so we won't bail out- which side steps the issue I mentioned.

Sorry for the dead end. 😄

Copy link
Collaborator

@acdlite acdlite left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Compare workInProgress.ref to current.ref as discussed offline

@bvaughn
Copy link
Contributor

bvaughn commented Apr 26, 2018

Cool! Agreed it wouldn't hurt, although I can't think of a case where it would currently matter.

@gaearon gaearon merged commit d883d59 into facebook:master Apr 26, 2018
@gaearon gaearon deleted the forward-memoize branch April 26, 2018 18:47
@nonken
Copy link

nonken commented Apr 30, 2018

❤️ awesome fix! When will a new version be released?

@gaearon
Copy link
Collaborator Author

gaearon commented Apr 30, 2018

Hopefully soon

@slorber
Copy link
Contributor

slorber commented May 1, 2018

Great! just encountered this bug where my "middle" component renders for unknown reasons after adding forwardRef

@gaearon
Copy link
Collaborator Author

gaearon commented May 24, 2018

Should be fixed in React 16.4.
https://reactjs.org/blog/2018/05/23/react-v-16-4.html

bors bot referenced this pull request in mythmon/corsica-tree-status May 24, 2018
20: Update react monorepo to v16.4.0 r=renovate[bot] a=renovate[bot]

This Pull Request renovates the package group "react monorepo".


-   [react-dom](https://github.com/facebook/react) (`dependencies`): from `16.3.2` to `16.4.0`
-   [react](https://github.com/facebook/react) (`dependencies`): from `16.3.2` to `16.4.0`

# Release Notes
<details>
<summary>facebook/react</summary>

### [`v16.4.0`](https://github.com/facebook/react/blob/master/CHANGELOG.md#&#8203;1640-May-23-2018)
[Compare Source](facebook/react@8e5f12c...v16.4.0)
##### React

* Add a new [experimental](`https://github.com/reactjs/rfcs/pull/51`) `React.unstable_Profiler` component for measuring performance. ([@&#8203;bvaughn] in [#&#8203;12745](`https://github.com/facebook/react/pull/12745`))
##### React DOM

* Add support for the Pointer Events specification. ([@&#8203;philipp-spiess] in [#&#8203;12507](`https://github.com/facebook/react/pull/12507`))
* Properly call `getDerivedStateFromProps()` regardless of the reason for re-rendering. ([@&#8203;acdlite] in [#&#8203;12600](`https://github.com/facebook/react/pull/12600`) and [#&#8203;12802](`https://github.com/facebook/react/pull/12802`))
* Fix a bug that prevented context propagation in some cases. ([@&#8203;gaearon] in [#&#8203;12708](`https://github.com/facebook/react/pull/12708`))
* Fix re-rendering of components using `forwardRef()` on a deeper `setState()`. ([@&#8203;gaearon] in [#&#8203;12690](`https://github.com/facebook/react/pull/12690`))
* Fix some attributes incorrectly getting removed from custom element nodes. ([@&#8203;airamrguez] in [#&#8203;12702](`https://github.com/facebook/react/pull/12702`))
* Fix context providers to not bail out on children if there's a legacy context provider above. ([@&#8203;gaearon] in [#&#8203;12586](`https://github.com/facebook/react/pull/12586`))
* Add the ability to specify `propTypes` on a context provider component. ([@&#8203;nicolevy] in [#&#8203;12658](`https://github.com/facebook/react/pull/12658`))
* Fix a false positive warning when using `react-lifecycles-compat` in `<StrictMode>`. ([@&#8203;bvaughn] in [#&#8203;12644](`https://github.com/facebook/react/pull/12644`))
* Warn when the `forwardRef()` render function has `propTypes` or `defaultProps`. ([@&#8203;bvaughn] in [#&#8203;12644](`https://github.com/facebook/react/pull/12644`))
* Improve how `forwardRef()` and context consumers are displayed in the component stack. ([@&#8203;sophiebits] in [#&#8203;12777](`https://github.com/facebook/react/pull/12777`))
* Change internal event names. This can break third-party packages that rely on React internals in unsupported ways. ([@&#8203;philipp-spiess] in [#&#8203;12629](`https://github.com/facebook/react/pull/12629`))
##### React Test Renderer

* Fix the `getDerivedStateFromProps()` support to match the new React DOM behavior. ([@&#8203;koba04] in [#&#8203;12676](`https://github.com/facebook/react/pull/12676`))
* Fix a `testInstance.parent` crash when the parent is a fragment or another special node. ([@&#8203;gaearon] in [#&#8203;12813](`https://github.com/facebook/react/pull/12813`))
* `forwardRef()` components are now discoverable by the test renderer traversal methods. ([@&#8203;gaearon] in [#&#8203;12725](`https://github.com/facebook/react/pull/12725`))
* Shallow renderer now ignores `setState()` updaters that return `null` or `undefined`. ([@&#8203;koba04] in [#&#8203;12756](`https://github.com/facebook/react/pull/12756`))
##### React ART

* Fix reading context provided from the tree managed by React DOM. ([@&#8203;acdlite] in [#&#8203;12779](`https://github.com/facebook/react/pull/12779`))
##### React Call Return (Experimental)

* This experiment was deleted because it was affecting the bundle size and the API wasn't good enough. It's likely to come back in the future in some other form. ([@&#8203;gaearon] in [#&#8203;12820](`https://github.com/facebook/react/pull/12820`))
##### React Reconciler (Experimental)

* The [new host config shape](https://github.com/facebook/react/blob/c601f7a64640290af85c9f0e33c78480656b46bc/packages/react-noop-renderer/src/createReactNoop.js#L82-L285) is flat and doesn't use nested objects. ([@&#8203;gaearon] in [#&#8203;12792](`https://github.com/facebook/react/pull/12792`))

---


</details>




---

This PR has been generated by [Renovate Bot](https://renovatebot.com).

Co-authored-by: Renovate Bot <bot@renovateapp.com>
bors bot referenced this pull request in mozilla/delivery-console May 24, 2018
164: Update react monorepo to v16.4.0 r=rehandalal a=renovate[bot]

This Pull Request renovates the package group "react monorepo".


-   [react-dom](https://github.com/facebook/react) (`dependencies`): from `16.3.2` to `16.4.0`
-   [react](https://github.com/facebook/react) (`dependencies`): from `16.3.2` to `16.4.0`

# Release Notes
<details>
<summary>facebook/react</summary>

### [`v16.4.0`](https://github.com/facebook/react/blob/master/CHANGELOG.md#&#8203;1640-May-23-2018)
[Compare Source](facebook/react@8e5f12c...v16.4.0)
##### React

* Add a new [experimental](`https://github.com/reactjs/rfcs/pull/51`) `React.unstable_Profiler` component for measuring performance. ([@&#8203;bvaughn] in [#&#8203;12745](`https://github.com/facebook/react/pull/12745`))
##### React DOM

* Add support for the Pointer Events specification. ([@&#8203;philipp-spiess] in [#&#8203;12507](`https://github.com/facebook/react/pull/12507`))
* Properly call `getDerivedStateFromProps()` regardless of the reason for re-rendering. ([@&#8203;acdlite] in [#&#8203;12600](`https://github.com/facebook/react/pull/12600`) and [#&#8203;12802](`https://github.com/facebook/react/pull/12802`))
* Fix a bug that prevented context propagation in some cases. ([@&#8203;gaearon] in [#&#8203;12708](`https://github.com/facebook/react/pull/12708`))
* Fix re-rendering of components using `forwardRef()` on a deeper `setState()`. ([@&#8203;gaearon] in [#&#8203;12690](`https://github.com/facebook/react/pull/12690`))
* Fix some attributes incorrectly getting removed from custom element nodes. ([@&#8203;airamrguez] in [#&#8203;12702](`https://github.com/facebook/react/pull/12702`))
* Fix context providers to not bail out on children if there's a legacy context provider above. ([@&#8203;gaearon] in [#&#8203;12586](`https://github.com/facebook/react/pull/12586`))
* Add the ability to specify `propTypes` on a context provider component. ([@&#8203;nicolevy] in [#&#8203;12658](`https://github.com/facebook/react/pull/12658`))
* Fix a false positive warning when using `react-lifecycles-compat` in `<StrictMode>`. ([@&#8203;bvaughn] in [#&#8203;12644](`https://github.com/facebook/react/pull/12644`))
* Warn when the `forwardRef()` render function has `propTypes` or `defaultProps`. ([@&#8203;bvaughn] in [#&#8203;12644](`https://github.com/facebook/react/pull/12644`))
* Improve how `forwardRef()` and context consumers are displayed in the component stack. ([@&#8203;sophiebits] in [#&#8203;12777](`https://github.com/facebook/react/pull/12777`))
* Change internal event names. This can break third-party packages that rely on React internals in unsupported ways. ([@&#8203;philipp-spiess] in [#&#8203;12629](`https://github.com/facebook/react/pull/12629`))
##### React Test Renderer

* Fix the `getDerivedStateFromProps()` support to match the new React DOM behavior. ([@&#8203;koba04] in [#&#8203;12676](`https://github.com/facebook/react/pull/12676`))
* Fix a `testInstance.parent` crash when the parent is a fragment or another special node. ([@&#8203;gaearon] in [#&#8203;12813](`https://github.com/facebook/react/pull/12813`))
* `forwardRef()` components are now discoverable by the test renderer traversal methods. ([@&#8203;gaearon] in [#&#8203;12725](`https://github.com/facebook/react/pull/12725`))
* Shallow renderer now ignores `setState()` updaters that return `null` or `undefined`. ([@&#8203;koba04] in [#&#8203;12756](`https://github.com/facebook/react/pull/12756`))
##### React ART

* Fix reading context provided from the tree managed by React DOM. ([@&#8203;acdlite] in [#&#8203;12779](`https://github.com/facebook/react/pull/12779`))
##### React Call Return (Experimental)

* This experiment was deleted because it was affecting the bundle size and the API wasn't good enough. It's likely to come back in the future in some other form. ([@&#8203;gaearon] in [#&#8203;12820](`https://github.com/facebook/react/pull/12820`))
##### React Reconciler (Experimental)

* The [new host config shape](https://github.com/facebook/react/blob/c601f7a64640290af85c9f0e33c78480656b46bc/packages/react-noop-renderer/src/createReactNoop.js#L82-L285) is flat and doesn't use nested objects. ([@&#8203;gaearon] in [#&#8203;12792](`https://github.com/facebook/react/pull/12792`))

---


</details>




---

This PR has been generated by [Renovate Bot](https://renovatebot.com).

Co-authored-by: Renovate Bot <bot@renovateapp.com>
@nonken
Copy link

nonken commented May 24, 2018

Woot woot @gaearon

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants