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

reconciliation behaves differently with children-as-function and Consumer function #12715

Closed
cristianbogdan opened this issue Apr 28, 2018 · 2 comments

Comments

@cristianbogdan
Copy link

cristianbogdan commented Apr 28, 2018

https://jsfiddle.net/d4egov72/

In React Developer Tools, with Highlight updates:

Image debugger

What is the expected behavior?

The two divs should reconcile the same way, but the children-as-function variant does not update the DOM on setState() (which would be the ideal behavior since its return value does not change), while the consumer variant changes the DOM on every setState()

Note that the difference does not occur if:

  • there is no empty first child ("") of Provider. In that case, both children-as-function and Consumer variants refresh on setState()
  • the function returns a single text node. In that case none of the two versions refreshes on setState() unless the text changes
@cristianbogdan cristianbogdan changed the title reconciliation behaves differently with children function and Consumer function reconciliation behaves differently with children-as-function and Consumer function Apr 28, 2018
@cristianbogdan
Copy link
Author

cristianbogdan commented Apr 29, 2018

I also wonder what should developers expect from function-as-child

  • Will it always be re-rendered in the DOM (like functional components)?
  • Will it only be re-rendered depdending on the content it returns?

@gaearon
Copy link
Collaborator

gaearon commented May 1, 2018

This turned out to be more complicated than I thought.

If we add some console logs we’ll indeed see that only the first span-creating function gets re-executed when the interval fires. This is why.

In the first example, Provider is rendered from a component that calls setState. It starts a regular React re-render. (Provider won't by itself do anything special to “block” rendering of its children.) Your component calls a function to create the children. Therefore, on every re-render, the render prop <span> will be a new element that needs to be reconciled. There is no special logic in React for handling your render props—it was your component’s code that called this.props.children() from a component that was re-rendering. React wouldn’t do anything to prevent you from doing so. This counts as an update.

In the second example, Provider is also rendered from a component that calls setState, and also starts a normal re-render. However, it returns this.props.children. In your example, those children are predefined—they’re always the same <span> referentially. Even though that <span> has something inside it, React knows not to “go deeper” because prevProps.children === nextProps.children. The only reason it would go deeper (or, rather, “skip through” some components) is if the context provider’s value prop changed. But this hasn’t happened. So this explains why in the second example, the render prop doesn’t get called.


Note that this is actually getting a bit more complicated than that in practice. In practice, React DevTools does highlight both of them if you remove the first empty text child:

there is no empty first child ("") of Provider. In that case, both children-as-function and Consumer variants refresh on setState()

This doesn’t mean React re-renders them both though. (You can verify by putting console logs.) What's happening here is that the top-level component that contains Provider has updated in both cases. We need to highlight something to indicate that. Currently, our strategy is to highlight the first closest child DOM node (which means it doesn't work well for fragments or other nodes like context providers that may have multiple children).

If we remove the text node, in your example tree, the closest node to the Comp component that re-renders is the span, in both cases.

screen shot 2018-05-01 at 9 17 44 pm

So when we don’t have a text node in the beginning, we end up highlighting the span even in both cases even though it’s only Comp that has re-rendered.

With the text node at the beginning, the tree looks like this:

screen shot 2018-05-01 at 9 20 23 pm

For the first row, again, we want to show that Comp has re-rendered, so we find only the first DOM child—a text node—and since it doesn't have a DOM measurement API, I suppose we don't highlight anything. (This is not ideal, but on the other hand, a component that renders only a text node is most likely not expensive anyway.)

For the second row, we want to show that both Comp and Consumer have re-rendered, and while for Comp we find the empty text node and don't highlight anything, for Consumer we actually find the span which is why you see it highlighted.

Now the question is why do we count Consumer as updated in the second example, if it's supposed to not have re-rendered? My hunch is it’s a false positive caused by this check. I'll file an issue with React DevTools.

To sum up:

Thanks for reporting!

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

2 participants