/** * 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 type {ReactNodeList} from 'shared/ReactTypes'; import type {ReactNativeCSType} from './ReactNativeCSTypes'; // Provided by CS: import {CSStatefulComponent} from 'CSStatefulComponent'; import ReactFiberReconciler from 'react-reconciler'; import ReactVersion from 'shared/ReactVersion'; const emptyObject = {}; type Container = { pendingChild: null | Instance | TextInstance, }; type InstanceProps = Props & {children: Array}; type Instance = { props: InstanceProps, options: {key: string, ref: null}, data: {type: 'NATIVE', name: string}, }; type Props = Object; type TextInstance = Instance; // We currently don't actually return a new state. We only use state updaters to trigger a // rerender. Therefore our state updater is the identity functions. When we later deal // with sync scheduling and aborted renders, we will need to update the state in render. const identityUpdater = state => state; // We currently don't have a hook for aborting render. Will add one once it is in place // in React Native proper. const infiniteDeadline = { timeRemaining: function() { return Infinity; }, }; const arePropsEqual = (oldProps: Props, newProps: Props): boolean => { let key; for (key in newProps) { if (key === 'children') { // Skip special case. continue; } if (newProps[key] !== oldProps[key]) { return false; } } for (key in oldProps) { if (key === 'children') { // Skip special case. continue; } if (!(key in newProps)) { return false; } } return true; }; // React doesn't expose its full keypath. To manage lifetime of instances, we instead use IDs. let nextComponentKey = 0; // Callback. Currently this is global. TODO: This should be per root. let scheduledCallback = null; // Updater. This is the CS updater we use to trigger the update. TODO: This should be per root. let scheduleUpdate = null; const ReactNativeCSFiberRenderer = ReactFiberReconciler({ appendInitialChild( parentInstance: Instance, child: Instance | TextInstance, ): void { if (parentInstance.props) { parentInstance.props.children.push(child); } else { // CSCustom (parentInstance: any).children.push(child); } }, createInstance( type: string, props: Props, rootContainerInstance: Container, hostContext: {}, internalInstanceHandle: Object, ): Instance { let key = '' + nextComponentKey++; let ref = null; // TODO: Always create Ref object so that getPublicInstance can use it. // We need a new props object so that we can represent flattened children. let newProps = Object.assign({}, props); newProps.children = []; if (type === 'CSCustom') { // Special cased type that treats the props as the object. // Useful for custom children types like FlexItem. return newProps; } return { props: newProps, options: {key, ref}, data: {type: 'NATIVE', name: type}, }; }, createTextInstance( text: string, rootContainerInstance: Container, hostContext: {}, internalInstanceHandle: Object, ): TextInstance { // Could auto-translate to CSText with some host context defined attributes. throw new Error('Not yet implemented.'); }, finalizeInitialChildren( parentInstance: Instance, type: string, props: Props, rootContainerInstance: Container, ): boolean { return false; }, getRootHostContext(): {} { return emptyObject; }, getChildHostContext(): {} { return emptyObject; }, getPublicInstance(instance: Instance) { return instance.options.ref; }, prepareForCommit(): void {}, prepareUpdate( instance: Instance, type: string, oldProps: Props, newProps: Props, rootContainerInstance: Container, hostContext: {}, ): null | InstanceProps { if (arePropsEqual(oldProps, newProps)) { return null; } return Object.assign({}, newProps); }, resetAfterCommit(): void {}, shouldDeprioritizeSubtree(type: string, props: Props): boolean { return false; }, scheduleDeferredCallback(callback) { scheduledCallback = callback; if (scheduleUpdate !== null) { scheduleUpdate(identityUpdater); } return 0; }, cancelDeferredCallback() { // Noop. This is always called right before scheduling a new update, so // should be fine. This renderer won't use requestIdleCallback, anyway. // Will switch to use shouldYield() API instead. }, shouldSetTextContent(type: string, props: Props): boolean { // TODO: Figure out when we should allow text content. return false; }, useSyncScheduling: false, now(): number { // TODO: Enable expiration by implementing this method. return 0; }, persistence: { cloneInstance( instance: Instance, updatePayload: null | InstanceProps, type: string, oldProps: Props, newProps: Props, internalInstanceHandle: Object, keepChildren: boolean, recyclableInstance: null | Instance, ): Instance { let newInstanceProps = updatePayload; if (newInstanceProps === null) { newInstanceProps = Object.assign({}, newProps); } // We need a new props object so that we can represent flattened children. newInstanceProps.children = keepChildren ? instance.props.children : []; if (type === 'CSCustom') { return newInstanceProps; } return { props: newInstanceProps, options: instance.options, data: instance.data, }; }, createContainerChildSet(container: Container): Container { // We'll only ever have one instance in the container. container.pendingChild = null; return container; }, appendChildToContainerChildSet( childSet: Container, child: Instance | TextInstance, ): void { if (childSet.pendingChild !== null) { throw new Error( 'CSReact does not support top level fragments. Wrap it in a primitve.', ); } childSet.pendingChild = child; }, finalizeContainerChildren( container: Container, newChildren: Container, ): void {}, replaceContainerChildren( container: Container, newChildren: Container, ): void {}, }, }); ReactNativeCSFiberRenderer.injectIntoDevTools({ bundleType: __DEV__ ? 1 : 0, version: ReactVersion, rendererPackageName: 'react-cs-renderer', }); type ReactCSProps = {children: ReactNodeList}; type ReactCSState = { root: Object, container: { pendingChild: null | Instance | TextInstance, }, }; const ReactCS = CSStatefulComponent({ getInitialState({props}: {props: ReactCSProps}): ReactCSState { let container = { pendingChild: null, }; let root = ReactNativeCSFiberRenderer.createContainer(container, false); return {root, container}; }, render({ props, state, stateUpdater, }: { props: ReactCSProps, state: ReactCSState, stateUpdater: (update: (oldState: ReactCSState) => ReactCSState) => void, }) { scheduleUpdate = stateUpdater; // TODO: For a props rerender updateContainer will schedule an additional state // update even though it is not necessary since we're already rendering. // We should only call scheduleUpdate for a React setState, not a top level // props update. ReactNativeCSFiberRenderer.updateContainer( props.children, state.root, null, null, ); if (scheduledCallback) { const callback = scheduledCallback; scheduledCallback = null; callback(infiniteDeadline); } return state.container.pendingChild; }, getInstance({state}: {state: ReactCSState}) { return ReactNativeCSFiberRenderer.getPublicRootInstance(state.root); }, // TODO: Unmount hook. E.g. finalizer. }); export default (ReactCS: ReactNativeCSType);