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.
This commit is contained in:
Sebastian Markbåge 2017-06-06 15:19:38 -07:00 committed by GitHub
parent 7b16a46e0c
commit 262b9c72f5
15 changed files with 278 additions and 76 deletions

View File

@ -1,5 +1 @@
src/renderers/dom/shared/__tests__/ReactDOMServerIntegration-test.js
* should not blow away user-entered text on successful reconnect to an uncontrolled checkbox
* should not blow away user-entered text on successful reconnect to a controlled checkbox
* should not blow away user-selected value on successful reconnect to an uncontrolled select
* should not blow away user-selected value on successful reconnect to an controlled select

View File

@ -1226,6 +1226,10 @@ src/renderers/dom/shared/__tests__/ReactDOMServerIntegration-test.js
* renders a controlled select with client render on top of good server markup
* should not blow away user-entered text on successful reconnect to an uncontrolled input
* should not blow away user-entered text on successful reconnect to a controlled input
* should not blow away user-entered text on successful reconnect to an uncontrolled checkbox
* should not blow away user-entered text on successful reconnect to a controlled checkbox
* should not blow away user-selected value on successful reconnect to an uncontrolled select
* should not blow away user-selected value on successful reconnect to an controlled select
* renders class child with context with server string render
* renders class child with context with clean client render
* renders class child with context with client render on top of good server markup

View File

@ -48,6 +48,7 @@ var {
setInitialProperties,
diffProperties,
updateProperties,
diffHydratedProperties,
} = ReactDOMFiberComponent;
var {precacheFiberNode, updateFiberProps} = ReactDOMComponentTree;
@ -409,19 +410,21 @@ var DOMRenderer = ReactFiberReconciler({
props: Props,
rootContainerInstance: Container,
internalInstanceHandle: Object,
): void {
): null | Array<mixed> {
precacheFiberNode(internalInstanceHandle, instance);
// TODO: Possibly defer this until the commit phase where all the events
// get attached.
updateFiberProps(instance, props);
setInitialProperties(instance, type, props, rootContainerInstance);
return diffHydratedProperties(instance, type, props, rootContainerInstance);
},
hydrateTextInstance(
textInstance: TextInstance,
text: string,
internalInstanceHandle: Object,
): void {
): boolean {
precacheFiberNode(internalInstanceHandle, textInstance);
return textInstance.nodeValue !== text;
},
scheduleAnimationCallback: ReactDOMFrameScheduling.rAF,

View File

@ -177,10 +177,10 @@ function setInitialDOMProperties(
isCustomComponentTag: boolean,
): void {
for (var propKey in nextProps) {
var nextProp = nextProps[propKey];
if (!nextProps.hasOwnProperty(propKey)) {
continue;
}
var nextProp = nextProps[propKey];
if (propKey === STYLE) {
if (__DEV__) {
if (nextProp) {
@ -406,7 +406,7 @@ var ReactDOMFiberComponent = {
props = rawProps;
break;
case 'input':
ReactDOMFiberInput.mountWrapper(domElement, rawProps);
ReactDOMFiberInput.initWrapperState(domElement, rawProps);
props = ReactDOMFiberInput.getHostProps(domElement, rawProps);
trapBubbledEventsLocal(domElement, tag);
// For controlled components we always need to ensure we're listening
@ -414,11 +414,11 @@ var ReactDOMFiberComponent = {
ensureListeningTo(rootContainerElement, 'onChange');
break;
case 'option':
ReactDOMFiberOption.mountWrapper(domElement, rawProps);
ReactDOMFiberOption.validateProps(domElement, rawProps);
props = ReactDOMFiberOption.getHostProps(domElement, rawProps);
break;
case 'select':
ReactDOMFiberSelect.mountWrapper(domElement, rawProps);
ReactDOMFiberSelect.initWrapperState(domElement, rawProps);
props = ReactDOMFiberSelect.getHostProps(domElement, rawProps);
trapBubbledEventsLocal(domElement, tag);
// For controlled components we always need to ensure we're listening
@ -426,7 +426,7 @@ var ReactDOMFiberComponent = {
ensureListeningTo(rootContainerElement, 'onChange');
break;
case 'textarea':
ReactDOMFiberTextarea.mountWrapper(domElement, rawProps);
ReactDOMFiberTextarea.initWrapperState(domElement, rawProps);
props = ReactDOMFiberTextarea.getHostProps(domElement, rawProps);
trapBubbledEventsLocal(domElement, tag);
// For controlled components we always need to ensure we're listening
@ -462,6 +462,9 @@ var ReactDOMFiberComponent = {
case 'option':
ReactDOMFiberOption.postMountWrapper(domElement, rawProps);
break;
case 'select':
ReactDOMFiberSelect.postMountWrapper(domElement, rawProps);
break;
default:
if (typeof props.onClick === 'function') {
// TODO: This cast may not be sound for SVG, MathML or custom elements.
@ -704,6 +707,131 @@ var ReactDOMFiberComponent = {
}
},
diffHydratedProperties(
domElement: Element,
tag: string,
rawProps: Object,
rootContainerElement: Element | Document,
): null | Array<mixed> {
if (__DEV__) {
var isCustomComponentTag = isCustomComponent(tag, rawProps);
validatePropertiesInDevelopment(tag, rawProps);
if (isCustomComponentTag && !didWarnShadyDOM && domElement.shadyRoot) {
warning(
false,
'%s is using shady DOM. Using shady DOM with React can ' +
'cause things to break subtly.',
getCurrentFiberOwnerName() || 'A component',
);
didWarnShadyDOM = true;
}
}
switch (tag) {
case 'audio':
case 'form':
case 'iframe':
case 'img':
case 'image':
case 'link':
case 'object':
case 'source':
case 'video':
case 'details':
trapBubbledEventsLocal(domElement, tag);
break;
case 'input':
ReactDOMFiberInput.initWrapperState(domElement, rawProps);
trapBubbledEventsLocal(domElement, tag);
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
ensureListeningTo(rootContainerElement, 'onChange');
break;
case 'option':
ReactDOMFiberOption.validateProps(domElement, rawProps);
break;
case 'select':
ReactDOMFiberSelect.initWrapperState(domElement, rawProps);
trapBubbledEventsLocal(domElement, tag);
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
ensureListeningTo(rootContainerElement, 'onChange');
break;
case 'textarea':
ReactDOMFiberTextarea.initWrapperState(domElement, rawProps);
trapBubbledEventsLocal(domElement, tag);
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
ensureListeningTo(rootContainerElement, 'onChange');
break;
}
assertValidProps(tag, rawProps, getCurrentFiberOwnerName);
var updatePayload = null;
for (var propKey in rawProps) {
if (!rawProps.hasOwnProperty(propKey)) {
continue;
}
var nextProp = rawProps[propKey];
if (propKey === CHILDREN) {
// For text content children we compare against textContent. This
// might match additional HTML that is hidden when we read it using
// textContent. E.g. "foo" will match "f<span>oo</span>" but that still
// satisfies our requirement. Our requirement is not to produce perfect
// HTML and attributes. Ideally we should preserve structure but it's
// ok not to if the visible content is still enough to indicate what
// even listeners these nodes might be wired up to.
// TODO: Warn if there is more than a single textNode as a child.
// TODO: Should we use domElement.firstChild.nodeValue to compare?
if (typeof nextProp === 'string') {
if (domElement.textContent !== nextProp) {
updatePayload = [CHILDREN, nextProp];
}
} else if (typeof nextProp === 'number') {
if (domElement.textContent !== '' + nextProp) {
updatePayload = [CHILDREN, '' + nextProp];
}
}
} else if (registrationNameModules.hasOwnProperty(propKey)) {
if (nextProp) {
ensureListeningTo(rootContainerElement, propKey);
}
}
}
switch (tag) {
case 'input':
// TODO: Make sure we check if this is still unmounted or do any clean
// up necessary since we never stop tracking anymore.
inputValueTracking.trackNode((domElement: any));
ReactDOMFiberInput.postMountWrapper(domElement, rawProps);
break;
case 'textarea':
// TODO: Make sure we check if this is still unmounted or do any clean
// up necessary since we never stop tracking anymore.
inputValueTracking.trackNode((domElement: any));
ReactDOMFiberTextarea.postMountWrapper(domElement, rawProps);
break;
case 'select':
case 'option':
// For input and textarea we current always set the value property at
// post mount to force it to diverge from attributes. However, for
// option and select we don't quite do the same thing and select
// is not resilient to the DOM state changing so we don't do that here.
// TODO: Consider not doing this for input and textarea.
break;
default:
if (typeof rawProps.onClick === 'function') {
// TODO: This cast may not be sound for SVG, MathML or custom elements.
trapClickOnNonInteractiveElement(((domElement: any): HTMLElement));
}
break;
}
return updatePayload;
},
restoreControlledState(
domElement: Element,
tag: string,

View File

@ -89,7 +89,7 @@ var ReactDOMInput = {
return hostProps;
},
mountWrapper: function(element: Element, props: Object) {
initWrapperState: function(element: Element, props: Object) {
if (__DEV__) {
ReactControlledValuePropTypes.checkPropTypes(
'input',

View File

@ -39,7 +39,7 @@ function flattenChildren(children) {
* Implements an <option> host component that warns when `selected` is set.
*/
var ReactDOMOption = {
mountWrapper: function(element: Element, props: Object) {
validateProps: function(element: Element, props: Object) {
// TODO (yungsters): Remove support for `selected` in <option>.
if (__DEV__) {
warning(

View File

@ -136,7 +136,7 @@ var ReactDOMSelect = {
});
},
mountWrapper: function(element: Element, props: Object) {
initWrapperState: function(element: Element, props: Object) {
var node = ((element: any): SelectWithWrapperState);
if (__DEV__) {
checkSelectPropTypes(props);
@ -163,8 +163,12 @@ var ReactDOMSelect = {
);
didWarnValueDefaultValue = true;
}
},
postMountWrapper: function(element: Element, props: Object) {
var node = ((element: any): SelectWithWrapperState);
node.multiple = !!props.multiple;
var value = props.value;
if (value != null) {
updateOptions(node, !!props.multiple, value);
} else if (props.defaultValue != null) {

View File

@ -66,7 +66,7 @@ var ReactDOMTextarea = {
return hostProps;
},
mountWrapper: function(element: Element, props: Object) {
initWrapperState: function(element: Element, props: Object) {
var node = ((element: any): TextAreaWithWrapperState);
if (__DEV__) {
ReactControlledValuePropTypes.checkPropTypes(

View File

@ -106,11 +106,31 @@ const clientRenderOnServerString = async (element, errorCount = 0) => {
return clientElement;
};
const clientRenderOnBadMarkup = (element, errorCount = 0) => {
function BadMarkupExpected() {}
const clientRenderOnBadMarkup = async (element, errorCount = 0) => {
// First we render the top of bad mark up.
var domElement = document.createElement('div');
domElement.innerHTML =
'<div id="badIdWhichWillCauseMismatch" data-reactroot="" data-reactid="1"></div>';
return renderIntoDom(element, domElement, errorCount + 1);
await renderIntoDom(element, domElement, errorCount + 1);
// This gives us the resulting text content.
var hydratedTextContent = domElement.textContent;
// Next we render the element into a clean DOM node client side.
const cleanDomElement = document.createElement('div');
ExecutionEnvironment.canUseDOM = true;
await asyncReactDOMRender(element, cleanDomElement);
ExecutionEnvironment.canUseDOM = false;
// This gives us the expected text content.
const cleanTextContent = cleanDomElement.textContent;
// The only guarantee is that text content has been patched up if needed.
expect(hydratedTextContent).toBe(cleanTextContent);
// Abort any further expects. All bets are off at this point.
throw new BadMarkupExpected();
};
// runs a DOM rendering test as four different tests, with four different rendering
@ -148,8 +168,17 @@ function itClientRenders(desc, testFn) {
testFn(clientCleanRender));
it(`renders ${desc} with client render on top of good server markup`, () =>
testFn(clientRenderOnServerString));
it(`renders ${desc} with client render on top of bad server markup`, () =>
testFn(clientRenderOnBadMarkup));
it(`renders ${desc} with client render on top of bad server markup`, async () => {
try {
await testFn(clientRenderOnBadMarkup);
} catch (x) {
// We expect this to trigger the BadMarkupExpected rejection.
if (!(x instanceof BadMarkupExpected)) {
// If not, rethrow.
throw x;
}
}
});
}
function itThrows(desc, testFn) {
@ -425,7 +454,7 @@ describe('ReactDOMServerIntegration', () => {
itRenders('no dangerouslySetInnerHTML attribute', async render => {
const e = await render(
<div dangerouslySetInnerHTML={{__html: 'foo'}} />,
<div dangerouslySetInnerHTML={{__html: '<foo />'}} />,
);
expect(e.getAttribute('dangerouslySetInnerHTML')).toBe(null);
});

View File

@ -66,7 +66,7 @@ if (__DEV__) {
module.exports = function<T, P, I, TI, PI, C, CX, PL>(
config: HostConfig<T, P, I, TI, PI, C, CX, PL>,
hostContext: HostContext<C, CX>,
hydrationContext: HydrationContext<I, TI, C>,
hydrationContext: HydrationContext<C>,
scheduleUpdate: (fiber: Fiber, priorityLevel: PriorityLevel) => void,
getPriorityContext: (fiber: Fiber, forceAsync: boolean) => PriorityLevel,
) {

View File

@ -392,10 +392,13 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
}
case HostComponent: {
const instance: I = finishedWork.stateNode;
if (instance != null && current !== null) {
if (instance != null) {
// Commit the work prepared earlier.
const newProps = finishedWork.memoizedProps;
const oldProps = current.memoizedProps;
// For hydration we reuse the update path but we treat the oldProps
// as the newProps. The updatePayload will contain the real change in
// this case.
const oldProps = current !== null ? current.memoizedProps : newProps;
const type = finishedWork.type;
// TODO: Type the updateQueue to be specific to host components.
const updatePayload: null | PL = (finishedWork.updateQueue: any);
@ -415,13 +418,18 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
}
case HostText: {
invariant(
finishedWork.stateNode !== null && current !== null,
'This should only be done during updates. This error is likely ' +
finishedWork.stateNode !== null,
'This should have a text node initialized. This error is likely ' +
'caused by a bug in React. Please file an issue.',
);
const textInstance: TI = finishedWork.stateNode;
const newText: string = finishedWork.memoizedProps;
const oldText: string = current.memoizedProps;
// For hydration we reuse the update path but we treat the oldProps
// as the newProps. The updatePayload will contain the real change in
// this case.
const oldText: string = current !== null
? current.memoizedProps
: newText;
commitTextUpdate(textInstance, oldText, newText);
return;
}

View File

@ -47,7 +47,7 @@ var invariant = require('fbjs/lib/invariant');
module.exports = function<T, P, I, TI, PI, C, CX, PL>(
config: HostConfig<T, P, I, TI, PI, C, CX, PL>,
hostContext: HostContext<C, CX>,
hydrationContext: HydrationContext<I, TI, C>,
hydrationContext: HydrationContext<C>,
) {
const {
createInstance,
@ -65,8 +65,8 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
} = hostContext;
const {
hydrateHostInstance,
hydrateHostTextInstance,
prepareToHydrateHostInstance,
prepareToHydrateHostTextInstance,
popHydrationState,
} = hydrationContext;
@ -275,15 +275,22 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
// "stack" as the parent. Then append children as we go in beginWork
// or completeWork depending on we want to add then top->down or
// bottom->up. Top->down is faster in IE11.
let instance;
let wasHydrated = popHydrationState(workInProgress);
if (wasHydrated) {
instance = hydrateHostInstance(
workInProgress,
rootContainerInstance,
);
// TOOD: Move this and createInstance step into the beginPhase
// to consolidate.
if (
prepareToHydrateHostInstance(
workInProgress,
rootContainerInstance,
)
) {
// If changes to the hydrated node needs to be applied at the
// commit-phase we mark this as such.
markUpdate(workInProgress);
}
} else {
instance = createInstance(
let instance = createInstance(
type,
newProps,
rootContainerInstance,
@ -306,9 +313,9 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
) {
markUpdate(workInProgress);
}
workInProgress.stateNode = instance;
}
workInProgress.stateNode = instance;
if (workInProgress.ref !== null) {
// If there is a ref on a host node we need to schedule a callback
markRef(workInProgress);
@ -337,19 +344,19 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
}
const rootContainerInstance = getRootHostContainer();
const currentHostContext = getHostContext();
let textInstance;
let wasHydrated = popHydrationState(workInProgress);
if (wasHydrated) {
textInstance = hydrateHostTextInstance(workInProgress);
if (prepareToHydrateHostTextInstance(workInProgress)) {
markUpdate(workInProgress);
}
} else {
textInstance = createTextInstance(
workInProgress.stateNode = createTextInstance(
newText,
rootContainerInstance,
currentHostContext,
workInProgress,
);
}
workInProgress.stateNode = textInstance;
}
return null;
}

View File

@ -17,23 +17,23 @@ import type {HostConfig} from 'ReactFiberReconciler';
var invariant = require('fbjs/lib/invariant');
const {HostComponent, HostRoot} = require('ReactTypeOfWork');
const {HostComponent, HostText, HostRoot} = require('ReactTypeOfWork');
const {Deletion, Placement} = require('ReactTypeOfSideEffect');
const {createFiberFromHostInstanceForDeletion} = require('ReactFiber');
export type HydrationContext<I, TI, C> = {
export type HydrationContext<C> = {
enterHydrationState(fiber: Fiber): boolean,
resetHydrationState(): void,
tryToClaimNextHydratableInstance(fiber: Fiber): void,
hydrateHostInstance(fiber: Fiber, rootContainerInstance: C): I,
hydrateHostTextInstance(fiber: Fiber): TI,
prepareToHydrateHostInstance(fiber: Fiber, rootContainerInstance: C): boolean,
prepareToHydrateHostTextInstance(fiber: Fiber): boolean,
popHydrationState(fiber: Fiber): boolean,
};
module.exports = function<T, P, I, TI, PI, C, CX, PL>(
config: HostConfig<T, P, I, TI, PI, C, CX, PL>,
): HydrationContext<I, TI, C> {
): HydrationContext<C> {
const {
shouldSetTextContent,
canHydrateInstance,
@ -59,10 +59,10 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
},
resetHydrationState() {},
tryToClaimNextHydratableInstance() {},
hydrateHostInstance() {
prepareToHydrateHostInstance() {
invariant(false, 'React bug.');
},
hydrateHostTextInstance() {
prepareToHydrateHostTextInstance() {
invariant(false, 'React bug.');
},
popHydrationState(fiber: Fiber) {
@ -89,16 +89,13 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
const childToDelete = createFiberFromHostInstanceForDeletion();
childToDelete.stateNode = instance;
childToDelete.return = returnFiber;
// Deletions are added in reversed order so we add it to the front.
const last = returnFiber.progressedLastDeletion;
if (last !== null) {
last.nextEffect = childToDelete;
returnFiber.progressedLastDeletion = childToDelete;
} else {
returnFiber.progressedFirstDeletion = returnFiber.progressedLastDeletion = childToDelete;
}
childToDelete.effectTag = Deletion;
// This might seem like it belongs on progressedFirstDeletion. However,
// these children are not part of the reconciliation list of children.
// Even if we abort and rereconcile the children, that will try to hydrate
// again and the nodes are still in the host tree so these will be
// recreated.
if (returnFiber.lastEffect !== null) {
returnFiber.lastEffect.nextEffect = childToDelete;
returnFiber.lastEffect = childToDelete;
@ -107,6 +104,21 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
}
}
function canHydrate(fiber, nextInstance) {
switch (fiber.tag) {
case HostComponent: {
const type = fiber.type;
const props = fiber.memoizedProps;
return canHydrateInstance(nextInstance, type, props);
}
case HostText: {
return canHydrateTextInstance(nextInstance);
}
default:
return false;
}
}
function tryToClaimNextHydratableInstance(fiber: Fiber) {
if (!isHydrating) {
return;
@ -119,14 +131,12 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
hydrationParentFiber = fiber;
return;
}
const type = fiber.type;
const props = fiber.memoizedProps;
if (!canHydrateInstance(nextInstance, type, props)) {
if (!canHydrate(fiber, nextInstance)) {
// If we can't hydrate this instance let's try the next one.
// We use this as a heuristic. It's based on intuition and not data so it
// might be flawed or unnecessary.
nextInstance = getNextHydratableSibling(nextInstance);
if (!nextInstance || !canHydrateInstance(nextInstance, type, props)) {
if (!nextInstance || !canHydrate(fiber, nextInstance)) {
// Nothing to hydrate. Make it an insertion.
fiber.effectTag |= Placement;
isHydrating = false;
@ -147,22 +157,36 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
nextHydratableInstance = getFirstHydratableChild(nextInstance);
}
function hydrateHostInstance(fiber: Fiber, rootContainerInstance: C): I {
function prepareToHydrateHostInstance(
fiber: Fiber,
rootContainerInstance: C,
): boolean {
const instance: I = fiber.stateNode;
hydrateInstance(
const updatePayload = hydrateInstance(
instance,
fiber.type,
fiber.memoizedProps,
rootContainerInstance,
fiber,
);
return instance;
// TODO: Type this specific to this type of component.
fiber.updateQueue = (updatePayload: any);
// If the update payload indicates that there is a change or if there
// is a new ref we mark this as an update.
if (updatePayload !== null) {
return true;
}
return false;
}
function hydrateHostTextInstance(fiber: Fiber): TI {
function prepareToHydrateHostTextInstance(fiber: Fiber): boolean {
const textInstance: TI = fiber.stateNode;
hydrateTextInstance(textInstance, fiber);
return textInstance;
const shouldUpdate = hydrateTextInstance(
textInstance,
fiber.memoizedProps,
fiber,
);
return shouldUpdate;
}
function popToNextHostParent(fiber: Fiber): void {
@ -229,8 +253,8 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
enterHydrationState,
resetHydrationState,
tryToClaimNextHydratableInstance,
hydrateHostInstance,
hydrateHostTextInstance,
prepareToHydrateHostInstance,
prepareToHydrateHostTextInstance,
popHydrationState,
};
};

View File

@ -126,11 +126,12 @@ export type HostConfig<T, P, I, TI, PI, C, CX, PL> = {
props: P,
rootContainerInstance: C,
internalInstanceHandle: OpaqueHandle,
) => void,
) => null | PL,
hydrateTextInstance?: (
textInstance: TI,
text: string,
internalInstanceHandle: OpaqueHandle,
) => void,
) => boolean,
useSyncScheduling?: boolean,
};

View File

@ -146,11 +146,9 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
config: HostConfig<T, P, I, TI, PI, C, CX, PL>,
) {
const hostContext = ReactFiberHostContext(config);
const hydrationContext: HydrationContext<
I,
TI,
C
> = ReactFiberHydrationContext(config);
const hydrationContext: HydrationContext<C> = ReactFiberHydrationContext(
config,
);
const {popHostContainer, popHostContext, resetHostContainer} = hostContext;
const {beginWork, beginFailedWork} = ReactFiberBeginWork(
config,