src/lib/React3Renderer.js
import * as THREE from 'three';
import reactElementWrapper from 'react/lib/ReactElement';
import ReactCurrentOwner from 'react/lib/ReactCurrentOwner';
import ReactComponent from 'react/lib/ReactComponent';
import KeyEscapeUtils from 'react/lib/KeyEscapeUtils';
import emptyObject from 'fbjs/lib/emptyObject';
import invariant from 'fbjs/lib/invariant';
import warning from 'fbjs/lib/warning';
import ReactInstanceMap from 'react-dom/lib/ReactInstanceMap';
import ReactReconciler from 'react-dom/lib/ReactReconciler';
import ReactUpdates from 'react-dom/lib/ReactUpdates';
import ReactUpdateQueue from 'react-dom/lib/ReactUpdateQueue';
import ReactInjection from 'react-dom/lib/ReactInjection';
import ReactReconcileTransaction from 'react-dom/lib/ReactReconcileTransaction';
import ReactDefaultBatchingStrategy from 'react-dom/lib/ReactDefaultBatchingStrategy';
import traverseAllChildren from 'react-dom/lib/traverseAllChildren';
import getHostComponentFromComposite from 'react-dom/lib/getHostComponentFromComposite';
import shouldUpdateReactComponent from 'react-dom/lib/shouldUpdateReactComponent';
import ReactInstrumentation from 'react-dom/lib/ReactInstrumentation';
import react3ContainerInfo from './React3ContainerInfo';
import EventDispatcher from './utils/EventDispatcher';
import InternalComponent from './InternalComponent';
import React3ComponentTree from './React3ComponentTree';
import ElementDescriptorContainer from './ElementDescriptorContainer';
import React3CompositeComponentWrapper from './React3CompositeComponentWrapper';
import ID_PROPERTY_NAME from './utils/idPropertyName';
import removeDevTool from './utils/removeDevTool';
let getDeclarationErrorAddendum;
let staticDebugIdHack;
let ReactComponentTreeHook;
if (process.env.NODE_ENV !== 'production') {
/* eslint-disable global-require */
if (!ReactComponentTreeHook) {
ReactComponentTreeHook = require('react/lib/ReactComponentTreeHook');
}
/* eslint-enable global-require */
}
if (process.env.NODE_ENV !== 'production') {
staticDebugIdHack = 0;
// prop type helpers
// the warnings for propTypes will not say <anonymous>.
// Some performance is sacrificed for this.
// TODO: could have an env variable to disable this?
if (!THREE._renamed) {
THREE._renamed = true;
THREE.Vector2.displayName = 'THREE.Vector2';
THREE.Vector3.displayName = 'THREE.Vector3';
THREE.Quaternion.displayName = 'THREE.Quaternion';
THREE.Color.displayName = 'THREE.Color';
THREE.Shape.displayName = 'THREE.Shape';
THREE.Euler.displayName = 'THREE.Euler';
THREE.Fog.displayName = 'THREE.Fog';
}
getDeclarationErrorAddendum = (owner) => {
if (owner) {
const name = owner.getName();
if (name) {
return ` Check the render method of \`${name}\`.`;
}
}
return '';
};
}
/**
* Unmounts a component and removes it from the DOM.
*
* @param {ReactComponent} instance React component instance.
* @param {*} container DOM element to unmount from.
* @param {bool} safely
* @final
* @internal
* @see {ReactMount.unmountComponentAtNode}
*/
function unmountComponentFromNode(instance, container, safely) {
if (process.env.NODE_ENV !== 'production') {
ReactInstrumentation.debugTool.onBeginFlush();
}
ReactReconciler.unmountComponent(instance, safely);
if (process.env.NODE_ENV !== 'production') {
ReactInstrumentation.debugTool.onEndFlush();
}
}
/* global __REACT_DEVTOOLS_GLOBAL_HOOK__ */
class TopLevelWrapper extends ReactComponent {
render() {
return this.props.child;
}
static isReactComponent = {};
static isReactTopLevelWrapper = true;
}
if (process.env.NODE_ENV !== 'production') {
TopLevelWrapper.displayName = 'TopLevelWrapper';
}
function internalGetID(markup) {
return (markup && markup[ID_PROPERTY_NAME]) || '';
}
// see ReactMount.js:getReactRootElementInContainer
/**
* @param {THREE.Object3D|HTMLCanvasElement} container That may contain
* a React component
* @return {?*} The markup that may have the reactRoot ID, or null.
*/
function getReactRootMarkupInContainer(container) {
if (!container) {
return null;
}
// in ReactMount this is container.firstChild.
return (container.userData && container.userData.markup
&& container.userData.markup.childrenMarkup[0]) || null;
}
/**
* Check if the type reference is a known internal type. I.e. not a user
* provided composite type.
*
* @param {function} type
* @return {boolean} Returns true if this is a valid internal type.
*/
function isInternalComponentType(type) {
return typeof type === 'function'
&& typeof type.prototype !== 'undefined'
&& typeof type.prototype.mountComponent === 'function'
&& typeof type.prototype.receiveComponent === 'function';
}
class React3Renderer {
// to be used by modules e.g. mouse input ( see examples )
static eventDispatcher = new EventDispatcher();
/**
* Returns the THREE.js object rendered by this element.
*
* @param {React.Component|THREE.Object3D|HTMLCanvasElement} componentOrElement
* @return {?THREE.Object3D} The root node of this element.
*/
static findTHREEObject(componentOrElement) {
if (process.env.NODE_ENV !== 'production') {
const owner = ReactCurrentOwner.current;
if (owner !== null) {
if (process.env.NODE_ENV !== 'production') {
warning(
owner._warnedAboutRefsInRender,
'%s is accessing findDOMNode inside its render(). ' +
'render() should be a pure function of props and state. It should ' +
'never access something that requires stale data from the previous ' +
'render, such as refs. Move this logic to componentDidMount and ' +
'componentDidUpdate instead.',
owner.getName() || 'A component'
);
}
owner._warnedAboutRefsInRender = true;
}
}
if (componentOrElement === null) {
return null;
}
if (componentOrElement instanceof THREE.Object3D ||
componentOrElement instanceof HTMLCanvasElement) {
return componentOrElement;
}
if (ReactInstanceMap.has(componentOrElement)) {
let instance = ReactInstanceMap.get(componentOrElement);
instance = getHostComponentFromComposite(instance);
return instance ? React3ComponentTree.getMarkupFromInstance(instance).threeObject : null;
}
if (!(componentOrElement.render === null || typeof componentOrElement.render !== 'function')) {
if (process.env.NODE_ENV !== 'production') {
invariant(false, 'Component (with keys: %s) contains `render` method ' +
'but is not mounted', Object.keys(componentOrElement));
} else {
invariant(false);
}
}
if (process.env.NODE_ENV !== 'production') {
invariant(false,
'Element appears to be neither ReactComponent, ' +
'a THREE.js object, nor a HTMLCanvasElement (keys: %s)',
Object.keys(componentOrElement));
} else {
invariant(false);
}
return null;
}
/**
* @see ReactChildReconciler.updateChildren
*
* Cloned because it uses
* @see React3Renderer.instantiateReactComponent
*
* Updates the rendered children and returns a new set of children.
*
* @param {?object} prevChildren Previously initialized set of children.
* @param {?object} nextChildren Flat child element maps.
* @param mountImages
* @param {?object} removedMarkups The map for removed nodes.
* @param {ReactReconcileTransaction} transaction
* @param hostParent
* @param hostContainerInfo
* @param {object} context
* @param selfDebugID
* @return {?object} A new set of child instances.
* @internal
*/
updateChildren(prevChildren,
nextChildren,
mountImages,
removedMarkups,
transaction,
hostParent,
hostContainerInfo,
context,
selfDebugID // 0 in production and for roots
) {
// We currently don't have a way to track moves here but if we use iterators
// instead of for..in we can zip the iterators and check if an item has
// moved.
// TODO: If nothing has changed, return the prevChildren object so that we
// can quickly bailout.
if (!nextChildren && !prevChildren) {
return null;
}
if (nextChildren) {
const nextChildrenKeys = Object.keys(nextChildren);
for (let i = 0; i < nextChildrenKeys.length; ++i) {
const childName = nextChildrenKeys[i];
const prevChild = prevChildren && prevChildren[childName];
const prevElement = prevChild && prevChild._currentElement;
const nextElement = nextChildren[childName];
if (prevChild !== null && prevChild !== undefined
&& shouldUpdateReactComponent(prevElement, nextElement)) {
ReactReconciler.receiveComponent(
prevChild, nextElement, transaction, context
);
if (prevChild._forceRemountOfComponent) {
removedMarkups[childName] = prevChild.getHostMarkup();
ReactReconciler.unmountComponent(prevChild, false);
const nextChildInstance = this.instantiateReactComponent(nextElement, true);
nextChildren[childName] = nextChildInstance;
// Creating mount image now ensures refs are resolved in right order
// (see https://github.com/facebook/react/pull/7101 for explanation).
const nextChildMountImage = ReactReconciler.mountComponent(
nextChildInstance,
transaction,
hostParent,
hostContainerInfo,
context,
selfDebugID
);
mountImages.push(nextChildMountImage);
} else {
nextChildren[childName] = prevChild;
}
} else {
if (prevChild) {
removedMarkups[childName] = prevChild.getHostMarkup();
ReactReconciler.unmountComponent(prevChild, false);
}
// The child must be instantiated before it's mounted.
const nextChildInstance = this.instantiateReactComponent(nextElement, true);
nextChildren[childName] = nextChildInstance;
// Creating mount image now ensures refs are resolved in right order
// (see https://github.com/facebook/react/pull/7101 for explanation).
const nextChildMountImage = ReactReconciler.mountComponent(
nextChildInstance,
transaction,
hostParent,
hostContainerInfo,
context,
selfDebugID /* parentDebugID */
);
mountImages.push(nextChildMountImage);
}
}
}
if (prevChildren) {
// Unmount children that are no longer present.
const prevChildrenKeys = Object.keys(prevChildren);
for (let i = 0; i < prevChildrenKeys.length; ++i) {
const childName = prevChildrenKeys[i];
if (!(nextChildren && nextChildren.hasOwnProperty(childName))) {
const prevChild = prevChildren[childName];
removedMarkups[childName] = prevChild.getHostMarkup();
ReactReconciler.unmountComponent(prevChild, false);
}
}
}
return nextChildren;
}
getElementDescriptor(name) {
return this.threeElementDescriptors[name];
}
constructor() {
this._instancesByReactRootID = {};
if (process.env.NODE_ENV !== 'production') {
this.rootMarkupsByReactRootID = {};
}
this.nextMountID = 1;
this.globalIdCounter = 1;
this.nextReactRootIndex = 0;
this.threeElementDescriptors = new ElementDescriptorContainer(this).descriptors;
this._highlightElement = document.createElement('div');
this._highlightCache = null;
if (process.env.NODE_ENV !== 'production') {
this._nextDebugID = 1;
this._debugIdPrefix = staticDebugIdHack++;
}
if (process.env.NODE_ENV !== 'production' || process.env.ENABLE_REACT_ADDON_HOOKS === 'true') {
this._agent = null;
this._onHideHighlightFromInspector = () => {
if (this._highlightCache && this._highlightCache
.threeObject.userData.react3internalComponent) {
const internalComponent = this._highlightCache
.threeObject.userData.react3internalComponent;
internalComponent.hideHighlight();
this._highlightCache = null;
}
};
this._onHighlightFromInspector = (highlightInfo) => {
if (highlightInfo.node === this._highlightElement) {
if (this._highlightCache && this._highlightCache
.threeObject.userData.react3internalComponent) {
const internalComponent = this._highlightCache
.threeObject.userData.react3internalComponent;
internalComponent.highlightComponent();
}
}
};
this._hookAgent = (agent) => {
this._agent = agent;
// agent.on('startInspecting', (...args) => {
// console.log('start inspecting?', args);
// });
// agent.on('setSelection', (...args) => {
// console.log('set selection?', args);
// });
// agent.on('selected', (...args) => {
// console.log('selected?', args);
// });
agent.on('highlight', this._onHighlightFromInspector);
agent.on('hideHighlight', this._onHideHighlightFromInspector);
// agent.on('highlightMany', (...args) => {
// console.log('highlightMany?', args);
// });
};
// Inject the runtime into a devtools global hook regardless of browser.
// Allows for debugging when the hook is injected on the page.
if (typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ !== 'undefined'
&& typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.inject === 'function') {
this._devToolsRendererDefinition = {
ComponentTree: {
getClosestInstanceFromNode(node) {
return React3ComponentTree.getClosestInstanceFromMarkup(node);
},
getNodeFromInstance(instInput) {
let inst = instInput;
// inst is an internal instance (but could be a composite)
while (inst._renderedComponent) {
inst = inst._renderedComponent;
}
if (inst) {
return React3ComponentTree.getMarkupFromInstance(inst);
}
return null;
},
},
Mount: this,
Reconciler: ReactReconciler,
TextComponent: InternalComponent,
};
const rendererListener = (info) => {
this._reactDevtoolsRendererId = info.id;
this._rendererListenerCleanup();
delete this._rendererListenerCleanup;
};
this._rendererListenerCleanup = __REACT_DEVTOOLS_GLOBAL_HOOK__
.sub('renderer', rendererListener);
__REACT_DEVTOOLS_GLOBAL_HOOK__.inject(this._devToolsRendererDefinition);
if (typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.reactDevtoolsAgent !== 'undefined'
&& __REACT_DEVTOOLS_GLOBAL_HOOK__.reactDevtoolsAgent) {
const agent = __REACT_DEVTOOLS_GLOBAL_HOOK__.reactDevtoolsAgent;
this._hookAgent(agent);
} else {
this._devtoolsCallbackCleanup = __REACT_DEVTOOLS_GLOBAL_HOOK__
.sub('react-devtools', (agent) => {
this._devtoolsCallbackCleanup();
this._hookAgent(agent);
});
}
}
}
}
/**
* @see ReactChildReconciler.instantiateChild
* Cloned because it uses
* @see React3Renderer.instantiateReactComponent
*
* @param childInstances
* @param child
* @param name
* @param selfDebugID
*/
instantiateChild = (childInstances, child, name, selfDebugID) => {
// We found a component instance.
const keyUnique = (childInstances[name] === undefined);
if (process.env.NODE_ENV !== 'production') {
if (!keyUnique) {
warning(
false,
'flattenChildren(...): Encountered two children with the same key, ' +
'`%s`. Child keys must be unique; when two children share a key, only ' +
'the first child will be used.%s',
KeyEscapeUtils.unescape(name),
ReactComponentTreeHook.getStackAddendumByID(selfDebugID)
);
}
}
if (child !== null && keyUnique) {
childInstances[name] = this.instantiateReactComponent(child, true);
}
};
/**
* @see ReactChildReconciler.instantiateChildren
* Cloned because it uses
* @see React3Renderer.instantiateChild
*
* 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} nestedChildNodes Nested child maps.
* @param transaction
* @param context
* @param selfDebugID
* @return {?object} A set of child instances.
* @internal
*/
instantiateChildren(nestedChildNodes,
transaction,
context,
selfDebugID // 0 in production and for roots
) {
if (nestedChildNodes === null) {
return null;
}
const childInstances = {};
if (process.env.NODE_ENV !== 'production') {
traverseAllChildren(
nestedChildNodes,
(childInsts, child, name) => this.instantiateChild(
childInsts,
child,
name,
selfDebugID
),
childInstances
);
} else {
traverseAllChildren(nestedChildNodes, this.instantiateChild, childInstances);
}
return childInstances;
}
containsChild(container, markup) {
const childrenMarkup = container.userData.markup.childrenMarkup;
for (let i = 0; i < childrenMarkup.length; i++) {
if (childrenMarkup[i] === markup) {
return true;
}
}
return false;
}
// DO NOT RENAME
// used by react devtools!
findNodeHandle = (instance) => {
const inst = React3ComponentTree.getRenderedHostOrTextFromComponent(instance);
if (!inst || !inst._threeObject) {
return null;
}
const markup = React3ComponentTree.getMarkupFromInstance(inst);
this._highlightCache = markup;
return this._highlightElement;
};
// used by react devtools
nativeTagToRootNodeID = () => 0;
hostTagToRootNodeID = () => 0;
_mountImageIntoNode(markup,
container,
instance,
shouldReuseMarkup,
transaction) { // eslint-disable-line no-unused-vars
// TODO try to do server-side rendering for THREE
if (!container.userData) {
// it has to be a HTMLCanvasElement I guess?
invariant(container instanceof HTMLCanvasElement,
'The root container can only be a THREE.js object ' +
'(with an userData property) or HTMLCanvasElement.');
container.userData = {
_createdByReact3: true,
};
}
const rootImage = markup;
const rootMarkup = {
threeObject: container,
parentMarkup: null,
childrenMarkup: [rootImage],
toJSON: () => '---MARKUP---',
};
Object.assign(container.userData, {
object3D: container,
toJSON: () => '---USERDATA---',
markup: rootMarkup,
});
rootImage.parentMarkup = rootMarkup;
const descriptorForChild = this.threeElementDescriptors[rootImage.elementType];
descriptorForChild.setParent(rootImage.threeObject, rootMarkup.threeObject);
// all objects now added can be marked as added to scene now!
rootImage.threeObject.mountedIntoRoot();
const firstChild = container.userData.markup.childrenMarkup[0];
React3ComponentTree.precacheMarkup(instance, firstChild);
if (process.env.NODE_ENV !== 'production') {
const hostInstance = React3ComponentTree.getInstanceFromMarkup(firstChild);
if (hostInstance._debugID !== 0) {
ReactInstrumentation.debugTool.onHostOperation({
instanceID: hostInstance._debugID,
type: 'mount',
payload: markup.toString(),
});
}
}
}
/**
*
* @param nextElement A react element
* @param container A canvas or a THREE.js object
* @param callback The callback function
* @returns {*}
*/
render(nextElement, container, callback) {
return this._renderSubtreeIntoContainer(null, nextElement, container, callback);
}
getHostRootInstanceInContainer(container) {
const rootMarkup = getReactRootMarkupInContainer(container);
const prevHostInstance = rootMarkup && React3ComponentTree.getInstanceFromMarkup(rootMarkup);
return prevHostInstance && !prevHostInstance._hostParent ? prevHostInstance : null;
}
getTopLevelWrapperInContainer(container) {
const root = this.getHostRootInstanceInContainer(container);
if (root) {
invariant(!!root._hostContainerInfo, 'Root should have native container info %s',
' but it does not');
}
return root ? root._hostContainerInfo._topLevelWrapper : null;
}
_renderSubtreeIntoContainer(parentComponent, nextElement, container, callback) {
if (!reactElementWrapper.isValidElement(nextElement)) {
if (process.env.NODE_ENV !== 'production') {
if (typeof nextElement === 'string') {
invariant(false, 'React3Renderer.render(): Invalid component element.%s',
' Instead of passing an element string, make sure to instantiate ' +
'it by passing it to React.createElement.');
} else if (typeof nextElement === 'function') {
invariant(false, 'React3Renderer.render(): Invalid component element.%s',
' Instead of passing a component class, make sure to instantiate ' +
'it by passing it to React.createElement.');
} else if (nextElement !== null && nextElement.props !== undefined) {
invariant(false, 'React3Renderer.render(): Invalid component element.%s',
' This may be caused by unintentionally loading two independent ' +
'copies of React.');
} else {
invariant(false, 'React3Renderer.render(): Invalid component element.');
}
} else {
invariant(false);
}
}
const nextWrappedElement = reactElementWrapper.createElement(
TopLevelWrapper,
{ child: nextElement }
);
let nextContext;
if (parentComponent) {
const parentInst = ReactInstanceMap.get(parentComponent);
nextContext = parentInst._processChildContext(parentInst._context);
} else {
nextContext = emptyObject;
}
const prevComponent = this.getTopLevelWrapperInContainer(container);
if (prevComponent) {
const prevWrappedElement = prevComponent._currentElement;
const prevElement = prevWrappedElement.props.child;
if (shouldUpdateReactComponent(prevElement, nextElement)) {
const publicInst = prevComponent._renderedComponent.getPublicInstance();
const updatedCallback = callback &&
(() => {
callback.call(publicInst);
});
this._updateRootComponent(prevComponent,
nextWrappedElement,
nextContext,
container,
updatedCallback);
return publicInst;
}
this.unmountComponentAtNode(container);
}
// aka first child
const reactRootMarkup = getReactRootMarkupInContainer(container);
const containerHasReactMarkup = reactRootMarkup && !!internalGetID(reactRootMarkup);
// containerHasNonRootReactChild not implemented
const shouldReuseMarkup = containerHasReactMarkup && !prevComponent;
const component = this._renderNewRootComponent(
nextWrappedElement,
container,
shouldReuseMarkup,
nextContext
)._renderedComponent.getPublicInstance();
if (callback) {
callback.call(component);
}
return component;
}
dispose() {
const rootIds = Object.keys(this._instancesByReactRootID);
for (let i = 0; i < rootIds.length; ++i) {
this.unmountComponentAtNode(this._instancesByReactRootID[rootIds[i]]
.getHostMarkup()
.parentMarkup
.threeObject);
}
delete this._instancesByReactRootID;
if (process.env.NODE_ENV !== 'production') {
delete this.rootMarkupsByReactRootID;
}
delete this._highlightElement;
this.nextMountID = 1;
this.nextReactRootIndex = 0;
if (process.env.NODE_ENV !== 'production' || process.env.ENABLE_REACT_ADDON_HOOKS === 'true') {
if (this._devtoolsCallbackCleanup) {
this._devtoolsCallbackCleanup();
delete this._devtoolsCallbackCleanup;
}
if (this._rendererListenerCleanup) {
this._rendererListenerCleanup();
delete this._rendererListenerCleanup;
}
if (this._devToolsRendererDefinition) {
if (this._agent) {
this._agent.onUnmounted(this._devToolsRendererDefinition);
this._agent.removeListener('highlight', this._onHighlightFromInspector);
this._agent.removeListener('hideHighlight', this._onHideHighlightFromInspector);
}
if (this._reactDevtoolsRendererId) {
delete __REACT_DEVTOOLS_GLOBAL_HOOK__._renderers[this._reactDevtoolsRendererId];
delete this._reactDevtoolsRendererId;
}
delete this._devToolsRendererDefinition;
delete this._agent;
}
delete this._onHighlightFromInspector;
delete this._onHideHighlightFromInspector;
delete this._hookAgent;
}
}
_updateRootComponent(prevComponent, nextElement, nextContext, container, callback) {
ReactUpdateQueue.enqueueElementInternal(prevComponent, nextElement, nextContext);
if (callback) {
ReactUpdateQueue.enqueueCallbackInternal(prevComponent, callback);
}
return prevComponent;
}
/**
* True if the supplied DOM node has a direct React-rendered child that is
* not a React root element. Useful for warning in `render`,
* `unmountComponentAtNode`, etc.
*
* @param {?*} container The container.
* @return {boolean} True if the DOM element contains a direct child that was
* rendered by React but is not a root element.
* @internal
*/
hasNonRootReactChild(container) {
const rootMarkup = getReactRootMarkupInContainer(container);
if (rootMarkup) {
const inst = React3ComponentTree.getInstanceFromMarkup(rootMarkup);
return !!(inst && inst._hostParent);
}
return false;
}
unmountComponentAtNode(container) {
// Various parts of our code (such as ReactCompositeComponent's
// _renderValidatedComponent) assume that calls to render aren't nested;
// verify that that's the case. (Strictly speaking, unmounting won't cause a
// render but we still don't expect to be in a render call here.)
if (process.env.NODE_ENV !== 'production') {
warning(
ReactCurrentOwner.current === null,
'unmountComponentAtNode(): Render methods should be a pure function ' +
'of props and state; triggering nested component updates from render ' +
'is not allowed. If necessary, trigger nested updates in ' +
'componentDidUpdate. Check the render method of %s.',
(ReactCurrentOwner.current && ReactCurrentOwner.current.getName()) ||
'ReactCompositeComponent'
);
}
const prevComponent = this.getTopLevelWrapperInContainer(container);
if (!prevComponent) {
// Check if the node being unmounted was rendered by React, but isn't a
// root node.
const containerHasNonRootReactChild = this.hasNonRootReactChild(container);
// Check if the container itself is a React root node.
const isContainerReactRoot = !!(
container
&& container.userData
&& container.userData.markup
&& container.userData.markup[ID_PROPERTY_NAME]
);
if (process.env.NODE_ENV !== 'production') {
warning(
!containerHasNonRootReactChild,
'unmountComponentAtNode(): The node you\'re attempting to unmount ' +
'was rendered by React and is not a top-level container. %s',
(
isContainerReactRoot ?
'You may have accidentally passed in a React root node instead ' +
'of its container.' :
'Instead, have the parent component update its state and ' +
'rerender in order to remove this component.'
)
);
}
return false;
}
delete this._instancesByReactRootID[prevComponent._instance.rootID];
ReactUpdates.batchedUpdates(
unmountComponentFromNode,
prevComponent,
container,
false
);
if (container && container.userData && container.userData._createdByReact3) {
delete container.userData;
}
return true;
}
/**
* @param {THREE.Object3D|HTMLCanvasElement} container THREE Object
* or HTML Canvas Element that may contain a React component.
* @return {?string} A "reactRoot" ID, if a React component is rendered.
*/
getReactRootID(container) {
const rootMarkup = getReactRootMarkupInContainer(container);
return rootMarkup && this.getID(rootMarkup);
}
// see instantiateReactComponent.js
/**
* @see #instantiateReactComponent
*
* Cloned because it uses
* @see InternalComponent
*
* @param _node ( from createElement )
* @param {boolean} shouldHaveDebugID
* @return {object} A new instance of the element's constructor.
*/
instantiateReactComponent(_node, shouldHaveDebugID) {
let instance;
const node = _node;
const isEmptyNode = (node === null || node === false);
if (isEmptyNode) {
// Create an object3D node so that empty components can be added anywhere
instance = new InternalComponent(
reactElementWrapper.createElement('object3D', {
visible: false,
}), this);
// original: instance = new ReactDOMEmptyComponent(this.instantiateReactComponent);
} else if (typeof node === 'object') {
const element = node;
if (!(element && (typeof element.type === 'function'
|| typeof element.type === 'string'))) {
if (process.env.NODE_ENV !== 'production') {
if (element.type == null) {
invariant(false, 'Element type is invalid:' +
' expected a string (for built-in components)' +
' or a class/function (for composite components)' +
' but got: %s.%s', element.type, getDeclarationErrorAddendum(element._owner));
} else {
invariant(false, 'Element type is invalid:' +
' expected a string (for built-in components)' +
' or a class/function (for composite components)' +
' but got: %s.%s', typeof element.type, getDeclarationErrorAddendum(element._owner));
}
} else if (element.type == null) {
invariant(element.type, getDeclarationErrorAddendum(element._owner));
} else {
invariant(typeof element.type, getDeclarationErrorAddendum(element._owner));
}
}
// Special case string values
if (typeof element.type === 'string') {
// original: instance = ReactHostComponent.createInternalComponent(element);
instance = new InternalComponent(element, this);
} else if (isInternalComponentType(element.type)) {
// This is temporarily available for custom components that are not string
// representations. I.e. ART. Once those are updated to use the string
// representation, we can drop this code path.
const Constructor = element.type;
instance = new Constructor(element);
// We renamed this. Allow the old name for compat. :(
if (!instance.getHostNode) {
instance.getHostNode = instance.getNativeNode;
}
} else {
instance = new React3CompositeComponentWrapper(element, this);
}
} else if (typeof node === 'string'
|| typeof node === 'number') {
// TODO create instance for text
if (process.env.NODE_ENV !== 'production') {
invariant(false, 'Encountered invalid React node of type %s : %s',
typeof node, node);
} else {
invariant(false);
}
} else if (process.env.NODE_ENV !== 'production') {
invariant(false, 'Encountered invalid React node of type %s', typeof element);
} else {
invariant(false);
}
if (process.env.NODE_ENV !== 'production') {
warning(
typeof instance.mountComponent === 'function' &&
typeof instance.receiveComponent === 'function' &&
typeof instance.getHostMarkup === 'function' &&
typeof instance.unmountComponent === 'function',
'Only React 3 Components can be mounted.'
);
}
// These two fields are used by the DOM and ART diffing algorithms
// respectively. Instead of using expandos on components, we should be
// storing the state needed by the diffing algorithms elsewhere.
instance._mountIndex = 0;
instance._mountImage = null;
if (process.env.NODE_ENV !== 'production') {
if (shouldHaveDebugID) {
const debugID = `r3r${this._debugIdPrefix}-${this._nextDebugID++}`;
instance._debugID = debugID;
} else {
instance._debugID = 0;
}
}
// Internal instances should fully constructed at this point, so they should
// not get any new fields added to them at this point.
if (process.env.NODE_ENV !== 'production') {
if (Object.preventExtensions) {
Object.preventExtensions(instance);
}
}
return instance;
}
/**
* @see ReactMount._renderNewRootComponent
*
* Cloned because it uses
* @see React3Renderer.instantiateReactComponent
*
* @param nextElement
* @param {THREE.Object3D | HTMLCanvasElement} container
* @param shouldReuseMarkup
* @param context
* @returns {*}
* @private
*/
_renderNewRootComponent(nextElement, container, shouldReuseMarkup, context) {
// Various parts of our code (such as ReactCompositeComponent's
// _renderValidatedComponent) assume that calls to render aren't nested;
// verify that that's the case.
if (process.env.NODE_ENV !== 'production') {
warning(ReactCurrentOwner.current === null,
'_renderNewRootComponent(): Render methods should be a pure function ' +
'of props and state; triggering nested component updates from ' +
'render is not allowed. If necessary, trigger nested updates in ' +
'componentDidUpdate. Check the render method of %s.',
(ReactCurrentOwner.current &&
ReactCurrentOwner.current.getName())
|| 'ReactCompositeComponent');
}
const componentInstance = this.instantiateReactComponent(nextElement, false);
if (!ReactUpdates.ReactReconcileTransaction) {
// If the ReactReconcileTransaction has not been injected
// let's just use the defaults from ReactMount.
ReactInjection.Updates.injectReconcileTransaction(ReactReconcileTransaction);
ReactInjection.Updates.injectBatchingStrategy(ReactDefaultBatchingStrategy);
}
let devToolRemoved;
if (process.env.NODE_ENV !== 'production') {
devToolRemoved = removeDevTool();
}
// The initial render is synchronous but any updates that happen during
// rendering, in componentWillMount or componentDidMount, will be batched
// according to the current batching strategy.
ReactUpdates.batchedUpdates(
this.batchedMountComponentIntoNode,
componentInstance,
container,
shouldReuseMarkup,
context
);
if (process.env.NODE_ENV !== 'production') {
if (devToolRemoved) {
removeDevTool.restore();
}
}
const wrapperID = componentInstance._instance.rootID;
this._instancesByReactRootID[wrapperID] = componentInstance;
return componentInstance;
}
/**
* Batched mount.
*
* @param {ReactComponent} componentInstance The instance to mount.
* @param {*} container Container.
* @param {boolean} shouldReuseMarkup If true, do not insert markup
* @param {*} context que?
*/
batchedMountComponentIntoNode = (componentInstance,
container,
shouldReuseMarkup,
context) => {
const transaction = ReactUpdates.ReactReconcileTransaction.getPooled(
!shouldReuseMarkup
);
transaction.perform(
this.mountComponentIntoNode,
null,
componentInstance,
container,
transaction,
shouldReuseMarkup,
context
);
ReactUpdates.ReactReconcileTransaction.release(transaction);
};
/**
* @see #mountComponentIntoNode
*
* Mounts this component and inserts it into the DOM.
*
* @param {ReactComponent} wrapperInstance The instance to mount.
* @param {*} container container to mount into.
* @param {ReactReconcileTransaction} transaction
* @param {boolean} shouldReuseMarkup If true, do not insert markup
* @param {*} context
*/
mountComponentIntoNode = (wrapperInstance,
container,
transaction,
shouldReuseMarkup,
context) => {
const markup = ReactReconciler.mountComponent(
wrapperInstance,
transaction,
null,
react3ContainerInfo(wrapperInstance, container),
context,
0 /* parentDebugID */
);
wrapperInstance._renderedComponent._topLevelWrapper = wrapperInstance;
this._mountImageIntoNode(
markup,
container,
wrapperInstance,
shouldReuseMarkup,
transaction
);
};
createReactRootID() {
return this.nextReactRootIndex++;
}
getID(markup) {
return internalGetID(markup);
}
}
module.exports = React3Renderer;