src/lib/InternalComponent.js
import invariant from 'fbjs/lib/invariant';
import emptyFunction from 'fbjs/lib/emptyFunction';
import flattenChildren from 'react/lib/flattenChildren';
import ReactCurrentOwner from 'react/lib/ReactCurrentOwner';
import ReactElement from 'react/lib/ReactElement';
import ReactInstrumentation from 'react-dom/lib/ReactInstrumentation';
import ReactReconciler from 'react-dom/lib/ReactReconciler';
import ReactMultiChild from 'react-dom/lib/ReactMultiChild';
import ReactRef from 'react-dom/lib/ReactRef';
import Flags from './React3ComponentFlags';
import ID_PROPERTY_NAME from './utils/idPropertyName';
import React3CompositeComponentWrapper from './React3CompositeComponentWrapper';
import React3ComponentTree from './React3ComponentTree';
function processChildContext(context) {
return context;
}
class RemountTrigger {
constructor() {
this.wantRemount = false;
this.onTrigger = function onTrigger() {
};
this.trigger = () => {
this.wantRemount = true;
this.onTrigger();
};
}
}
const registrationNameModules = {};
function deleteListener(rootNodeID, propKey) {
console.log('deleteListener', rootNodeID, propKey); // eslint-disable-line
debugger; // eslint-disable-line
}
function enqueuePutListener(rootNodeID, propKey, nextProp, transaction) {
console.log('enqueuePutListener', rootNodeID, propKey, nextProp, transaction); // eslint-disable-line
debugger; // eslint-disable-line
}
function _arrayMove(array, oldIndex, newIndex) {
array.splice(newIndex, 0, array.splice(oldIndex, 1)[0]);
}
let setChildrenForInstrumentation = emptyFunction;
let setContentChildForInstrumentation = emptyFunction;
let getDebugID;
if (process.env.NODE_ENV !== 'production') {
/* eslint-disable global-require */
const ReactInstanceMap = require('react-dom/lib/ReactInstanceMap');
/* eslint-enable global-require */
getDebugID = function _(inst) {
if (!inst._debugID) {
// Check for ART-like instances. TODO: This is silly/gross.
const internal = ReactInstanceMap.get(inst);
if (internal) {
return internal._debugID;
}
}
return inst._debugID;
};
setChildrenForInstrumentation = function _(children) {
ReactInstrumentation.debugTool.onSetChildren(
this._debugID,
children ? Object.keys(children).map(key => children[key]._debugID) : []
);
};
setContentChildForInstrumentation = function _(content) {
const hasExistingContent = this._contentDebugID !== null && this._contentDebugID !== undefined;
const debugID = this._debugID;
// This ID represents the inlined child that has no backing instance:
const contentDebugID = `CDID-${debugID}`;
if (content == null) {
if (hasExistingContent) {
ReactInstrumentation.debugTool.onUnmountComponent(this._contentDebugID);
}
this._contentDebugID = null;
return;
}
this._contentDebugID = contentDebugID;
if (hasExistingContent) {
ReactInstrumentation.debugTool.onBeforeUpdateComponent(contentDebugID, content);
ReactInstrumentation.debugTool.onUpdateComponent(contentDebugID);
} else {
ReactInstrumentation.debugTool.onBeforeMountComponent(contentDebugID, content, debugID);
ReactInstrumentation.debugTool.onMountComponent(contentDebugID);
ReactInstrumentation.debugTool.onSetChildren(debugID, [contentDebugID]);
}
};
}
const getThreeObjectFromMountImage = img => img.threeObject;
const ReactMultiChildMixin = ReactMultiChild.Mixin;
// TODO sync ReactDOMComponent
class InternalComponent {
static displayName = 'React3Component';
constructor(element, react3RendererInstance) {
this._currentElement = element;
/**
* @type React3Renderer
*/
this._react3RendererInstance = react3RendererInstance;
this._elementType = element.type; // _tag
this._renderedChildren = [];
this._hostMarkup = null; // _hostNode
this._hostParent = null;
this._rootNodeID = 0;
this._hostID = 0; // _domID
this._hostContainerInfo = null;
this._threeObject = null;
this._topLevelWrapper = null;
this._markup = null;
this._nodeWithLegacyProperties = null;
this._forceRemountOfComponent = false;
this._flags = 0;
if (process.env.NODE_ENV !== 'production') {
this._ancestorInfo = null;
setContentChildForInstrumentation.call(this, null);
}
this.threeElementDescriptor = react3RendererInstance.threeElementDescriptors[element.type];
if (!this.threeElementDescriptor) {
if (process.env.NODE_ENV !== 'production') {
invariant(false, `No constructor for ${element.type}`);
} else {
invariant(false);
}
}
if (process.env.NODE_ENV !== 'production' || process.env.ENABLE_REACT_ADDON_HOOKS === 'true') {
this.highlightComponent = () => {
this.threeElementDescriptor.highlight(this._threeObject);
};
this.hideHighlight = () => {
this.threeElementDescriptor.hideHighlight(this._threeObject);
};
}
this.remountTrigger = new RemountTrigger();
this.remountTrigger.onTrigger = () => {
this._forceRemountOfComponent = true;
};
}
getHostMarkup() {
return this._markup;
}
getHostNode() {
// console.warn('host node?'); // eslint-disable-line no-console
return this._markup;
}
/**
* Generates root tag markup then recurses. This method has side effects and
* is not idempotent.
*
* @internal
* @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
* @param {?InternalComponent} hostParent the containing DOM component instance
* @param {?React3ContainerInfo} hostContainerInfo info about the host container
* @param {object} context
* @return {object} The computed markup.
*/
mountComponent(transaction, hostParent, hostContainerInfo, context) {
this._rootNodeID = this._react3RendererInstance.globalIdCounter++;
this._hostID = hostContainerInfo._idCounter++;
this._hostParent = hostParent;
this._hostContainerInfo = hostContainerInfo;
const element = this._currentElement;
if (process.env.NODE_ENV !== 'production') {
this.threeElementDescriptor.checkPropTypes(element,
this._currentElement._owner, this._debugID, element.props);
}
this._threeObject = this.threeElementDescriptor.construct(element.props);
this.threeElementDescriptor.applyInitialProps(this._threeObject, element.props);
this.threeElementDescriptor.placeRemountTrigger(this._threeObject, this.remountTrigger.trigger);
// create initial children
const childrenToUse = element.props.children;
let mountImages;
if (childrenToUse) {
mountImages = this.mountChildren(childrenToUse, transaction, context);
} else {
mountImages = [];
}
const markup = {
[ID_PROPERTY_NAME]: this._hostID,
_rootInstance: null,
elementType: element.type,
threeObject: this._threeObject,
parentMarkup: null,
childrenMarkup: mountImages,
toJSON: () => '---MARKUP---',
};
if (process.env.NODE_ENV !== 'production') {
invariant(!!this._threeObject.userData,
'No userdata present in threeobject for %s', element.type);
} else {
invariant(!!this._threeObject.userData);
}
Object.assign(this._threeObject.userData, {
object3D: this._threeObject,
react3internalComponent: this, // used for highlighting etc
toJSON: () => '---USERDATA---',
markup,
});
const threeElementDescriptors = this._react3RendererInstance.threeElementDescriptors;
if (mountImages && mountImages.length > 0) {
this.threeElementDescriptor.addChildren(this._threeObject,
mountImages.map(getThreeObjectFromMountImage));
for (let i = 0; i < mountImages.length; ++i) {
const mountImage = mountImages[i];
const descriptorForChild = threeElementDescriptors[mountImage.elementType];
mountImage.parentMarkup = markup;
descriptorForChild.setParent(mountImage.threeObject, this._threeObject);
}
}
this._markup = markup;
React3ComponentTree.precacheMarkup(this, this._markup);
this._flags |= Flags.hasCachedChildMarkups;
return markup;
}
/**
* @see ReactMultiChild._reconcilerInstantiateChildren
* Cloned because it uses
* @see React3Renderer.instantiateChildren
*
* @param nestedChildren
* @param transaction
* @param context
* @returns {*}
* @private
*/
_reconcilerInstantiateChildren(nestedChildren,
transaction,
context) {
if (process.env.NODE_ENV !== 'production') {
const selfDebugID = getDebugID(this);
if (this._currentElement) {
const previousCurrent = ReactCurrentOwner.current;
try {
ReactCurrentOwner.current = this._currentElement._owner;
return this._react3RendererInstance.instantiateChildren(
nestedChildren,
transaction,
context,
selfDebugID
);
} finally {
ReactCurrentOwner.current = previousCurrent;
}
}
}
return this._react3RendererInstance.instantiateChildren(
nestedChildren,
transaction,
context,
0
);
}
/**
* @see ReactMultiChild._reconcilerUpdateChildren
* Cloned because it uses
* @see React3Renderer.updateChildren
*
* @param prevChildren
* @param nextNestedChildrenElements
* @param mountImages
* @param removedMarkups
* @param transaction
* @param context
* @returns {?Object}
* @private
*/
_reconcilerUpdateChildren(prevChildren,
nextNestedChildrenElements,
mountImages,
removedMarkups,
transaction,
context) {
let nextChildren;
let selfDebugID = 0;
if (process.env.NODE_ENV !== 'production') {
selfDebugID = getDebugID(this);
if (this._currentElement) {
const previousCurrent = ReactCurrentOwner.current;
try {
ReactCurrentOwner.current = this._currentElement._owner;
nextChildren = flattenChildren(nextNestedChildrenElements, selfDebugID);
} finally {
ReactCurrentOwner.current = previousCurrent;
}
this._react3RendererInstance.updateChildren(
prevChildren,
nextChildren,
mountImages,
removedMarkups,
transaction,
this,
this._hostContainerInfo,
context,
selfDebugID
);
return nextChildren;
}
}
nextChildren = flattenChildren(nextNestedChildrenElements, selfDebugID);
this._react3RendererInstance.updateChildren(
prevChildren,
nextChildren,
mountImages,
removedMarkups,
transaction,
this,
this._hostContainerInfo,
context,
selfDebugID
);
return nextChildren;
}
/**
* @see ReactMultiChild.mountChildren
*
* Generates a "mount image" for each of the supplied children. In the case
* of `ReactDOMComponent`, a mount image is a string of markup.
*
* @param {?object} nestedChildren Nested child maps.
* @param transaction
* @param context
* @return {array} An array of mounted representations.
* @internal
*/
mountChildren(nestedChildren, transaction, context) {
const children = this._reconcilerInstantiateChildren(
nestedChildren,
transaction,
context,
);
this._renderedChildren = children;
const mountImages = [];
let index = 0;
if (children) {
const childrenNames = Object.keys(children);
for (let i = 0; i < childrenNames.length; ++i) {
const name = childrenNames[i];
const child = children[name];
let selfDebugID = 0;
if (process.env.NODE_ENV !== 'production') {
selfDebugID = getDebugID(this);
}
const mountImage = ReactReconciler.mountComponent(
child,
transaction,
this,
this._hostContainerInfo,
context,
selfDebugID
);
// const mountImage = ReactReconciler.mountComponent(child, rootID, transaction, context);
child._mountIndex = index;
mountImages.push(mountImage);
index++;
}
}
if (process.env.NODE_ENV !== 'production') {
setChildrenForInstrumentation.call(this, children);
}
return mountImages;
}
moveChild(child, toIndex, lastIndex) {
if (child._mountIndex === toIndex) {
return;
}
this.threeElementDescriptor.moveChild(this._threeObject,
child._threeObject, toIndex, child._mountIndex);
const markup = this._markup;
_arrayMove(markup.childrenMarkup, lastIndex, toIndex);
}
receiveComponent(nextElement, transaction, context) {
// console.log('receive component');
const prevElement = this._currentElement;
this._currentElement = nextElement;
this.updateComponent(transaction, prevElement, nextElement, context);
if (this._forceRemountOfComponent) {
this._currentElement = null;
}
}
/**
* @see ReactDOMComponent.updateComponent
*
* Updates a DOM component after it has already been allocated and
* attached to the DOM. Reconciles the root DOM node, then recurses.
*
* @param {ReactReconcileTransaction} transaction
* @param {ReactElement} prevElement
* @param {ReactElement} nextElement
* @param context
* @internal
* @overridable
*/
updateComponent(transaction, prevElement, nextElement, context) {
const lastProps = prevElement.props;
const nextProps = this._currentElement.props;
if (prevElement.type !== nextElement.type) {
if (process.env.NODE_ENV !== 'production') {
invariant(false, 'The component type changed unexpectedly');
} else {
invariant(false);
}
}
this._updateObjectProperties(lastProps, nextProps, transaction, context);
if (!this._forceRemountOfComponent) {
this._updateChildrenObjects(nextProps, transaction, processChildContext(context, this));
}
}
// see _updateDOMChildren
_updateChildrenObjects(nextProps, transaction, context) {
const nextChildren = nextProps.children || null;
if (process.env.NODE_ENV !== 'production') {
setContentChildForInstrumentation.call(this, null);
}
this.updateChildren(nextChildren, transaction, context);
}
// original: _updateDOMProperties
_updateObjectProperties(lastProps, nextProps, transaction) {
const remountTrigger = this.remountTrigger;
remountTrigger.wantRemount = false;
this.threeElementDescriptor.beginPropertyUpdates(this._threeObject, nextProps);
if (process.env.NODE_ENV !== 'production') {
this.threeElementDescriptor.checkPropTypes(this._currentElement,
this._currentElement._owner, this._debugID, nextProps);
}
const lastPropKeys = Object.keys(lastProps);
// https://jsperf.com/object-keys-vs-for-in-with-closure/3
for (let i = 0; i < lastPropKeys.length; ++i) {
const propKey = lastPropKeys[i];
if (nextProps.hasOwnProperty(propKey)) {
continue;
}
if (propKey === 'children') {
continue;
}
if (remountTrigger.wantRemount) {
break;
}
if (registrationNameModules.hasOwnProperty(propKey)) {
if (lastProps[propKey]) {
// Only call deleteListener if there was a listener previously or
// else willDeleteListener gets called when there wasn't actually a
// listener (e.g., onClick={null})
deleteListener(this._rootNodeID, propKey);
}
} else {
this.threeElementDescriptor.deleteProperty(this._threeObject, propKey);
}
}
const nextPropKeys = Object.keys(nextProps);
for (let i = 0; i < nextPropKeys.length; ++i) {
const propKey = nextPropKeys[i];
if (propKey === 'children') {
continue;
}
if (remountTrigger.wantRemount) {
break;
}
const nextProp = nextProps[propKey];
const lastProp = lastProps[propKey];
if (nextProp === lastProp) {
continue;
}
if (registrationNameModules.hasOwnProperty(propKey)) {
if (nextProp) {
enqueuePutListener(this._rootNodeID, propKey, nextProp, transaction);
} else if (lastProp) {
deleteListener(this._rootNodeID, propKey);
}
} else {
this.threeElementDescriptor.updateProperty(this._threeObject, propKey, nextProp);
}
}
this.threeElementDescriptor.completePropertyUpdates(this._threeObject);
}
_removeAllChildRefs() {
const renderedChildren = this._renderedChildren;
if (renderedChildren) {
const renderedChildrenKeys = Object.keys(renderedChildren);
for (let i = 0; i < renderedChildrenKeys.length; ++i) {
const name = renderedChildrenKeys[i];
const renderedChild = renderedChildren[name];
if (renderedChild && renderedChild._currentElement && renderedChild._currentElement.ref) {
ReactRef.detachRefs(renderedChild, renderedChild._currentElement);
renderedChild._currentElement = ReactElement.cloneElement(renderedChild._currentElement, {
ref: null,
});
}
renderedChild._removeAllChildRefs();
}
}
}
/**
* @see ReactDOMComponent.Mixin.unmountComponent
*/
unmountComponent(safely) {
if (this._threeObject !== null) {
this.threeElementDescriptor.componentWillUnmount(this._threeObject);
}
if (this._forceRemountOfComponent) {
this._removeAllChildRefs(); // prevent attaching of refs to children
}
this.unmountChildren(safely);
React3ComponentTree.uncacheMarkup(this);
if (this._threeObject !== null) {
this.threeElementDescriptor.unmount(this._threeObject);
// delete this._threeObject.userData.markup;
}
this._markup = null;
this._rootNodeID = 0;
if (this._nodeWithLegacyProperties) {
const node = this._nodeWithLegacyProperties;
node._reactInternalComponent = null;
this._nodeWithLegacyProperties = null;
}
if (process.env.NODE_ENV !== 'production') {
setContentChildForInstrumentation.call(this, null);
}
}
emptyJson() {
debugger; // eslint-disable-line
return '...';
}
getPublicInstance() {
return this._markup.threeObject;
}
/**
* @see ReactMultiChildMixin._updateChildren
*
* Improve performance by isolating this hot code path from the try/catch
* block in `updateChildren`.
*
* @param {?object} nextNestedChildrenElements Nested child maps.
* @param {ReactReconcileTransaction} transaction
* @param {any} context
* @final
* @protected
*/
_updateChildren(nextNestedChildrenElements, transaction, context) {
const prevChildren = this._renderedChildren;
const removedMarkups = {};
const mountImages = [];
const nextChildren = this._reconcilerUpdateChildren(
prevChildren,
nextNestedChildrenElements,
mountImages,
removedMarkups,
transaction,
context
);
if (!nextChildren && !prevChildren) {
return;
}
const remountTrigger = this.remountTrigger;
remountTrigger.wantRemount = false;
this.threeElementDescriptor.beginChildUpdates(this._threeObject);
// `nextIndex` will increment for each child in `nextChildren`, but
// `lastIndex` will be the last index visited in `prevChildren`.
let nextIndex = 0;
let lastIndex = 0;
// `nextMountIndex` will increment for each newly mounted child.
let nextMountIndex = 0;
if (nextChildren) {
const nextChildrenNames = Object.keys(nextChildren);
for (let i = 0; i < nextChildrenNames.length; ++i) {
const childName = nextChildrenNames[i];
if (remountTrigger.wantRemount) {
// This component will be remounted, (see extrude geometry)
// No need to update children any more as they will also be remounted!
continue;
}
const prevChild = prevChildren && prevChildren[childName];
const nextChild = nextChildren[childName];
if (prevChild === nextChild) {
this.moveChild(prevChild, nextIndex, lastIndex);
lastIndex = Math.max(prevChild._mountIndex, lastIndex);
prevChild._mountIndex = nextIndex;
} else {
if (prevChild) {
// Update `lastIndex` before `_mountIndex` gets unset by unmounting.
lastIndex = Math.max(prevChild._mountIndex, lastIndex);
const removedChildMarkup = removedMarkups[childName];
// handle removal here to allow replacing of components that are expected to be present
// only once in the parent
invariant(!!removedChildMarkup, 'Removed markup map should contain this child');
delete removedMarkups[childName];
this._unmountChild(prevChild, removedChildMarkup);
}
if (!remountTrigger.wantRemount) {
// The remount can be triggered by unmountChild as well (see extrude geometry)
// The child must be instantiated before it's mounted.
this._mountChildAtIndex(
nextChild,
mountImages[nextMountIndex],
null,
nextIndex,
transaction,
context
);
nextMountIndex++;
}
}
nextIndex++;
}
}
const removedMarkupNames = Object.keys(removedMarkups);
for (let i = 0; i < removedMarkupNames.length; ++i) {
const removedMarkupName = removedMarkupNames[i];
this._unmountChild(prevChildren[removedMarkupName], removedMarkups[removedMarkupName]);
}
this._renderedChildren = nextChildren;
if (process.env.NODE_ENV !== 'production') {
setChildrenForInstrumentation.call(this, nextChildren);
}
this.threeElementDescriptor.completeChildUpdates(this._threeObject);
}
// afterNode unused
createChild(child, afterNode, mountImage) {
const mountIndex = child._mountIndex;
this._markup.childrenMarkup.splice(mountIndex, 0, mountImage);
mountImage.parentMarkup = this._markup;
this.threeElementDescriptor.addChild(this._threeObject, mountImage.threeObject, mountIndex);
const descriptorForChild = this._react3RendererInstance
.threeElementDescriptors[mountImage.elementType];
descriptorForChild.setParent(mountImage.threeObject, this._threeObject);
}
/**
* Removes a child component.
*
* @param {ReactComponent} child Child to remove.
* @param {*} markup The markup for the child.
* @protected
*/
removeChild(child, markup) { // eslint-disable-line no-unused-vars
if (process.env.NODE_ENV !== 'production') {
invariant(!!markup && !!markup.threeObject, 'The child markup to replace has no threeObject');
}
this.threeElementDescriptor.removeChild(this._threeObject, markup.threeObject);
if (child instanceof InternalComponent) {
child.threeElementDescriptor.removedFromParent(markup.threeObject);
} else if (child instanceof React3CompositeComponentWrapper) {
markup.threeObject.userData
.react3internalComponent
.threeElementDescriptor.removedFromParent(markup.threeObject);
} else if (process.env.NODE_ENV !== 'production') {
invariant(false, 'Cannot remove child because it is not a known component type');
} else {
invariant(false);
}
const childrenMarkup = this._markup.childrenMarkup;
for (let i = 0; i < childrenMarkup.length; i++) {
const childMarkup = childrenMarkup[i];
if (childMarkup.threeObject === markup.threeObject) {
childrenMarkup.splice(i, 1);
delete childMarkup.parentMarkup;
return;
}
}
if (process.env.NODE_ENV !== 'production') {
invariant(false, 'Trying to remove a child that is not mounted');
} else {
invariant(false);
}
}
updateChildren = ReactMultiChildMixin.updateChildren.bind(this);
_mountChildAtIndex = ReactMultiChildMixin._mountChildAtIndex.bind(this);
_unmountChild = ReactMultiChildMixin._unmountChild.bind(this);
unmountChildren = ReactMultiChildMixin.unmountChildren.bind(this);
}
module.exports = InternalComponent;