mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
47b003a828
* Extract base Jest config This makes it easier to change the source config without affecting the build test config. * Statically import the host config This changes react-reconciler to import HostConfig instead of getting it through a function argument. Rather than start with packages like ReactDOM that want to inline it, I started with React Noop and ensured that *custom* renderers using react-reconciler package still work. To do this, I'm making HostConfig module in the reconciler look at a global variable by default (which, in case of the react-reconciler npm package, ends up being the host config argument in the top-level scope). This is still very broken. * Add scaffolding for importing an inlined renderer * Fix the build * ES exports for renderer methods * ES modules for host configs * Remove closures from the reconciler * Check each renderer's config with Flow * Fix uncovered Flow issue We know nextHydratableInstance doesn't get mutated inside this function, but Flow doesn't so it thinks it may be null. Help Flow. * Prettier * Get rid of enable*Reconciler flags They are not as useful anymore because for almost all cases (except third party renderers) we *know* whether it supports mutation or persistence. This refactoring means react-reconciler and react-reconciler/persistent third-party packages now ship the same thing. Not ideal, but this seems worth how simpler the code becomes. We can later look into addressing it by having a single toggle instead. * Prettier again * Fix Flow config creation issue * Fix imprecise Flow typing * Revert accidental changes
581 lines
15 KiB
JavaScript
581 lines
15 KiB
JavaScript
/**
|
|
* Copyright (c) 2013-present, Facebook, Inc.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @flow
|
|
*/
|
|
|
|
import * as ReactScheduler from 'react-scheduler';
|
|
|
|
import * as ReactDOMComponentTree from './ReactDOMComponentTree';
|
|
import * as ReactDOMFiberComponent from './ReactDOMFiberComponent';
|
|
import * as ReactInputSelection from './ReactInputSelection';
|
|
import setTextContent from './setTextContent';
|
|
import validateDOMNesting from './validateDOMNesting';
|
|
import * as ReactBrowserEventEmitter from '../events/ReactBrowserEventEmitter';
|
|
import {getChildNamespace} from '../shared/DOMNamespaces';
|
|
import {
|
|
ELEMENT_NODE,
|
|
TEXT_NODE,
|
|
COMMENT_NODE,
|
|
DOCUMENT_NODE,
|
|
DOCUMENT_FRAGMENT_NODE,
|
|
} from '../shared/HTMLNodeType';
|
|
|
|
export type Type = string;
|
|
export type Props = {
|
|
autoFocus?: boolean,
|
|
children?: mixed,
|
|
hidden?: boolean,
|
|
suppressHydrationWarning?: boolean,
|
|
};
|
|
export type Container = Element | Document;
|
|
export type Instance = Element;
|
|
export type TextInstance = Text;
|
|
export type HydratableInstance = Element | Text;
|
|
export type PublicInstance = Element | Text;
|
|
type HostContextDev = {
|
|
namespace: string,
|
|
ancestorInfo: mixed,
|
|
};
|
|
type HostContextProd = string;
|
|
export type HostContext = HostContextDev | HostContextProd;
|
|
export type UpdatePayload = Array<mixed>;
|
|
export type ChildSet = void; // Unused
|
|
|
|
const {
|
|
createElement,
|
|
createTextNode,
|
|
setInitialProperties,
|
|
diffProperties,
|
|
updateProperties,
|
|
diffHydratedProperties,
|
|
diffHydratedText,
|
|
warnForUnmatchedText,
|
|
warnForDeletedHydratableElement,
|
|
warnForDeletedHydratableText,
|
|
warnForInsertedHydratedElement,
|
|
warnForInsertedHydratedText,
|
|
} = ReactDOMFiberComponent;
|
|
const {updatedAncestorInfo} = validateDOMNesting;
|
|
const {precacheFiberNode, updateFiberProps} = ReactDOMComponentTree;
|
|
|
|
let SUPPRESS_HYDRATION_WARNING;
|
|
if (__DEV__) {
|
|
SUPPRESS_HYDRATION_WARNING = 'suppressHydrationWarning';
|
|
}
|
|
|
|
let eventsEnabled: ?boolean = null;
|
|
let selectionInformation: ?mixed = null;
|
|
|
|
function shouldAutoFocusHostComponent(type: string, props: Props): boolean {
|
|
switch (type) {
|
|
case 'button':
|
|
case 'input':
|
|
case 'select':
|
|
case 'textarea':
|
|
return !!props.autoFocus;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
export * from 'shared/HostConfigWithNoPersistence';
|
|
|
|
export function getRootHostContext(
|
|
rootContainerInstance: Container,
|
|
): HostContext {
|
|
let type;
|
|
let namespace;
|
|
const nodeType = rootContainerInstance.nodeType;
|
|
switch (nodeType) {
|
|
case DOCUMENT_NODE:
|
|
case DOCUMENT_FRAGMENT_NODE: {
|
|
type = nodeType === DOCUMENT_NODE ? '#document' : '#fragment';
|
|
let root = (rootContainerInstance: any).documentElement;
|
|
namespace = root ? root.namespaceURI : getChildNamespace(null, '');
|
|
break;
|
|
}
|
|
default: {
|
|
const container: any =
|
|
nodeType === COMMENT_NODE
|
|
? rootContainerInstance.parentNode
|
|
: rootContainerInstance;
|
|
const ownNamespace = container.namespaceURI || null;
|
|
type = container.tagName;
|
|
namespace = getChildNamespace(ownNamespace, type);
|
|
break;
|
|
}
|
|
}
|
|
if (__DEV__) {
|
|
const validatedTag = type.toLowerCase();
|
|
const ancestorInfo = updatedAncestorInfo(null, validatedTag, null);
|
|
return {namespace, ancestorInfo};
|
|
}
|
|
return namespace;
|
|
}
|
|
|
|
export function getChildHostContext(
|
|
parentHostContext: HostContext,
|
|
type: string,
|
|
rootContainerInstance: Container,
|
|
): HostContext {
|
|
if (__DEV__) {
|
|
const parentHostContextDev = ((parentHostContext: any): HostContextDev);
|
|
const namespace = getChildNamespace(parentHostContextDev.namespace, type);
|
|
const ancestorInfo = updatedAncestorInfo(
|
|
parentHostContextDev.ancestorInfo,
|
|
type,
|
|
null,
|
|
);
|
|
return {namespace, ancestorInfo};
|
|
}
|
|
const parentNamespace = ((parentHostContext: any): HostContextProd);
|
|
return getChildNamespace(parentNamespace, type);
|
|
}
|
|
|
|
export function getPublicInstance(instance: Instance): * {
|
|
return instance;
|
|
}
|
|
|
|
export function prepareForCommit(containerInfo: Container): void {
|
|
eventsEnabled = ReactBrowserEventEmitter.isEnabled();
|
|
selectionInformation = ReactInputSelection.getSelectionInformation();
|
|
ReactBrowserEventEmitter.setEnabled(false);
|
|
}
|
|
|
|
export function resetAfterCommit(containerInfo: Container): void {
|
|
ReactInputSelection.restoreSelection(selectionInformation);
|
|
selectionInformation = null;
|
|
ReactBrowserEventEmitter.setEnabled(eventsEnabled);
|
|
eventsEnabled = null;
|
|
}
|
|
|
|
export function createInstance(
|
|
type: string,
|
|
props: Props,
|
|
rootContainerInstance: Container,
|
|
hostContext: HostContext,
|
|
internalInstanceHandle: Object,
|
|
): Instance {
|
|
let parentNamespace: string;
|
|
if (__DEV__) {
|
|
// TODO: take namespace into account when validating.
|
|
const hostContextDev = ((hostContext: any): HostContextDev);
|
|
validateDOMNesting(type, null, hostContextDev.ancestorInfo);
|
|
if (
|
|
typeof props.children === 'string' ||
|
|
typeof props.children === 'number'
|
|
) {
|
|
const string = '' + props.children;
|
|
const ownAncestorInfo = updatedAncestorInfo(
|
|
hostContextDev.ancestorInfo,
|
|
type,
|
|
null,
|
|
);
|
|
validateDOMNesting(null, string, ownAncestorInfo);
|
|
}
|
|
parentNamespace = hostContextDev.namespace;
|
|
} else {
|
|
parentNamespace = ((hostContext: any): HostContextProd);
|
|
}
|
|
const domElement: Instance = createElement(
|
|
type,
|
|
props,
|
|
rootContainerInstance,
|
|
parentNamespace,
|
|
);
|
|
precacheFiberNode(internalInstanceHandle, domElement);
|
|
updateFiberProps(domElement, props);
|
|
return domElement;
|
|
}
|
|
|
|
export function appendInitialChild(
|
|
parentInstance: Instance,
|
|
child: Instance | TextInstance,
|
|
): void {
|
|
parentInstance.appendChild(child);
|
|
}
|
|
|
|
export function finalizeInitialChildren(
|
|
domElement: Instance,
|
|
type: string,
|
|
props: Props,
|
|
rootContainerInstance: Container,
|
|
hostContext: HostContext,
|
|
): boolean {
|
|
setInitialProperties(domElement, type, props, rootContainerInstance);
|
|
return shouldAutoFocusHostComponent(type, props);
|
|
}
|
|
|
|
export function prepareUpdate(
|
|
domElement: Instance,
|
|
type: string,
|
|
oldProps: Props,
|
|
newProps: Props,
|
|
rootContainerInstance: Container,
|
|
hostContext: HostContext,
|
|
): null | Array<mixed> {
|
|
if (__DEV__) {
|
|
const hostContextDev = ((hostContext: any): HostContextDev);
|
|
if (
|
|
typeof newProps.children !== typeof oldProps.children &&
|
|
(typeof newProps.children === 'string' ||
|
|
typeof newProps.children === 'number')
|
|
) {
|
|
const string = '' + newProps.children;
|
|
const ownAncestorInfo = updatedAncestorInfo(
|
|
hostContextDev.ancestorInfo,
|
|
type,
|
|
null,
|
|
);
|
|
validateDOMNesting(null, string, ownAncestorInfo);
|
|
}
|
|
}
|
|
return diffProperties(
|
|
domElement,
|
|
type,
|
|
oldProps,
|
|
newProps,
|
|
rootContainerInstance,
|
|
);
|
|
}
|
|
|
|
export function shouldSetTextContent(type: string, props: Props): boolean {
|
|
return (
|
|
type === 'textarea' ||
|
|
typeof props.children === 'string' ||
|
|
typeof props.children === 'number' ||
|
|
(typeof props.dangerouslySetInnerHTML === 'object' &&
|
|
props.dangerouslySetInnerHTML !== null &&
|
|
typeof props.dangerouslySetInnerHTML.__html === 'string')
|
|
);
|
|
}
|
|
|
|
export function shouldDeprioritizeSubtree(type: string, props: Props): boolean {
|
|
return !!props.hidden;
|
|
}
|
|
|
|
export function createTextInstance(
|
|
text: string,
|
|
rootContainerInstance: Container,
|
|
hostContext: HostContext,
|
|
internalInstanceHandle: Object,
|
|
): TextInstance {
|
|
if (__DEV__) {
|
|
const hostContextDev = ((hostContext: any): HostContextDev);
|
|
validateDOMNesting(null, text, hostContextDev.ancestorInfo);
|
|
}
|
|
const textNode: TextInstance = createTextNode(text, rootContainerInstance);
|
|
precacheFiberNode(internalInstanceHandle, textNode);
|
|
return textNode;
|
|
}
|
|
|
|
export const now = ReactScheduler.now;
|
|
export const isPrimaryRenderer = true;
|
|
export const scheduleDeferredCallback = ReactScheduler.scheduleWork;
|
|
export const cancelDeferredCallback = ReactScheduler.cancelScheduledWork;
|
|
|
|
// -------------------
|
|
// Mutation
|
|
// -------------------
|
|
|
|
export const supportsMutation = true;
|
|
|
|
export function commitMount(
|
|
domElement: Instance,
|
|
type: string,
|
|
newProps: Props,
|
|
internalInstanceHandle: Object,
|
|
): void {
|
|
// Despite the naming that might imply otherwise, this method only
|
|
// fires if there is an `Update` effect scheduled during mounting.
|
|
// This happens if `finalizeInitialChildren` returns `true` (which it
|
|
// does to implement the `autoFocus` attribute on the client). But
|
|
// there are also other cases when this might happen (such as patching
|
|
// up text content during hydration mismatch). So we'll check this again.
|
|
if (shouldAutoFocusHostComponent(type, newProps)) {
|
|
((domElement: any):
|
|
| HTMLButtonElement
|
|
| HTMLInputElement
|
|
| HTMLSelectElement
|
|
| HTMLTextAreaElement).focus();
|
|
}
|
|
}
|
|
|
|
export function commitUpdate(
|
|
domElement: Instance,
|
|
updatePayload: Array<mixed>,
|
|
type: string,
|
|
oldProps: Props,
|
|
newProps: Props,
|
|
internalInstanceHandle: Object,
|
|
): void {
|
|
// Update the props handle so that we know which props are the ones with
|
|
// with current event handlers.
|
|
updateFiberProps(domElement, newProps);
|
|
// Apply the diff to the DOM node.
|
|
updateProperties(domElement, updatePayload, type, oldProps, newProps);
|
|
}
|
|
|
|
export function resetTextContent(domElement: Instance): void {
|
|
setTextContent(domElement, '');
|
|
}
|
|
|
|
export function commitTextUpdate(
|
|
textInstance: TextInstance,
|
|
oldText: string,
|
|
newText: string,
|
|
): void {
|
|
textInstance.nodeValue = newText;
|
|
}
|
|
|
|
export function appendChild(
|
|
parentInstance: Instance,
|
|
child: Instance | TextInstance,
|
|
): void {
|
|
parentInstance.appendChild(child);
|
|
}
|
|
|
|
export function appendChildToContainer(
|
|
container: Container,
|
|
child: Instance | TextInstance,
|
|
): void {
|
|
if (container.nodeType === COMMENT_NODE) {
|
|
(container.parentNode: any).insertBefore(child, container);
|
|
} else {
|
|
container.appendChild(child);
|
|
}
|
|
}
|
|
|
|
export function insertBefore(
|
|
parentInstance: Instance,
|
|
child: Instance | TextInstance,
|
|
beforeChild: Instance | TextInstance,
|
|
): void {
|
|
parentInstance.insertBefore(child, beforeChild);
|
|
}
|
|
|
|
export function insertInContainerBefore(
|
|
container: Container,
|
|
child: Instance | TextInstance,
|
|
beforeChild: Instance | TextInstance,
|
|
): void {
|
|
if (container.nodeType === COMMENT_NODE) {
|
|
(container.parentNode: any).insertBefore(child, beforeChild);
|
|
} else {
|
|
container.insertBefore(child, beforeChild);
|
|
}
|
|
}
|
|
|
|
export function removeChild(
|
|
parentInstance: Instance,
|
|
child: Instance | TextInstance,
|
|
): void {
|
|
parentInstance.removeChild(child);
|
|
}
|
|
|
|
export function removeChildFromContainer(
|
|
container: Container,
|
|
child: Instance | TextInstance,
|
|
): void {
|
|
if (container.nodeType === COMMENT_NODE) {
|
|
(container.parentNode: any).removeChild(child);
|
|
} else {
|
|
container.removeChild(child);
|
|
}
|
|
}
|
|
|
|
// -------------------
|
|
// Hydration
|
|
// -------------------
|
|
|
|
export const supportsHydration = true;
|
|
|
|
export function canHydrateInstance(
|
|
instance: Instance | TextInstance,
|
|
type: string,
|
|
props: Props,
|
|
): null | Instance {
|
|
if (
|
|
instance.nodeType !== ELEMENT_NODE ||
|
|
type.toLowerCase() !== instance.nodeName.toLowerCase()
|
|
) {
|
|
return null;
|
|
}
|
|
// This has now been refined to an element node.
|
|
return ((instance: any): Instance);
|
|
}
|
|
|
|
export function canHydrateTextInstance(
|
|
instance: Instance | TextInstance,
|
|
text: string,
|
|
): null | TextInstance {
|
|
if (text === '' || instance.nodeType !== TEXT_NODE) {
|
|
// Empty strings are not parsed by HTML so there won't be a correct match here.
|
|
return null;
|
|
}
|
|
// This has now been refined to a text node.
|
|
return ((instance: any): TextInstance);
|
|
}
|
|
|
|
export function getNextHydratableSibling(
|
|
instance: Instance | TextInstance,
|
|
): null | Instance | TextInstance {
|
|
let node = instance.nextSibling;
|
|
// Skip non-hydratable nodes.
|
|
while (
|
|
node &&
|
|
node.nodeType !== ELEMENT_NODE &&
|
|
node.nodeType !== TEXT_NODE
|
|
) {
|
|
node = node.nextSibling;
|
|
}
|
|
return (node: any);
|
|
}
|
|
|
|
export function getFirstHydratableChild(
|
|
parentInstance: Container | Instance,
|
|
): null | Instance | TextInstance {
|
|
let next = parentInstance.firstChild;
|
|
// Skip non-hydratable nodes.
|
|
while (
|
|
next &&
|
|
next.nodeType !== ELEMENT_NODE &&
|
|
next.nodeType !== TEXT_NODE
|
|
) {
|
|
next = next.nextSibling;
|
|
}
|
|
return (next: any);
|
|
}
|
|
|
|
export function hydrateInstance(
|
|
instance: Instance,
|
|
type: string,
|
|
props: Props,
|
|
rootContainerInstance: Container,
|
|
hostContext: HostContext,
|
|
internalInstanceHandle: Object,
|
|
): null | Array<mixed> {
|
|
precacheFiberNode(internalInstanceHandle, instance);
|
|
// TODO: Possibly defer this until the commit phase where all the events
|
|
// get attached.
|
|
updateFiberProps(instance, props);
|
|
let parentNamespace: string;
|
|
if (__DEV__) {
|
|
const hostContextDev = ((hostContext: any): HostContextDev);
|
|
parentNamespace = hostContextDev.namespace;
|
|
} else {
|
|
parentNamespace = ((hostContext: any): HostContextProd);
|
|
}
|
|
return diffHydratedProperties(
|
|
instance,
|
|
type,
|
|
props,
|
|
parentNamespace,
|
|
rootContainerInstance,
|
|
);
|
|
}
|
|
|
|
export function hydrateTextInstance(
|
|
textInstance: TextInstance,
|
|
text: string,
|
|
internalInstanceHandle: Object,
|
|
): boolean {
|
|
precacheFiberNode(internalInstanceHandle, textInstance);
|
|
return diffHydratedText(textInstance, text);
|
|
}
|
|
|
|
export function didNotMatchHydratedContainerTextInstance(
|
|
parentContainer: Container,
|
|
textInstance: TextInstance,
|
|
text: string,
|
|
) {
|
|
if (__DEV__) {
|
|
warnForUnmatchedText(textInstance, text);
|
|
}
|
|
}
|
|
|
|
export function didNotMatchHydratedTextInstance(
|
|
parentType: string,
|
|
parentProps: Props,
|
|
parentInstance: Instance,
|
|
textInstance: TextInstance,
|
|
text: string,
|
|
) {
|
|
if (__DEV__ && parentProps[SUPPRESS_HYDRATION_WARNING] !== true) {
|
|
warnForUnmatchedText(textInstance, text);
|
|
}
|
|
}
|
|
|
|
export function didNotHydrateContainerInstance(
|
|
parentContainer: Container,
|
|
instance: Instance | TextInstance,
|
|
) {
|
|
if (__DEV__) {
|
|
if (instance.nodeType === 1) {
|
|
warnForDeletedHydratableElement(parentContainer, (instance: any));
|
|
} else {
|
|
warnForDeletedHydratableText(parentContainer, (instance: any));
|
|
}
|
|
}
|
|
}
|
|
|
|
export function didNotHydrateInstance(
|
|
parentType: string,
|
|
parentProps: Props,
|
|
parentInstance: Instance,
|
|
instance: Instance | TextInstance,
|
|
) {
|
|
if (__DEV__ && parentProps[SUPPRESS_HYDRATION_WARNING] !== true) {
|
|
if (instance.nodeType === 1) {
|
|
warnForDeletedHydratableElement(parentInstance, (instance: any));
|
|
} else {
|
|
warnForDeletedHydratableText(parentInstance, (instance: any));
|
|
}
|
|
}
|
|
}
|
|
|
|
export function didNotFindHydratableContainerInstance(
|
|
parentContainer: Container,
|
|
type: string,
|
|
props: Props,
|
|
) {
|
|
if (__DEV__) {
|
|
warnForInsertedHydratedElement(parentContainer, type, props);
|
|
}
|
|
}
|
|
|
|
export function didNotFindHydratableContainerTextInstance(
|
|
parentContainer: Container,
|
|
text: string,
|
|
) {
|
|
if (__DEV__) {
|
|
warnForInsertedHydratedText(parentContainer, text);
|
|
}
|
|
}
|
|
|
|
export function didNotFindHydratableInstance(
|
|
parentType: string,
|
|
parentProps: Props,
|
|
parentInstance: Instance,
|
|
type: string,
|
|
props: Props,
|
|
) {
|
|
if (__DEV__ && parentProps[SUPPRESS_HYDRATION_WARNING] !== true) {
|
|
warnForInsertedHydratedElement(parentInstance, type, props);
|
|
}
|
|
}
|
|
|
|
export function didNotFindHydratableTextInstance(
|
|
parentType: string,
|
|
parentProps: Props,
|
|
parentInstance: Instance,
|
|
text: string,
|
|
) {
|
|
if (__DEV__ && parentProps[SUPPRESS_HYDRATION_WARNING] !== true) {
|
|
warnForInsertedHydratedText(parentInstance, text);
|
|
}
|
|
}
|