react-native-macos
Version:
A framework for building native macOS apps using React
475 lines (399 loc) • 13.4 kB
JavaScript
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule ReactNativeFiber
* @flow
*/
;
const ReactFiberErrorLogger = require('ReactFiberErrorLogger');
const ReactFiberReconciler = require('ReactFiberReconciler');
const ReactGenericBatching = require('ReactGenericBatching');
const ReactNativeAttributePayload = require('ReactNativeAttributePayload');
const ReactNativeComponentTree = require('ReactNativeComponentTree');
const ReactNativeFiberErrorDialog = require('ReactNativeFiberErrorDialog');
const ReactNativeFiberHostComponent = require('ReactNativeFiberHostComponent');
const ReactNativeInjection = require('ReactNativeInjection');
const ReactNativeTagHandles = require('ReactNativeTagHandles');
const ReactNativeViewConfigRegistry = require('ReactNativeViewConfigRegistry');
const ReactPortal = require('ReactPortal');
const ReactVersion = require('ReactVersion');
const UIManager = require('UIManager');
const deepFreezeAndThrowOnMutationInDev = require('deepFreezeAndThrowOnMutationInDev');
const emptyObject = require('fbjs/lib/emptyObject');
const findNodeHandle = require('findNodeHandle');
const invariant = require('fbjs/lib/invariant');
const {injectInternals} = require('ReactFiberDevToolsHook');
import type {Element} from 'React';
import type {Fiber} from 'ReactFiber';
import type {
ReactNativeBaseComponentViewConfig,
} from 'ReactNativeViewConfigRegistry';
import type {ReactNodeList} from 'ReactTypes';
const {
precacheFiberNode,
uncacheFiberNode,
updateFiberProps,
} = ReactNativeComponentTree;
ReactNativeInjection.inject();
type Container = number;
export type Instance = {
_children: Array<Instance | number>,
_nativeTag: number,
viewConfig: ReactNativeBaseComponentViewConfig,
};
type Props = Object;
type TextInstance = number;
function recursivelyUncacheFiberNode(node: Instance | TextInstance) {
if (typeof node === 'number') {
// Leaf node (eg text)
uncacheFiberNode(node);
} else {
uncacheFiberNode((node: any)._nativeTag);
(node: any)._children.forEach(recursivelyUncacheFiberNode);
}
}
const NativeRenderer = ReactFiberReconciler({
appendChild(
parentInstance: Instance | Container,
child: Instance | TextInstance,
): void {
const childTag = typeof child === 'number' ? child : child._nativeTag;
if (typeof parentInstance === 'number') {
// Root container
UIManager.setChildren(
parentInstance, // containerTag
[childTag], // reactTags
);
} else {
const children = parentInstance._children;
const index = children.indexOf(child);
if (index >= 0) {
children.splice(index, 1);
children.push(child);
UIManager.manageChildren(
parentInstance._nativeTag, // containerTag
[index], // moveFromIndices
[children.length - 1], // moveToIndices
[], // addChildReactTags
[], // addAtIndices
[], // removeAtIndices
);
} else {
children.push(child);
UIManager.manageChildren(
parentInstance._nativeTag, // containerTag
[], // moveFromIndices
[], // moveToIndices
[childTag], // addChildReactTags
[children.length - 1], // addAtIndices
[], // removeAtIndices
);
}
}
},
appendInitialChild(
parentInstance: Instance,
child: Instance | TextInstance,
): void {
parentInstance._children.push(child);
},
commitTextUpdate(
textInstance: TextInstance,
oldText: string,
newText: string,
): void {
UIManager.updateView(
textInstance, // reactTag
'RCTRawText', // viewName
{text: newText}, // props
);
},
commitMount(
instance: Instance,
type: string,
newProps: Props,
internalInstanceHandle: Object,
): void {
// Noop
},
commitUpdate(
instance: Instance,
updatePayloadTODO: Object,
type: string,
oldProps: Props,
newProps: Props,
internalInstanceHandle: Object,
): void {
const viewConfig = instance.viewConfig;
updateFiberProps(instance._nativeTag, newProps);
const updatePayload = ReactNativeAttributePayload.diff(
oldProps,
newProps,
viewConfig.validAttributes,
);
UIManager.updateView(
instance._nativeTag, // reactTag
viewConfig.uiViewClassName, // viewName
updatePayload, // props
);
},
createInstance(
type: string,
props: Props,
rootContainerInstance: Container,
hostContext: {},
internalInstanceHandle: Object,
): Instance {
const tag = ReactNativeTagHandles.allocateTag();
const viewConfig = ReactNativeViewConfigRegistry.get(type);
if (__DEV__) {
for (const key in viewConfig.validAttributes) {
if (props.hasOwnProperty(key)) {
deepFreezeAndThrowOnMutationInDev(props[key]);
}
}
}
const updatePayload = ReactNativeAttributePayload.create(
props,
viewConfig.validAttributes,
);
UIManager.createView(
tag, // reactTag
viewConfig.uiViewClassName, // viewName
rootContainerInstance, // rootTag
updatePayload, // props
);
const component = new ReactNativeFiberHostComponent(tag, viewConfig);
precacheFiberNode(internalInstanceHandle, tag);
updateFiberProps(tag, props);
// Not sure how to avoid this cast. Flow is okay if the component is defined
// in the same file but if it's external it can't see the types.
return ((component: any): Instance);
},
createTextInstance(
text: string,
rootContainerInstance: Container,
hostContext: {},
internalInstanceHandle: Object,
): TextInstance {
const tag = ReactNativeTagHandles.allocateTag();
UIManager.createView(
tag, // reactTag
'RCTRawText', // viewName
rootContainerInstance, // rootTag
{text: text}, // props
);
precacheFiberNode(internalInstanceHandle, tag);
return tag;
},
finalizeInitialChildren(
parentInstance: Instance,
type: string,
props: Props,
rootContainerInstance: Container,
): boolean {
// Don't send a no-op message over the bridge.
if (parentInstance._children.length === 0) {
return false;
}
// Map from child objects to native tags.
// Either way we need to pass a copy of the Array to prevent it from being frozen.
const nativeTags = parentInstance._children.map(
child =>
(typeof child === 'number'
? child // Leaf node (eg text)
: child._nativeTag),
);
UIManager.setChildren(
parentInstance._nativeTag, // containerTag
nativeTags, // reactTags
);
return false;
},
getRootHostContext(): {} {
return emptyObject;
},
getChildHostContext(): {} {
return emptyObject;
},
getPublicInstance(instance) {
return instance;
},
insertBefore(
parentInstance: Instance | Container,
child: Instance | TextInstance,
beforeChild: Instance | TextInstance,
): void {
// TODO (bvaughn): Remove this check when...
// We create a wrapper object for the container in ReactNative render()
// Or we refactor to remove wrapper objects entirely.
// For more info on pros/cons see PR #8560 description.
invariant(
typeof parentInstance !== 'number',
'Container does not support insertBefore operation',
);
const children = (parentInstance: any)._children;
const index = children.indexOf(child);
// Move existing child or add new child?
if (index >= 0) {
children.splice(index, 1);
const beforeChildIndex = children.indexOf(beforeChild);
children.splice(beforeChildIndex, 0, child);
UIManager.manageChildren(
(parentInstance: any)._nativeTag, // containerID
[index], // moveFromIndices
[beforeChildIndex], // moveToIndices
[], // addChildReactTags
[], // addAtIndices
[], // removeAtIndices
);
} else {
const beforeChildIndex = children.indexOf(beforeChild);
children.splice(beforeChildIndex, 0, child);
const childTag = typeof child === 'number' ? child : child._nativeTag;
UIManager.manageChildren(
(parentInstance: any)._nativeTag, // containerID
[], // moveFromIndices
[], // moveToIndices
[childTag], // addChildReactTags
[beforeChildIndex], // addAtIndices
[], // removeAtIndices
);
}
},
prepareForCommit(): void {
// Noop
},
prepareUpdate(
instance: Instance,
type: string,
oldProps: Props,
newProps: Props,
rootContainerInstance: Container,
hostContext: {},
): null | Object {
return emptyObject;
},
removeChild(
parentInstance: Instance | Container,
child: Instance | TextInstance,
): void {
recursivelyUncacheFiberNode(child);
if (typeof parentInstance === 'number') {
UIManager.manageChildren(
parentInstance, // containerID
[], // moveFromIndices
[], // moveToIndices
[], // addChildReactTags
[], // addAtIndices
[0], // removeAtIndices
);
} else {
const children = parentInstance._children;
const index = children.indexOf(child);
children.splice(index, 1);
UIManager.manageChildren(
parentInstance._nativeTag, // containerID
[], // moveFromIndices
[], // moveToIndices
[], // addChildReactTags
[], // addAtIndices
[index], // removeAtIndices
);
}
},
resetAfterCommit(): void {
// Noop
},
resetTextContent(instance: Instance): void {
// Noop
},
shouldDeprioritizeSubtree(type: string, props: Props): boolean {
return false;
},
scheduleAnimationCallback: global.requestAnimationFrame,
scheduleDeferredCallback: global.requestIdleCallback,
shouldSetTextContent(props: Props): boolean {
// TODO (bvaughn) Revisit this decision.
// Always returning false simplifies the createInstance() implementation,
// But creates an additional child Fiber for raw text children.
// No additional native views are created though.
// It's not clear to me which is better so I'm deferring for now.
// More context @ github.com/facebook/react/pull/8560#discussion_r92111303
return false;
},
useSyncScheduling: true,
});
ReactGenericBatching.injection.injectFiberBatchedUpdates(
NativeRenderer.batchedUpdates,
);
const roots = new Map();
findNodeHandle.injection.injectFindNode((fiber: Fiber) =>
NativeRenderer.findHostInstance(fiber),
);
findNodeHandle.injection.injectFindRootNodeID(instance => instance);
// Intercept lifecycle errors and ensure they are shown with the correct stack
// trace within the native redbox component.
ReactFiberErrorLogger.injection.injectDialog(
ReactNativeFiberErrorDialog.showDialog,
);
const ReactNative = {
// External users of findNodeHandle() expect the host tag number return type.
// The injected findNodeHandle() strategy returns the instance wrapper though.
// See NativeMethodsMixin#setNativeProps for more info on why this is done.
findNodeHandle(componentOrHandle: any): ?number {
const instance: any = findNodeHandle(componentOrHandle);
if (instance == null || typeof instance === 'number') {
return instance;
}
return instance._nativeTag;
},
render(element: Element<any>, containerTag: any, callback: ?Function) {
let root = roots.get(containerTag);
if (!root) {
// TODO (bvaughn): If we decide to keep the wrapper component,
// We could create a wrapper for containerTag as well to reduce special casing.
root = NativeRenderer.createContainer(containerTag);
roots.set(containerTag, root);
}
NativeRenderer.updateContainer(element, root, null, callback);
return NativeRenderer.getPublicRootInstance(root);
},
unmountComponentAtNode(containerTag: number) {
const root = roots.get(containerTag);
if (root) {
// TODO: Is it safe to reset this now or should I wait since this unmount could be deferred?
NativeRenderer.updateContainer(null, root, null, () => {
roots.delete(containerTag);
});
}
},
unmountComponentAtNodeAndRemoveContainer(containerTag: number) {
ReactNative.unmountComponentAtNode(containerTag);
// Call back into native to remove all of the subviews from this container
UIManager.removeRootView(containerTag);
},
unstable_createPortal(
children: ReactNodeList,
containerTag: number,
key: ?string = null,
) {
return ReactPortal.createPortal(children, containerTag, null, key);
},
unstable_batchedUpdates: ReactGenericBatching.batchedUpdates,
};
if (typeof injectInternals === 'function') {
injectInternals({
findFiberByHostInstance: ReactNativeComponentTree.getClosestInstanceFromNode,
findHostInstanceByFiber: NativeRenderer.findHostInstance,
// This is an enum because we may add more (e.g. profiler build)
bundleType: __DEV__ ? 1 : 0,
version: ReactVersion,
});
}
module.exports = ReactNative;