@samitouri / QOS-React / commits / 262b9c72f5

Don't hydrate any properties other than event listeners and text content (#9858)

* Don't hydrate any properties other than event listeners and text content This strategy assumes that the rendered HTML is correct if the tree lines up. Therefore we don't diff any attributes of the rendered HTML. However, as a precaution I ensure that textContent *is* updated. This ensures that if something goes wrong with keys lining up etc. at least there is some feedback that the event handlers might not line up. With what you expect. This might not be what you want e.g. for date formatting where it can different between server and client. It is expected that content will line up. To ensure that I will in a follow up ensure that the warning is issued if it doesn't line up so that in development this can be addressed. The text content updates are now moved to the commit phase so if the tree is asynchronously hydrated it doesn't start partially swapping out. I use the regular update side-effect with payload if the content doesn't match up. Since we no longer guarantee that attributes is correct I changed the bad mark up SSR integration tests to only assert on the textContent instead. * Hydrate text node if possible Currently we're never matching text nodes so we need to properly branch.

Sebastian Markbåge committed Jun 6, 2017 at 15:19 UTC 262b9c72f5f40a2d2ac8e2252ccbf296ee05e2e9
15 files changed +278 -76
scripts/fiber/tests-failing.txt
+1 -5
@@ -1,5 +1 @@
1 -src/renderers/dom/shared/__tests__/ReactDOMServerIntegration-test.js
2 -* should not blow away user-entered text on successful reconnect to an uncontrolled checkbox
3 -* should not blow away user-entered text on successful reconnect to a controlled checkbox
4 -* should not blow away user-selected value on successful reconnect to an uncontrolled select
5 -* should not blow away user-selected value on successful reconnect to an controlled select
1 +
scripts/fiber/tests-passing.txt
+4
@@ -1226,6 +1226,10 @@ src/renderers/dom/shared/__tests__/ReactDOMServerIntegration-test.js
1226 * renders a controlled select with client render on top of good server markup
1227 * should not blow away user-entered text on successful reconnect to an uncontrolled input
1228 * should not blow away user-entered text on successful reconnect to a controlled input
1229 +* should not blow away user-entered text on successful reconnect to an uncontrolled checkbox
1230 +* should not blow away user-entered text on successful reconnect to a controlled checkbox
1231 +* should not blow away user-selected value on successful reconnect to an uncontrolled select
1232 +* should not blow away user-selected value on successful reconnect to an controlled select
1233 * renders class child with context with server string render
1234 * renders class child with context with clean client render
1235 * renders class child with context with client render on top of good server markup
src/renderers/dom/fiber/ReactDOMFiber.js
+6 -3
@@ -48,6 +48,7 @@ var {
48 setInitialProperties,
49 diffProperties,
50 updateProperties,
51 + diffHydratedProperties,
52 } = ReactDOMFiberComponent;
53 var {precacheFiberNode, updateFiberProps} = ReactDOMComponentTree;
54
@@ -409,19 +410,21 @@ var DOMRenderer = ReactFiberReconciler({
410 props: Props,
411 rootContainerInstance: Container,
412 internalInstanceHandle: Object,
412 - ): void {
413 + ): null | Array<mixed> {
414 precacheFiberNode(internalInstanceHandle, instance);
415 // TODO: Possibly defer this until the commit phase where all the events
416 // get attached.
417 updateFiberProps(instance, props);
417 - setInitialProperties(instance, type, props, rootContainerInstance);
418 + return diffHydratedProperties(instance, type, props, rootContainerInstance);
419 },
420
421 hydrateTextInstance(
422 textInstance: TextInstance,
423 + text: string,
424 internalInstanceHandle: Object,
423 - ): void {
425 + ): boolean {
426 precacheFiberNode(internalInstanceHandle, textInstance);
427 + return textInstance.nodeValue !== text;
428 },
429
430 scheduleAnimationCallback: ReactDOMFrameScheduling.rAF,
src/renderers/dom/fiber/ReactDOMFiberComponent.js
+133 -5
@@ -177,10 +177,10 @@ function setInitialDOMProperties(
177 isCustomComponentTag: boolean,
178 ): void {
179 for (var propKey in nextProps) {
180 - var nextProp = nextProps[propKey];
180 if (!nextProps.hasOwnProperty(propKey)) {
181 continue;
182 }
183 + var nextProp = nextProps[propKey];
184 if (propKey === STYLE) {
185 if (__DEV__) {
186 if (nextProp) {
@@ -406,7 +406,7 @@ var ReactDOMFiberComponent = {
406 props = rawProps;
407 break;
408 case 'input':
409 - ReactDOMFiberInput.mountWrapper(domElement, rawProps);
409 + ReactDOMFiberInput.initWrapperState(domElement, rawProps);
410 props = ReactDOMFiberInput.getHostProps(domElement, rawProps);
411 trapBubbledEventsLocal(domElement, tag);
412 // For controlled components we always need to ensure we're listening
@@ -414,11 +414,11 @@ var ReactDOMFiberComponent = {
414 ensureListeningTo(rootContainerElement, 'onChange');
415 break;
416 case 'option':
417 - ReactDOMFiberOption.mountWrapper(domElement, rawProps);
417 + ReactDOMFiberOption.validateProps(domElement, rawProps);
418 props = ReactDOMFiberOption.getHostProps(domElement, rawProps);
419 break;
420 case 'select':
421 - ReactDOMFiberSelect.mountWrapper(domElement, rawProps);
421 + ReactDOMFiberSelect.initWrapperState(domElement, rawProps);
422 props = ReactDOMFiberSelect.getHostProps(domElement, rawProps);
423 trapBubbledEventsLocal(domElement, tag);
424 // For controlled components we always need to ensure we're listening
@@ -426,7 +426,7 @@ var ReactDOMFiberComponent = {
426 ensureListeningTo(rootContainerElement, 'onChange');
427 break;
428 case 'textarea':
429 - ReactDOMFiberTextarea.mountWrapper(domElement, rawProps);
429 + ReactDOMFiberTextarea.initWrapperState(domElement, rawProps);
430 props = ReactDOMFiberTextarea.getHostProps(domElement, rawProps);
431 trapBubbledEventsLocal(domElement, tag);
432 // For controlled components we always need to ensure we're listening
@@ -462,6 +462,9 @@ var ReactDOMFiberComponent = {
462 case 'option':
463 ReactDOMFiberOption.postMountWrapper(domElement, rawProps);
464 break;
465 + case 'select':
466 + ReactDOMFiberSelect.postMountWrapper(domElement, rawProps);
467 + break;
468 default:
469 if (typeof props.onClick === 'function') {
470 // TODO: This cast may not be sound for SVG, MathML or custom elements.
@@ -704,6 +707,131 @@ var ReactDOMFiberComponent = {
707 }
708 },
709
710 + diffHydratedProperties(
711 + domElement: Element,
712 + tag: string,
713 + rawProps: Object,
714 + rootContainerElement: Element | Document,
715 + ): null | Array<mixed> {
716 + if (__DEV__) {
717 + var isCustomComponentTag = isCustomComponent(tag, rawProps);
718 + validatePropertiesInDevelopment(tag, rawProps);
719 + if (isCustomComponentTag && !didWarnShadyDOM && domElement.shadyRoot) {
720 + warning(
721 + false,
722 + '%s is using shady DOM. Using shady DOM with React can ' +
723 + 'cause things to break subtly.',
724 + getCurrentFiberOwnerName() || 'A component',
725 + );
726 + didWarnShadyDOM = true;
727 + }
728 + }
729 +
730 + switch (tag) {
731 + case 'audio':
732 + case 'form':
733 + case 'iframe':
734 + case 'img':
735 + case 'image':
736 + case 'link':
737 + case 'object':
738 + case 'source':
739 + case 'video':
740 + case 'details':
741 + trapBubbledEventsLocal(domElement, tag);
742 + break;
743 + case 'input':
744 + ReactDOMFiberInput.initWrapperState(domElement, rawProps);
745 + trapBubbledEventsLocal(domElement, tag);
746 + // For controlled components we always need to ensure we're listening
747 + // to onChange. Even if there is no listener.
748 + ensureListeningTo(rootContainerElement, 'onChange');
749 + break;
750 + case 'option':
751 + ReactDOMFiberOption.validateProps(domElement, rawProps);
752 + break;
753 + case 'select':
754 + ReactDOMFiberSelect.initWrapperState(domElement, rawProps);
755 + trapBubbledEventsLocal(domElement, tag);
756 + // For controlled components we always need to ensure we're listening
757 + // to onChange. Even if there is no listener.
758 + ensureListeningTo(rootContainerElement, 'onChange');
759 + break;
760 + case 'textarea':
761 + ReactDOMFiberTextarea.initWrapperState(domElement, rawProps);
762 + trapBubbledEventsLocal(domElement, tag);
763 + // For controlled components we always need to ensure we're listening
764 + // to onChange. Even if there is no listener.
765 + ensureListeningTo(rootContainerElement, 'onChange');
766 + break;
767 + }
768 +
769 + assertValidProps(tag, rawProps, getCurrentFiberOwnerName);
770 +
771 + var updatePayload = null;
772 + for (var propKey in rawProps) {
773 + if (!rawProps.hasOwnProperty(propKey)) {
774 + continue;
775 + }
776 + var nextProp = rawProps[propKey];
777 + if (propKey === CHILDREN) {
778 + // For text content children we compare against textContent. This
779 + // might match additional HTML that is hidden when we read it using
780 + // textContent. E.g. "foo" will match "f<span>oo</span>" but that still
781 + // satisfies our requirement. Our requirement is not to produce perfect
782 + // HTML and attributes. Ideally we should preserve structure but it's
783 + // ok not to if the visible content is still enough to indicate what
784 + // even listeners these nodes might be wired up to.
785 + // TODO: Warn if there is more than a single textNode as a child.
786 + // TODO: Should we use domElement.firstChild.nodeValue to compare?
787 + if (typeof nextProp === 'string') {
788 + if (domElement.textContent !== nextProp) {
789 + updatePayload = [CHILDREN, nextProp];
790 + }
791 + } else if (typeof nextProp === 'number') {
792 + if (domElement.textContent !== '' + nextProp) {
793 + updatePayload = [CHILDREN, '' + nextProp];
794 + }
795 + }
796 + } else if (registrationNameModules.hasOwnProperty(propKey)) {
797 + if (nextProp) {
798 + ensureListeningTo(rootContainerElement, propKey);
799 + }
800 + }
801 + }
802 +
803 + switch (tag) {
804 + case 'input':
805 + // TODO: Make sure we check if this is still unmounted or do any clean
806 + // up necessary since we never stop tracking anymore.
807 + inputValueTracking.trackNode((domElement: any));
808 + ReactDOMFiberInput.postMountWrapper(domElement, rawProps);
809 + break;
810 + case 'textarea':
811 + // TODO: Make sure we check if this is still unmounted or do any clean
812 + // up necessary since we never stop tracking anymore.
813 + inputValueTracking.trackNode((domElement: any));
814 + ReactDOMFiberTextarea.postMountWrapper(domElement, rawProps);
815 + break;
816 + case 'select':
817 + case 'option':
818 + // For input and textarea we current always set the value property at
819 + // post mount to force it to diverge from attributes. However, for
820 + // option and select we don't quite do the same thing and select
821 + // is not resilient to the DOM state changing so we don't do that here.
822 + // TODO: Consider not doing this for input and textarea.
823 + break;
824 + default:
825 + if (typeof rawProps.onClick === 'function') {
826 + // TODO: This cast may not be sound for SVG, MathML or custom elements.
827 + trapClickOnNonInteractiveElement(((domElement: any): HTMLElement));
828 + }
829 + break;
830 + }
831 +
832 + return updatePayload;
833 + },
834 +
835 restoreControlledState(
836 domElement: Element,
837 tag: string,
src/renderers/dom/fiber/wrappers/ReactDOMFiberInput.js
+1 -1
@@ -89,7 +89,7 @@ var ReactDOMInput = {
89 return hostProps;
90 },
91
92 - mountWrapper: function(element: Element, props: Object) {
92 + initWrapperState: function(element: Element, props: Object) {
93 if (__DEV__) {
94 ReactControlledValuePropTypes.checkPropTypes(
95 'input',
src/renderers/dom/fiber/wrappers/ReactDOMFiberOption.js
+1 -1
@@ -39,7 +39,7 @@ function flattenChildren(children) {
39 * Implements an <option> host component that warns when `selected` is set.
40 */
41 var ReactDOMOption = {
42 - mountWrapper: function(element: Element, props: Object) {
42 + validateProps: function(element: Element, props: Object) {
43 // TODO (yungsters): Remove support for `selected` in <option>.
44 if (__DEV__) {
45 warning(
src/renderers/dom/fiber/wrappers/ReactDOMFiberSelect.js
+5 -1
@@ -136,7 +136,7 @@ var ReactDOMSelect = {
136 });
137 },
138
139 - mountWrapper: function(element: Element, props: Object) {
139 + initWrapperState: function(element: Element, props: Object) {
140 var node = ((element: any): SelectWithWrapperState);
141 if (__DEV__) {
142 checkSelectPropTypes(props);
@@ -163,8 +163,12 @@ var ReactDOMSelect = {
163 );
164 didWarnValueDefaultValue = true;
165 }
166 + },
167
168 + postMountWrapper: function(element: Element, props: Object) {
169 + var node = ((element: any): SelectWithWrapperState);
170 node.multiple = !!props.multiple;
171 + var value = props.value;
172 if (value != null) {
173 updateOptions(node, !!props.multiple, value);
174 } else if (props.defaultValue != null) {
src/renderers/dom/fiber/wrappers/ReactDOMFiberTextarea.js
+1 -1
@@ -66,7 +66,7 @@ var ReactDOMTextarea = {
66 return hostProps;
67 },
68
69 - mountWrapper: function(element: Element, props: Object) {
69 + initWrapperState: function(element: Element, props: Object) {
70 var node = ((element: any): TextAreaWithWrapperState);
71 if (__DEV__) {
72 ReactControlledValuePropTypes.checkPropTypes(
src/renderers/dom/shared/__tests__/ReactDOMServerIntegration-test.js
+34 -5
@@ -106,11 +106,31 @@ const clientRenderOnServerString = async (element, errorCount = 0) => {
106 return clientElement;
107 };
108
109 -const clientRenderOnBadMarkup = (element, errorCount = 0) => {
109 +function BadMarkupExpected() {}
110 +
111 +const clientRenderOnBadMarkup = async (element, errorCount = 0) => {
112 + // First we render the top of bad mark up.
113 var domElement = document.createElement('div');
114 domElement.innerHTML =
115 '<div id="badIdWhichWillCauseMismatch" data-reactroot="" data-reactid="1"></div>';
113 - return renderIntoDom(element, domElement, errorCount + 1);
116 + await renderIntoDom(element, domElement, errorCount + 1);
117 +
118 + // This gives us the resulting text content.
119 + var hydratedTextContent = domElement.textContent;
120 +
121 + // Next we render the element into a clean DOM node client side.
122 + const cleanDomElement = document.createElement('div');
123 + ExecutionEnvironment.canUseDOM = true;
124 + await asyncReactDOMRender(element, cleanDomElement);
125 + ExecutionEnvironment.canUseDOM = false;
126 + // This gives us the expected text content.
127 + const cleanTextContent = cleanDomElement.textContent;
128 +
129 + // The only guarantee is that text content has been patched up if needed.
130 + expect(hydratedTextContent).toBe(cleanTextContent);
131 +
132 + // Abort any further expects. All bets are off at this point.
133 + throw new BadMarkupExpected();
134 };
135
136 // runs a DOM rendering test as four different tests, with four different rendering
@@ -148,8 +168,17 @@ function itClientRenders(desc, testFn) {
168 testFn(clientCleanRender));
169 it(`renders ${desc} with client render on top of good server markup`, () =>
170 testFn(clientRenderOnServerString));
151 - it(`renders ${desc} with client render on top of bad server markup`, () =>
152 - testFn(clientRenderOnBadMarkup));
171 + it(`renders ${desc} with client render on top of bad server markup`, async () => {
172 + try {
173 + await testFn(clientRenderOnBadMarkup);
174 + } catch (x) {
175 + // We expect this to trigger the BadMarkupExpected rejection.
176 + if (!(x instanceof BadMarkupExpected)) {
177 + // If not, rethrow.
178 + throw x;
179 + }
180 + }
181 + });
182 }
183
184 function itThrows(desc, testFn) {
@@ -425,7 +454,7 @@ describe('ReactDOMServerIntegration', () => {
454
455 itRenders('no dangerouslySetInnerHTML attribute', async render => {
456 const e = await render(
428 - <div dangerouslySetInnerHTML={{__html: 'foo'}} />,
457 + <div dangerouslySetInnerHTML={{__html: '<foo />'}} />,
458 );
459 expect(e.getAttribute('dangerouslySetInnerHTML')).toBe(null);
460 });
src/renderers/shared/fiber/ReactFiberBeginWork.js
+1 -1
@@ -66,7 +66,7 @@ if (__DEV__) {
66 module.exports = function<T, P, I, TI, PI, C, CX, PL>(
67 config: HostConfig<T, P, I, TI, PI, C, CX, PL>,
68 hostContext: HostContext<C, CX>,
69 - hydrationContext: HydrationContext<I, TI, C>,
69 + hydrationContext: HydrationContext<C>,
70 scheduleUpdate: (fiber: Fiber, priorityLevel: PriorityLevel) => void,
71 getPriorityContext: (fiber: Fiber, forceAsync: boolean) => PriorityLevel,
72 ) {
src/renderers/shared/fiber/ReactFiberCommitWork.js
+13 -5
@@ -392,10 +392,13 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
392 }
393 case HostComponent: {
394 const instance: I = finishedWork.stateNode;
395 - if (instance != null && current !== null) {
395 + if (instance != null) {
396 // Commit the work prepared earlier.
397 const newProps = finishedWork.memoizedProps;
398 - const oldProps = current.memoizedProps;
398 + // For hydration we reuse the update path but we treat the oldProps
399 + // as the newProps. The updatePayload will contain the real change in
400 + // this case.
401 + const oldProps = current !== null ? current.memoizedProps : newProps;
402 const type = finishedWork.type;
403 // TODO: Type the updateQueue to be specific to host components.
404 const updatePayload: null | PL = (finishedWork.updateQueue: any);
@@ -415,13 +418,18 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
418 }
419 case HostText: {
420 invariant(
418 - finishedWork.stateNode !== null && current !== null,
419 - 'This should only be done during updates. This error is likely ' +
421 + finishedWork.stateNode !== null,
422 + 'This should have a text node initialized. This error is likely ' +
423 'caused by a bug in React. Please file an issue.',
424 );
425 const textInstance: TI = finishedWork.stateNode;
426 const newText: string = finishedWork.memoizedProps;
424 - const oldText: string = current.memoizedProps;
427 + // For hydration we reuse the update path but we treat the oldProps
428 + // as the newProps. The updatePayload will contain the real change in
429 + // this case.
430 + const oldText: string = current !== null
431 + ? current.memoizedProps
432 + : newText;
433 commitTextUpdate(textInstance, oldText, newText);
434 return;
435 }
src/renderers/shared/fiber/ReactFiberCompleteWork.js
+21 -14
@@ -47,7 +47,7 @@ var invariant = require('fbjs/lib/invariant');
47 module.exports = function<T, P, I, TI, PI, C, CX, PL>(
48 config: HostConfig<T, P, I, TI, PI, C, CX, PL>,
49 hostContext: HostContext<C, CX>,
50 - hydrationContext: HydrationContext<I, TI, C>,
50 + hydrationContext: HydrationContext<C>,
51 ) {
52 const {
53 createInstance,
@@ -65,8 +65,8 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
65 } = hostContext;
66
67 const {
68 - hydrateHostInstance,
69 - hydrateHostTextInstance,
68 + prepareToHydrateHostInstance,
69 + prepareToHydrateHostTextInstance,
70 popHydrationState,
71 } = hydrationContext;
72
@@ -275,15 +275,22 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
275 // "stack" as the parent. Then append children as we go in beginWork
276 // or completeWork depending on we want to add then top->down or
277 // bottom->up. Top->down is faster in IE11.
278 - let instance;
278 let wasHydrated = popHydrationState(workInProgress);
279 if (wasHydrated) {
281 - instance = hydrateHostInstance(
282 - workInProgress,
283 - rootContainerInstance,
284 - );
280 + // TOOD: Move this and createInstance step into the beginPhase
281 + // to consolidate.
282 + if (
283 + prepareToHydrateHostInstance(
284 + workInProgress,
285 + rootContainerInstance,
286 + )
287 + ) {
288 + // If changes to the hydrated node needs to be applied at the
289 + // commit-phase we mark this as such.
290 + markUpdate(workInProgress);
291 + }
292 } else {
286 - instance = createInstance(
293 + let instance = createInstance(
294 type,
295 newProps,
296 rootContainerInstance,
@@ -306,9 +313,9 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
313 ) {
314 markUpdate(workInProgress);
315 }
316 + workInProgress.stateNode = instance;
317 }
318
311 - workInProgress.stateNode = instance;
319 if (workInProgress.ref !== null) {
320 // If there is a ref on a host node we need to schedule a callback
321 markRef(workInProgress);
@@ -337,19 +344,19 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
344 }
345 const rootContainerInstance = getRootHostContainer();
346 const currentHostContext = getHostContext();
340 - let textInstance;
347 let wasHydrated = popHydrationState(workInProgress);
348 if (wasHydrated) {
343 - textInstance = hydrateHostTextInstance(workInProgress);
349 + if (prepareToHydrateHostTextInstance(workInProgress)) {
350 + markUpdate(workInProgress);
351 + }
352 } else {
345 - textInstance = createTextInstance(
353 + workInProgress.stateNode = createTextInstance(
354 newText,
355 rootContainerInstance,
356 currentHostContext,
357 workInProgress,
358 );
359 }
352 - workInProgress.stateNode = textInstance;
360 }
361 return null;
362 }
src/renderers/shared/fiber/ReactFiberHydrationContext.js
+51 -27
@@ -17,23 +17,23 @@ import type {HostConfig} from 'ReactFiberReconciler';
17
18 var invariant = require('fbjs/lib/invariant');
19
20 -const {HostComponent, HostRoot} = require('ReactTypeOfWork');
20 +const {HostComponent, HostText, HostRoot} = require('ReactTypeOfWork');
21 const {Deletion, Placement} = require('ReactTypeOfSideEffect');
22
23 const {createFiberFromHostInstanceForDeletion} = require('ReactFiber');
24
25 -export type HydrationContext<I, TI, C> = {
25 +export type HydrationContext<C> = {
26 enterHydrationState(fiber: Fiber): boolean,
27 resetHydrationState(): void,
28 tryToClaimNextHydratableInstance(fiber: Fiber): void,
29 - hydrateHostInstance(fiber: Fiber, rootContainerInstance: C): I,
30 - hydrateHostTextInstance(fiber: Fiber): TI,
29 + prepareToHydrateHostInstance(fiber: Fiber, rootContainerInstance: C): boolean,
30 + prepareToHydrateHostTextInstance(fiber: Fiber): boolean,
31 popHydrationState(fiber: Fiber): boolean,
32 };
33
34 module.exports = function<T, P, I, TI, PI, C, CX, PL>(
35 config: HostConfig<T, P, I, TI, PI, C, CX, PL>,
36 -): HydrationContext<I, TI, C> {
36 +): HydrationContext<C> {
37 const {
38 shouldSetTextContent,
39 canHydrateInstance,
@@ -59,10 +59,10 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
59 },
60 resetHydrationState() {},
61 tryToClaimNextHydratableInstance() {},
62 - hydrateHostInstance() {
62 + prepareToHydrateHostInstance() {
63 invariant(false, 'React bug.');
64 },
65 - hydrateHostTextInstance() {
65 + prepareToHydrateHostTextInstance() {
66 invariant(false, 'React bug.');
67 },
68 popHydrationState(fiber: Fiber) {
@@ -89,16 +89,13 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
89 const childToDelete = createFiberFromHostInstanceForDeletion();
90 childToDelete.stateNode = instance;
91 childToDelete.return = returnFiber;
92 - // Deletions are added in reversed order so we add it to the front.
93 - const last = returnFiber.progressedLastDeletion;
94 - if (last !== null) {
95 - last.nextEffect = childToDelete;
96 - returnFiber.progressedLastDeletion = childToDelete;
97 - } else {
98 - returnFiber.progressedFirstDeletion = returnFiber.progressedLastDeletion = childToDelete;
99 - }
92 childToDelete.effectTag = Deletion;
93
94 + // This might seem like it belongs on progressedFirstDeletion. However,
95 + // these children are not part of the reconciliation list of children.
96 + // Even if we abort and rereconcile the children, that will try to hydrate
97 + // again and the nodes are still in the host tree so these will be
98 + // recreated.
99 if (returnFiber.lastEffect !== null) {
100 returnFiber.lastEffect.nextEffect = childToDelete;
101 returnFiber.lastEffect = childToDelete;
@@ -107,6 +104,21 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
104 }
105 }
106
107 + function canHydrate(fiber, nextInstance) {
108 + switch (fiber.tag) {
109 + case HostComponent: {
110 + const type = fiber.type;
111 + const props = fiber.memoizedProps;
112 + return canHydrateInstance(nextInstance, type, props);
113 + }
114 + case HostText: {
115 + return canHydrateTextInstance(nextInstance);
116 + }
117 + default:
118 + return false;
119 + }
120 + }
121 +
122 function tryToClaimNextHydratableInstance(fiber: Fiber) {
123 if (!isHydrating) {
124 return;
@@ -119,14 +131,12 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
131 hydrationParentFiber = fiber;
132 return;
133 }
122 - const type = fiber.type;
123 - const props = fiber.memoizedProps;
124 - if (!canHydrateInstance(nextInstance, type, props)) {
134 + if (!canHydrate(fiber, nextInstance)) {
135 // If we can't hydrate this instance let's try the next one.
136 // We use this as a heuristic. It's based on intuition and not data so it
137 // might be flawed or unnecessary.
138 nextInstance = getNextHydratableSibling(nextInstance);
129 - if (!nextInstance || !canHydrateInstance(nextInstance, type, props)) {
139 + if (!nextInstance || !canHydrate(fiber, nextInstance)) {
140 // Nothing to hydrate. Make it an insertion.
141 fiber.effectTag |= Placement;
142 isHydrating = false;
@@ -147,22 +157,36 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
157 nextHydratableInstance = getFirstHydratableChild(nextInstance);
158 }
159
150 - function hydrateHostInstance(fiber: Fiber, rootContainerInstance: C): I {
160 + function prepareToHydrateHostInstance(
161 + fiber: Fiber,
162 + rootContainerInstance: C,
163 + ): boolean {
164 const instance: I = fiber.stateNode;
152 - hydrateInstance(
165 + const updatePayload = hydrateInstance(
166 instance,
167 fiber.type,
168 fiber.memoizedProps,
169 rootContainerInstance,
170 fiber,
171 );
159 - return instance;
172 + // TODO: Type this specific to this type of component.
173 + fiber.updateQueue = (updatePayload: any);
174 + // If the update payload indicates that there is a change or if there
175 + // is a new ref we mark this as an update.
176 + if (updatePayload !== null) {
177 + return true;
178 + }
179 + return false;
180 }
181
162 - function hydrateHostTextInstance(fiber: Fiber): TI {
182 + function prepareToHydrateHostTextInstance(fiber: Fiber): boolean {
183 const textInstance: TI = fiber.stateNode;
164 - hydrateTextInstance(textInstance, fiber);
165 - return textInstance;
184 + const shouldUpdate = hydrateTextInstance(
185 + textInstance,
186 + fiber.memoizedProps,
187 + fiber,
188 + );
189 + return shouldUpdate;
190 }
191
192 function popToNextHostParent(fiber: Fiber): void {
@@ -229,8 +253,8 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
253 enterHydrationState,
254 resetHydrationState,
255 tryToClaimNextHydratableInstance,
232 - hydrateHostInstance,
233 - hydrateHostTextInstance,
256 + prepareToHydrateHostInstance,
257 + prepareToHydrateHostTextInstance,
258 popHydrationState,
259 };
260 };
src/renderers/shared/fiber/ReactFiberReconciler.js
+3 -2
@@ -126,11 +126,12 @@ export type HostConfig<T, P, I, TI, PI, C, CX, PL> = {
126 props: P,
127 rootContainerInstance: C,
128 internalInstanceHandle: OpaqueHandle,
129 - ) => void,
129 + ) => null | PL,
130 hydrateTextInstance?: (
131 textInstance: TI,
132 + text: string,
133 internalInstanceHandle: OpaqueHandle,
133 - ) => void,
134 + ) => boolean,
135
136 useSyncScheduling?: boolean,
137 };
src/renderers/shared/fiber/ReactFiberScheduler.js
+3 -5
@@ -146,11 +146,9 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
146 config: HostConfig<T, P, I, TI, PI, C, CX, PL>,
147 ) {
148 const hostContext = ReactFiberHostContext(config);
149 - const hydrationContext: HydrationContext<
150 - I,
151 - TI,
152 - C
153 - > = ReactFiberHydrationContext(config);
149 + const hydrationContext: HydrationContext<C> = ReactFiberHydrationContext(
150 + config,
151 + );
152 const {popHostContainer, popHostContext, resetHostContainer} = hostContext;
153 const {beginWork, beginFailedWork} = ReactFiberBeginWork(
154 config,