Skip to content

Commit f93b9fd

Browse files
authored
Skip hydration errors when a view transition has been applied (facebook#35380)
When the Fizz runtime runs a view-transition we apply `view-transition-name` and `view-transition-class` to the `style`. These can be observed by Fiber when hydrating which incorrectly leads to hydration errors. More over, even after we remove them, the `style` attribute has now been normalized which we are unable to diff because we diff against the SSR generated `style` attribute string and not the normalized form. So if there are other inline styles defined, we have to skip diffing them in this scenario.
1 parent b731fe2 commit f93b9fd

File tree

1 file changed

+71
-6
lines changed

1 file changed

+71
-6
lines changed

packages/react-dom-bindings/src/client/ReactDOMComponent.js

Lines changed: 71 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -235,17 +235,60 @@ function warnForPropDifference(
235235
}
236236
}
237237

238+
function hasViewTransition(htmlElement: HTMLElement): boolean {
239+
return !!(
240+
htmlElement.getAttribute('vt-share') ||
241+
htmlElement.getAttribute('vt-exit') ||
242+
htmlElement.getAttribute('vt-enter') ||
243+
htmlElement.getAttribute('vt-update')
244+
);
245+
}
246+
247+
function isExpectedViewTransitionName(htmlElement: HTMLElement): boolean {
248+
if (!hasViewTransition(htmlElement)) {
249+
// We didn't expect to see a view transition name applied.
250+
return false;
251+
}
252+
const expectedVtName = htmlElement.getAttribute('vt-name');
253+
const actualVtName: string = (htmlElement.style: any)['view-transition-name'];
254+
if (expectedVtName) {
255+
return expectedVtName === actualVtName;
256+
} else {
257+
// Auto-generated name.
258+
// TODO: If Fizz starts applying a prefix to this name, we need to consider that.
259+
return actualVtName.startsWith('_T_');
260+
}
261+
}
262+
238263
function warnForExtraAttributes(
239264
domElement: Element,
240265
attributeNames: Set<string>,
241266
serverDifferences: {[propName: string]: mixed},
242267
) {
243268
if (__DEV__) {
244269
attributeNames.forEach(function (attributeName) {
245-
serverDifferences[getPropNameFromAttributeName(attributeName)] =
246-
attributeName === 'style'
247-
? getStylesObjectFromElement(domElement)
248-
: domElement.getAttribute(attributeName);
270+
if (attributeName === 'style') {
271+
if (domElement.getAttribute(attributeName) === '') {
272+
// Skip empty style. It's fine.
273+
return;
274+
}
275+
const htmlElement = ((domElement: any): HTMLElement);
276+
const style = htmlElement.style;
277+
const isOnlyVTStyles =
278+
(style.length === 1 && style[0] === 'view-transition-name') ||
279+
(style.length === 2 &&
280+
style[0] === 'view-transition-class' &&
281+
style[1] === 'view-transition-name');
282+
if (isOnlyVTStyles && isExpectedViewTransitionName(htmlElement)) {
283+
// If the only extra style was the view-transition-name that we applied from the Fizz
284+
// runtime, then we should ignore it.
285+
} else {
286+
serverDifferences.style = getStylesObjectFromElement(domElement);
287+
}
288+
} else {
289+
serverDifferences[getPropNameFromAttributeName(attributeName)] =
290+
domElement.getAttribute(attributeName);
291+
}
249292
});
250293
}
251294
}
@@ -1977,13 +2020,21 @@ function getStylesObjectFromElement(domElement: Element): {
19772020
[styleName: string]: string,
19782021
} {
19792022
const serverValueInObjectForm: {[prop: string]: string} = {};
1980-
const style = ((domElement: any): HTMLElement).style;
2023+
const htmlElement: HTMLElement = (domElement: any);
2024+
const style = htmlElement.style;
19812025
for (let i = 0; i < style.length; i++) {
19822026
const styleName: string = style[i];
19832027
// TODO: We should use the original prop value here if it is equivalent.
19842028
// TODO: We could use the original client capitalization if the equivalent
19852029
// other capitalization exists in the DOM.
1986-
serverValueInObjectForm[styleName] = style.getPropertyValue(styleName);
2030+
if (
2031+
styleName === 'view-transition-name' &&
2032+
isExpectedViewTransitionName(htmlElement)
2033+
) {
2034+
// This is a view transition name added by the Fizz runtime, not the user's props.
2035+
} else {
2036+
serverValueInObjectForm[styleName] = style.getPropertyValue(styleName);
2037+
}
19872038
}
19882039
return serverValueInObjectForm;
19892040
}
@@ -2018,6 +2069,20 @@ function diffHydratedStyles(
20182069
return;
20192070
}
20202071

2072+
if (
2073+
// Trailing semi-colon means this was regenerated.
2074+
normalizedServerValue[normalizedServerValue.length - 1] === ';' &&
2075+
// TODO: Should we just ignore any style if the style as been manipulated?
2076+
hasViewTransition((domElement: any))
2077+
) {
2078+
// If this had a view transition we might have applied a view transition
2079+
// name/class and removed it. If that happens, the style attribute gets
2080+
// regenerated from the style object. This means we've lost the format
2081+
// that we sent from the server and is unable to diff it. We just treat
2082+
// it as passing even if it should be a mismatch in this edge case.
2083+
return;
2084+
}
2085+
20212086
// Otherwise, we create the object from the DOM for the diff view.
20222087
serverDifferences.style = getStylesObjectFromElement(domElement);
20232088
}

0 commit comments

Comments
 (0)