-
Notifications
You must be signed in to change notification settings - Fork 47.3k
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
Warn When The HTML Mismatches in DEV #10026
Conversation
df20df1
to
3ada2c9
Compare
* should error reconnecting a div with children separated by different whitespace on the server | ||
* should error reconnecting a div with children separated by different whitespace | ||
* can distinguish an empty component from a dom node | ||
* renders a textarea with a value and an onChange with client render on top of good server markup |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We currently apparently removing the text node from the textarea even though we really just manage the value
property. Need to fix that by not trying to hydrate textarea content. Might be related to dangerouslySetInnerHTML.
* renders a textarea value overriding defaultValue no matter the prop order with client render on top of good server markup | ||
* renders a controlled textarea with client render on top of good server markup | ||
* throws when rendering if getChildContext exists without childContextTypes with client render on top of bad server markup | ||
* throws when rendering if getChildContext returns a value not in childContextTypes with client render on top of bad server markup |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These might need to be updated. This is issuing a warning because the rendered content of an error boundary is not the same content as the server rendered content.
@aickin This basically would've been a broken mess without your tests. So super helpful to find edge cases. |
* renders a self-closing tag as a child with client render on top of bad server markup | ||
* renders simple numbers with client render on top of bad server markup | ||
* renders simple strings with client render on top of bad server markup | ||
* renders string prop with true value with client render on top of bad server markup |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess all of these should test these particular values can issue warnings, but in reality they all just test that the tag name didn't match. Which is why this fixes all of them.
ec6e11d
to
fe9fcca
Compare
1c1ec1c
to
6e97863
Compare
6e97863
to
a62d031
Compare
serverText, | ||
clientText, | ||
); | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sort of beside the point, but I'm curious if this warning gets lifted out with uglify. Has there been any consideration to a warnOnce
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Even if hoisted, this assignment would get stripped, so it'd still get DCE.
We have had a few warnOnce solutions but the context of "once" varies a lot so hasn't been worth the abstraction cost. A few:
- Once per React module instance.
- Once per component class.
- Once per component instance.
- Once per subtree.
These can then combine with:
- Once per each type of warning.
- Once for any of multiple types of warnings.
- Etc.
In this case, we should ideally gather all of these warnings up into one actionable "diff view" of all the differences. That's on my todo. To avoid spam, right now I just want to show the first diff among any type of diff.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome, that's great to know. Very cool.
case 'checked': | ||
break; | ||
case 'selected': | ||
break; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could these be collapsed into a single break;
, like:
case 'data-reactroot':
case 'data-reactid':
// etc
break;
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have a lint rule against fall through.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We fall through elsewhere, e.g. https://github.com/facebook/react/blob/master/src/renderers/shared/fiber/ReactFiberBeginWork.js#L864-L869
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This triggers our non-reuse mode. This is covered by ReactMount already but the test doesn't pass yet without also landing facebook#10026.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
This triggers our non-reuse mode. This is covered by ReactMount already but the test doesn't pass yet without also landing facebook#10026.
This triggers our non-reuse mode. This is covered by ReactMount already but the test doesn't pass yet without also landing facebook#10026.
This triggers our non-reuse mode. This is covered by ReactMount already but the test doesn't pass yet without also landing facebook#10026.
This triggers our non-reuse mode. This is covered by ReactMount already but the test doesn't pass yet without also landing facebook#10026.
* Warn if unmounting a non-container * Warn if the 2nd+ child is a "root" element but not first This triggers our non-reuse mode. This is covered by ReactMount already but the test doesn't pass yet without also landing #10026.
Warns if there are unknown extra attributes in the hydrated node. It also tries to compare the existing property or attribute against the expected value. It does this by reading the property and comparing it to the prop. Except it's not that simple because multiple prop values can yield the same output. For this we pass an extra expected value that is a hint as to which one was used. This is a bit weird but I'm not sure the alternatives were much better.
This warns if there is ever an insertion or deletion due to hydration failing to find a match. Currently we can't warn for insertions required into the root because that's how we do all non-hydrating renders atm. Left a todo. This strategy is a bit unfortunate that it leads to so much plumbing code. And we have to add three extra methods to the HostConfig that are only used in DEV and not for anything else. I don't really have a better idea.
Textareas are special cases. The initial mount inserts children as the default value, but we should leave that untouched. This is the same as the special case where we set text content of children so I'll use that mechanism.
3729ca5
to
f9499b9
Compare
In Stack this is presented as HTML which needs to have normalized escaping rules. In Fiber it is currently not presented as HTML but a raw string so we don't escape it.
f9499b9
to
b5dd140
Compare
In Fiber, the second warning isn't issued because it's considered an update not a new initial render and we don't fire the warning for those.
In Fiber we don't expect empty strings to be different from rendering null. In fact, in a follow up I plan on formalizing this by never creating text Fibers for empty strings.
We can't just compare the raw innerHTML value because it will have been normalized. Instead, we'll create another element, set its innerHTML and read it back. Since there can be custom elements registered with this document, we want to avoid any side-effects they might cause. So I do this in a fresh new document. I'm not sure how this would affect controlled components and other stuff that could have changed after runtime. I think for those cases maybe we just need a general way of opting out of the diff.
var names = []; | ||
attributeNames.forEach(function(name) { | ||
names.push(name); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting, I wouldn't have guessed that forEach
was the fastest way to do this. TIL. https://jsperf.com/set-iterator-vs-foreach/4
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just ran the benchmarks from that link locally in node 8.1.3
and the iterator wins there by a tiny bit. I feel like microbenchmarks cannot be trusted in js. Perhaps @bmeurer can chime in?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately both are pretty slow in V8 at the moment. The fact that Set.prototype.forEach
is faster in some cases isn't intended, and I wouldn't rely on that. We plan to improve the performance of the collection iterators in the future BTW.
My suggestion here: Don't base decisions on current perf results. Use whatever makes sense for your code and try to improve readability instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't a perf hot path at all. It only exist in development mode and only fires if something already went wrong.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool
* Warn for text content * Warn for different attributes/properties values Warns if there are unknown extra attributes in the hydrated node. It also tries to compare the existing property or attribute against the expected value. It does this by reading the property and comparing it to the prop. Except it's not that simple because multiple prop values can yield the same output. For this we pass an extra expected value that is a hint as to which one was used. This is a bit weird but I'm not sure the alternatives were much better. * Warn when there is an insertion or deletion during hydration This warns if there is ever an insertion or deletion due to hydration failing to find a match. Currently we can't warn for insertions required into the root because that's how we do all non-hydrating renders atm. Left a todo. This strategy is a bit unfortunate that it leads to so much plumbing code. And we have to add three extra methods to the HostConfig that are only used in DEV and not for anything else. I don't really have a better idea. * Don't try to delete children of a textarea Textareas are special cases. The initial mount inserts children as the default value, but we should leave that untouched. This is the same as the special case where we set text content of children so I'll use that mechanism. * Change expected format for text differences In Stack this is presented as HTML which needs to have normalized escaping rules. In Fiber it is currently not presented as HTML but a raw string so we don't escape it. * Unmount component in between tests In Fiber, the second warning isn't issued because it's considered an update not a new initial render and we don't fire the warning for those. * Change expectation of white space text behavior in Fiber In Fiber we don't expect empty strings to be different from rendering null. In fact, in a follow up I plan on formalizing this by never creating text Fibers for empty strings. * Warn for different dangerouslySetInnerHTML We can't just compare the raw innerHTML value because it will have been normalized. Instead, we'll create another element, set its innerHTML and read it back. Since there can be custom elements registered with this document, we want to avoid any side-effects they might cause. So I do this in a fresh new document. I'm not sure how this would affect controlled components and other stuff that could have changed after runtime. I think for those cases maybe we just need a general way of opting out of the diff. Warn for different dangerouslySetInnerHTML We can't just compare the raw innerHTML value because it will have been normalized. Instead, we'll create another element, set its innerHTML and read it back. Since there can be custom elements registered with this document, we want to avoid any side-effects they might cause. So I do this in a fresh new document. I'm not sure how this would affect controlled components and other stuff that could have changed after runtime. I think for those cases maybe we just need a general way of opting out of the diff.
In the new hydration strategy we do our best to recover when the server response isn't valid but we don't check the attributes. However, we should still warn in DEV because that can end up pretty bad.
This starts adding warnings for those cases. I still have several steps left but this can ideally be landed at any midway point if someone wants to review it.
Left this for a follow up PR.