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

Fixed an issue with nested contexts unwinding when server rendering. Issue #12984 #12985

Merged
merged 8 commits into from Jun 11, 2018

Conversation

ericsoderberghp
Copy link
Contributor

@ericsoderberghp ericsoderberghp commented Jun 5, 2018

The key change her is to unwind to the previous provider of the same type as the provider being popped.

Addresses #12984.

// find the correct previous provider based on type
let previousProvider;
if (this.providerIndex > -1) {
for (let i = 0; i <= this.providerIndex; i += 1) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Instead of searching from the bottom, shouldn't we start search at the top and stop at the first match? Try adding more nesting to the test.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Duh. Fixed.

let previousProvider;
if (this.providerIndex > -1) {
for (let i = 0; i <= this.providerIndex; i += 1) {
if (this.providerStack[i] &&
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please be explicit in type checks (e.g. !== null if that's what you mean). Don't rely on falsiness.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

OK. I wasn't sure if there was a more Flow appropriate way to check. Flow wanted explicit checks for both null and undefined.

@@ -692,8 +692,8 @@ class ReactDOMServerRenderer {
// find the correct previous provider based on type
let previousProvider;
if (this.providerIndex > -1) {
for (let i = 0; i <= this.providerIndex; i += 1) {
if (this.providerStack[i] &&
for (let i = this.providerIndex; i >= 0; i -= 1) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

If you don't mind, i-- would feel a bit more intuitive to me

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done. I'm used to not allowing infix from our projects lint rules, old habits.

for (let i = 0; i <= this.providerIndex; i += 1) {
if (this.providerStack[i] &&
for (let i = this.providerIndex; i >= 0; i -= 1) {
if (this.providerStack[i] !== null && this.providerStack[i] !== undefined &&
Copy link
Collaborator

Choose a reason for hiding this comment

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

So, do we actually need those checks? I wouldn't expect a provider "below" to ever be null. We only clear them out as we pop. As long as we check i doesn't get below zero we should be fine.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agreed. But, Flow doesn't seem to be aware of that. Perhaps we need some special Flow syntax I'm not aware of, which is highly likely?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I addressed this by changing the array definition to be stricter and pop via setting the length. It didn't seem right to allow null as an array item but then not test for null when iterating over the array, even though the logic of how the array was being manipulated was OK.

@@ -692,8 +692,8 @@ class ReactDOMServerRenderer {
// find the correct previous provider based on type
let previousProvider;
if (this.providerIndex > -1) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Seems like we can remove this condition? For loop already covers it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

OK.

}
}
}
if (previousProvider) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Let's initialize it to null and then compare to null instead of a truthy check.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good idea.

if (this.providerIndex > -1) {
for (let i = this.providerIndex; i >= 0; i--) {
if (this.providerStack[i] !== null && this.providerStack[i] !== undefined &&
(this.providerStack[i]: ReactProvider<any>).type === provider.type) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

This comment was about why we felt comfortable using Flow any:

// We assume this type is correct because of the index check above.

Please keep it above the any case. Maybe change to "Flow type" to disambiguate.

// We assume this Flow type is correct because of the index check above.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

OK.

@gaearon
Copy link
Collaborator

gaearon commented Jun 5, 2018

GH collapsed some older comments but they're still relevant. Thanks!

@aweary
Copy link
Contributor

aweary commented Jun 5, 2018

AFAICT The client implementation uses a stack of cursors for popping/pushing context providers without iteration. Could that same approach be reused here?

@gaearon
Copy link
Collaborator

gaearon commented Jun 6, 2018

AFAICT The client implementation uses a stack of cursors for popping/pushing context providers without iteration. Could that same approach be reused here?

We have many kinds of contexts there (even for a single provider) but here we have just one so I thought normally you probably wouldn't need to go too high up.

So we could consider this but I'd prefer to separate it from the bugfix.

@@ -686,17 +686,22 @@ class ReactDOMServerRenderer {
'Unexpected pop.',
);
}
this.providerStack[this.providerIndex] = null;
Copy link
Collaborator

Choose a reason for hiding this comment

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

What does .length assignment do in V8? I'd like to make sure it doesn't think the array length change is likely to "stay" because it changes all the time. I think just using null is more straightforward and less surprising to the engine (even though the typing isn't as nice for us).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

OK. But, going back to using null seems to require testing for null when accessing array items by index, since Flow has been told that null and undefined are expected array item values. Or, is there an alternate Flow syntax that allows us to remove the explicit checks?

let previousProvider = null;
for (let i = this.providerIndex; i >= 0; i--) {
// We assume this Flow type is correct because of the index check above.
if ((this.providerStack[i]: ReactProvider<any>).type === provider.type) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please read this.providerStack[i] once. (Move it to a constant.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep.

const previousProvider: ReactProvider<any> = (this.providerStack[
this.providerIndex
]: any);
// find the correct previous provider based on type
Copy link
Collaborator

Choose a reason for hiding this comment

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

This comment is redundant, IMO the code speaks for itself

Copy link
Contributor Author

Choose a reason for hiding this comment

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

OK

@aweary
Copy link
Contributor

aweary commented Jun 6, 2018

We have many kinds of contexts there (even for a single provider) but here we have just one so I thought normally you probably wouldn't need to go too high up.

What do you mean by just having one here? You can use any number of context instances right? For example:

const Foo = React.createContext("foo");
const Bar = React.createContext("bar");

const App = () => (
  <Foo.Provider value="new foo">
    <Bar.Provider>
      <Bar.Provider>
        {/* Imagine this is nested within a bunch of Bar.Providers */}
        <Foo.Provider>{children}</Foo.Provider>
      </Bar.Provider>
    </Bar.Provider>
  </Foo.Provider>
);

When the deeply nested Foo.Provider gets popped the provider stack would have a bunch of Bar providers that it had to iterate through to get to the next Foo provider. That would mean that the runtime of popping a provider is bound by the number of other providers up the tree. Or am I misunderstanding how the provider stack works?

@gaearon
Copy link
Collaborator

gaearon commented Jun 6, 2018

I mean one per provider (in Fiber every provider has three context stack values associated with it). And we also keep track of host context and a bunch of other things in that client stack. So there is good justification for over-engineering that client stack.

I agree with what you're saying in general. If it's avoidable let's avoid it.

@pull-bot
Copy link

pull-bot commented Jun 6, 2018

Details of bundled changes.

Comparing: c5a733e...5114a3e

react-dom

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-dom-server.browser.development.js +0.3% +0.4% 101.54 KB 101.88 KB 26.48 KB 26.58 KB UMD_DEV
react-dom-server.browser.production.min.js 🔺+0.4% 🔺+0.6% 15.06 KB 15.11 KB 5.76 KB 5.79 KB UMD_PROD
react-dom-server.browser.development.js +0.4% +0.4% 90.84 KB 91.18 KB 24.23 KB 24.32 KB NODE_DEV
react-dom-server.browser.production.min.js 🔺+0.4% 🔺+0.4% 14.42 KB 14.48 KB 5.51 KB 5.54 KB NODE_PROD
react-dom-server.node.development.js +0.4% +0.4% 92.77 KB 93.1 KB 24.77 KB 24.87 KB NODE_DEV
react-dom-server.node.production.min.js 🔺+0.4% 🔺+0.5% 15.23 KB 15.28 KB 5.81 KB 5.83 KB NODE_PROD
ReactDOMServer-dev.js +0.4% +0.4% 94.62 KB 94.96 KB 24.1 KB 24.19 KB FB_WWW_DEV
ReactDOMServer-prod.js 🔺+0.9% 🔺+0.8% 31.39 KB 31.68 KB 7.71 KB 7.77 KB FB_WWW_PROD

Generated by 🚫 dangerJS

priorProvider.type === provider.type) {
if (
priorProvider !== null &&
priorProvider !== undefined &&
Copy link
Collaborator

Choose a reason for hiding this comment

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

These checks aren’t truly necessary, are they? We know they can’t be empty because we’re moving to the left and check the index.

If type system disagrees I think we should do an unsafe cast with a comment.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Happy to. I wasn't sure how strict you wanted to be with the type checking.

@SamyPesse
Copy link

@ericsoderberghp @gaearon Is there any changes left to make on this PR ? Is there any way I can help ?

It's impacting us at GitBook, I've never contributed to the react core, but I'd be happy to start :)

@gaearon
Copy link
Collaborator

gaearon commented Jun 11, 2018

@SamyPesse

Hi! One thing that concerns me about this solution is that it's O(n) where n is the number of providers on the stack. It would be good to avoid iterating over them as we pop them.

I'll get this fix in but I'll file a followup issue to try to remove the iteration. Do you want to try to help with this? I'm a bit hesitant to cut a release without a fix for this.

@aweary
Copy link
Contributor

aweary commented Jun 11, 2018

@gaearon that does make it a lot clearer, thank you! So the key to making this work is pushing the previous Provider into the stack when a Provider is pushed.

                       A    B    stack

                       A0    B0   []
push A1                A1    B0   [A0]
push   B1              A1    B1   [A0, B0]
push     B2            A1    B2   [A0, B0, B1]
push       B3          A1    B3   [A0, B0, B1, B2]
push         A2        A2    B3   [A0, B0, B1, B2, A1]
pop          A2        A1    B1   [A0, B0, B1, B2]
...

@gaearon
Copy link
Collaborator

gaearon commented Jun 11, 2018

Yep.

                      A    B    values               providers

init                 A0    B0   []                   []
push A1              A1    B0   [A0]                 [A]
push   B1            A1    B1   [A0, B0]             [A, B]
push     A2          A2    B1   [A0, B0, A1]         [A, B, A]
push       B2        A2    B2   [A0, B0, A1, B1]     [A, B, A, B]
pop        B2        A2    B1   [A0, B0, A1]         [A, B, A]
pop      A2          A1    B1   [A0, B0]             [A, B]
pop    B1            A1    B0   [A0]                 [A]
pop  A1              A0    B0   []                   []

push(next_provider, next_value):
  index++
  values[index] = next_provider.current_value
  providers[index] = next_provider
  next_provider.current_value = next_value

pop():
  prev_value = values[index]
  prev_provider = providers[index]
  values[index] = null
  providers[index] = null
  index--
  prev_provider.current_value = prev_value

@gaearon
Copy link
Collaborator

gaearon commented Jun 11, 2018

omg did I just accidentally write some Python

@ericsoderberghp
Copy link
Contributor Author

I think what it means is that at the time A2 is encountered, you need a reference to A1 to be able to push it.

@gaearon
Copy link
Collaborator

gaearon commented Jun 11, 2018

Yep, so we keep two stacks. One for "previous values to restore to" and one for "providers who need restoration".

@ericsoderberghp
Copy link
Contributor Author

OK. I'm happy to work on this if you'd like the help.

@gaearon
Copy link
Collaborator

gaearon commented Jun 11, 2018

I have it running so I'll send a PR shortly. Would appreciate a review.

@gaearon
Copy link
Collaborator

gaearon commented Jun 11, 2018

#13019

bors bot added a commit to mozilla/delivery-console that referenced this pull request Jun 13, 2018
193: Update dependency react to v16.4.1 r=magopian a=renovate[bot]

This Pull Request updates dependency [react](https://github.com/facebook/react) from `v16.4.0` to `v16.4.1`



<details>
<summary>Release Notes</summary>

### [`v16.4.1`](https://github.com/facebook/react/blob/master/CHANGELOG.md#&#8203;1641-June-13-2018)
[Compare Source](facebook/react@v16.4.0...v16.4.1)
##### React

* You can now assign `propTypes` to components returned by `React.ForwardRef`. ([@&#8203;bvaughn] in [#&#8203;12911](`facebook/react#12911))
##### React DOM

* Fix a crash when the input `type` changes from some other types to `text`. ([@&#8203;spirosikmd] in [#&#8203;12135](`facebook/react#12135))
* Fix a crash in IE11 when restoring focus to an SVG element. ([@&#8203;ThaddeusJiang] in [#&#8203;12996](`facebook/react#12996))
* Fix a range input not updating in some cases. ([@&#8203;Illu] in [#&#8203;12939](`facebook/react#12939))
* Fix input validation triggering unnecessarily in Firefox. ([@&#8203;nhunzaker] in [#&#8203;12925](`facebook/react#12925))
* Fix an incorrect `event.target` value for the `onChange` event in IE9. ([@&#8203;nhunzaker] in [#&#8203;12976](`facebook/react#12976))
* Fix a false positive error when returning an empty `<React.Fragment />` from a component. ([@&#8203;philipp-spiess] in [#&#8203;12966](`facebook/react#12966))
##### React DOM Server

* Fix an incorrect value being provided by new context API. ([@&#8203;ericsoderberghp] in [#&#8203;12985](`facebook/react#12985), [@&#8203;gaearon] in [#&#8203;13019](`facebook/react#13019))
##### React Test Renderer

* Allow multiple root children in test renderer traversal API. ([@&#8203;gaearon] in [#&#8203;13017](`facebook/react#13017))
* Fix `getDerivedStateFromProps()` in the shallow renderer to not discard the pending state. ([@&#8203;fatfisz] in [#&#8203;13030](`facebook/react#13030))

---

</details>




---

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

Co-authored-by: Renovate Bot <bot@renovateapp.com>
bors bot added a commit to mythmon/corsica-tree-status that referenced this pull request Jun 14, 2018
25: Update react monorepo to v16.4.1 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.4.0` to `16.4.1`
-   [react](https://github.com/facebook/react) (`dependencies`): from `16.4.0` to `16.4.1`

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

### [`v16.4.1`](https://github.com/facebook/react/blob/master/CHANGELOG.md#&#8203;1641-June-13-2018)
[Compare Source](facebook/react@v16.4.0...v16.4.1)
##### React

* You can now assign `propTypes` to components returned by `React.ForwardRef`. ([@&#8203;bvaughn] in [#&#8203;12911](`facebook/react#12911))
##### React DOM

* Fix a crash when the input `type` changes from some other types to `text`. ([@&#8203;spirosikmd] in [#&#8203;12135](`facebook/react#12135))
* Fix a crash in IE11 when restoring focus to an SVG element. ([@&#8203;ThaddeusJiang] in [#&#8203;12996](`facebook/react#12996))
* Fix a range input not updating in some cases. ([@&#8203;Illu] in [#&#8203;12939](`facebook/react#12939))
* Fix input validation triggering unnecessarily in Firefox. ([@&#8203;nhunzaker] in [#&#8203;12925](`facebook/react#12925))
* Fix an incorrect `event.target` value for the `onChange` event in IE9. ([@&#8203;nhunzaker] in [#&#8203;12976](`facebook/react#12976))
* Fix a false positive error when returning an empty `<React.Fragment />` from a component. ([@&#8203;philipp-spiess] in [#&#8203;12966](`facebook/react#12966))
##### React DOM Server

* Fix an incorrect value being provided by new context API. ([@&#8203;ericsoderberghp] in [#&#8203;12985](`facebook/react#12985), [@&#8203;gaearon] in [#&#8203;13019](`facebook/react#13019))
##### React Test Renderer

* Allow multiple root children in test renderer traversal API. ([@&#8203;gaearon] in [#&#8203;13017](`facebook/react#13017))
* Fix `getDerivedStateFromProps()` in the shallow renderer to not discard the pending state. ([@&#8203;fatfisz] in [#&#8203;13030](`facebook/react#13030))

---


</details>




---

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

Co-authored-by: Renovate Bot <bot@renovateapp.com>
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.

None yet

6 participants