diff --git a/packages/react-cs-renderer/src/ReactNativeCS.js b/packages/react-cs-renderer/src/ReactNativeCS.js index a1751cc778..f0fa7dfec8 100644 --- a/packages/react-cs-renderer/src/ReactNativeCS.js +++ b/packages/react-cs-renderer/src/ReactNativeCS.js @@ -9,43 +9,44 @@ 'use strict'; -import type {ReactNativeCSType} from './ReactNativeCSTypes'; - +const {CSStatefulComponent} = require('CSStatefulComponent'); const ReactFiberReconciler = require('react-reconciler'); -// TODO: direct imports like some-package/src/* are bad. Fix me. +const ReactVersion = require('shared/ReactVersion'); + const { injectInternals, } = require('react-reconciler/src/ReactFiberDevToolsHook'); -const ReactGenericBatching = require('events/ReactGenericBatching'); -const ReactVersion = require('shared/ReactVersion'); -const emptyObject = require('fbjs/lib/emptyObject'); -export type Container = number; -export type Instance = number; -export type Props = Object; -export type TextInstance = number; +import type {ReactNodeList} from 'shared/ReactTypes'; +import type {ReactNativeCSType} from './ReactNativeCSTypes'; -function processProps(instance: number, props: Props): Object { - const propsPayload = {}; - for (var key in props) { - if (key === 'children') { - // Skip special case. - continue; - } - var value = props[key]; - if (typeof value === 'function') { - value = { - style: 'rt-event', - event: key, - tag: instance, - }; - } - propsPayload[key] = value; - } - return propsPayload; -} +const emptyObject = {}; -function arePropsEqual(oldProps: Props, newProps: Props): boolean { +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 => { var key; for (key in newProps) { if (key === 'children') { @@ -66,13 +67,28 @@ function arePropsEqual(oldProps: Props, newProps: Props): boolean { } } 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 {}, + ): void { + if (parentInstance.props) { + parentInstance.props.children.push(child); + } else { + // CSCustom + (parentInstance: any).children.push(child); + } + }, createInstance( type: string, @@ -81,7 +97,21 @@ const ReactNativeCSFiberRenderer = ReactFiberReconciler({ hostContext: {}, internalInstanceHandle: Object, ): Instance { - return 0; + 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( @@ -90,7 +120,8 @@ const ReactNativeCSFiberRenderer = ReactFiberReconciler({ hostContext: {}, internalInstanceHandle: Object, ): TextInstance { - return 0; + // Could auto-translate to CSText with some host context defined attributes. + throw new Error('Not yet implemented.'); }, finalizeInitialChildren( @@ -110,8 +141,8 @@ const ReactNativeCSFiberRenderer = ReactFiberReconciler({ return emptyObject; }, - getPublicInstance(instance) { - return instance; + getPublicInstance(instance: Instance) { + return instance.options.ref; }, prepareForCommit(): void {}, @@ -123,11 +154,11 @@ const ReactNativeCSFiberRenderer = ReactFiberReconciler({ newProps: Props, rootContainerInstance: Container, hostContext: {}, - ): null | Object { + ): null | InstanceProps { if (arePropsEqual(oldProps, newProps)) { return null; } - return processProps(instance, newProps); + return Object.assign({}, newProps); }, resetAfterCommit(): void {}, @@ -136,14 +167,19 @@ const ReactNativeCSFiberRenderer = ReactFiberReconciler({ return false; }, - scheduleDeferredCallback: global.requestIdleCallback, + scheduleDeferredCallback(callback) { + scheduledCallback = callback; + if (scheduleUpdate !== null) { + scheduleUpdate(identityUpdater); + } + }, shouldSetTextContent(type: string, props: Props): boolean { // TODO: Figure out when we should allow text content. return false; }, - useSyncScheduling: true, + useSyncScheduling: false, now(): number { // TODO: Enable expiration by implementing this method. @@ -153,7 +189,7 @@ const ReactNativeCSFiberRenderer = ReactFiberReconciler({ persistence: { cloneInstance( instance: Instance, - updatePayload: null | Object, + updatePayload: null | InstanceProps, type: string, oldProps: Props, newProps: Props, @@ -161,64 +197,52 @@ const ReactNativeCSFiberRenderer = ReactFiberReconciler({ keepChildren: boolean, recyclableInstance: null | Instance, ): Instance { - return 0; + 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, - ): Array { - return []; + createContainerChildSet(container: Container): Container { + // We'll only ever have one instance in the container. + container.pendingChild = null; + return container; }, appendChildToContainerChildSet( - childSet: Array, + childSet: Container, child: Instance | TextInstance, - ): void {}, + ): 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: Array, + newChildren: Container, ): void {}, replaceContainerChildren( container: Container, - newChildren: Array, + newChildren: Container, ): void {}, }, }); -const roots = new Map(); - -const ReactNativeCSFiber: ReactNativeCSType = { - render(element: React$Element, 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 = ReactNativeCSFiberRenderer.createContainer(containerTag, false); - roots.set(containerTag, root); - } - ReactNativeCSFiberRenderer.updateContainer(element, root, null, callback); - - return ReactNativeCSFiberRenderer.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? - ReactNativeCSFiberRenderer.updateContainer(null, root, null, () => { - roots.delete(containerTag); - }); - } - }, - - unstable_batchedUpdates: ReactGenericBatching.batchedUpdates, - - flushSync: ReactNativeCSFiberRenderer.flushSync, -}; - injectInternals({ findHostInstanceByFiber: ReactNativeCSFiberRenderer.findHostInstance, // This is an enum because we may add more (e.g. profiler build) @@ -227,4 +251,53 @@ injectInternals({ rendererPackageName: 'react-cs-renderer', }); -module.exports = ReactNativeCSFiber; +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. +}); + +module.exports = (ReactCS: ReactNativeCSType); diff --git a/packages/react-cs-renderer/src/ReactNativeCSTypes.js b/packages/react-cs-renderer/src/ReactNativeCSTypes.js index 6976804c17..2108215fb1 100644 --- a/packages/react-cs-renderer/src/ReactNativeCSTypes.js +++ b/packages/react-cs-renderer/src/ReactNativeCSTypes.js @@ -5,19 +5,26 @@ * LICENSE file in the root directory of this source tree. * * @flow + * @providesModule ReactNativeCSTypes */ 'use strict'; /** * Flat CS renderer bundles are too big for Flow to parse efficiently. - * Provide minimal Flow typing for the high-level RN API and call it a day. + * Provide minimal Flow typing for the high-level API and call it a day. */ -export type ReactNativeCSType = { - render( - element: React$Element, - containerTag: any, - callback: ?Function, - ): any, - unmountComponentAtNode(containerTag: number): any, - unstable_batchedUpdates: any, // TODO (bvaughn) Add types -}; + +import type {Options, Element} from 'CSComponent'; + +export type Children = {| + +children: $ReadOnlyArray>, +|}; + +type StatelessComponent = React$StatelessFunctionalComponent; + +type ClassComponent = Class & Instance>; + +export type ReactNativeCSType = ( + props: Children | StatelessComponent>, + options: Options | void, +) => Element; diff --git a/packages/react-cs-renderer/src/__mocks__/CSStatefulComponent.js b/packages/react-cs-renderer/src/__mocks__/CSStatefulComponent.js new file mode 100644 index 0000000000..8938cccdee --- /dev/null +++ b/packages/react-cs-renderer/src/__mocks__/CSStatefulComponent.js @@ -0,0 +1,14 @@ +/** + * 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. + */ + +'use strict'; + +// Mock of the CS Hooks + +exports.CSStatefulComponent = function(spec) { + return spec; +}; diff --git a/packages/react-cs-renderer/src/__tests__/ReactNativeCS-test.js b/packages/react-cs-renderer/src/__tests__/ReactNativeCS-test.js index 7545a72ea7..998a54107e 100644 --- a/packages/react-cs-renderer/src/__tests__/ReactNativeCS-test.js +++ b/packages/react-cs-renderer/src/__tests__/ReactNativeCS-test.js @@ -25,7 +25,10 @@ describe('ReactNativeCS', () => { }); it('should be able to create and render a native component', () => { - const RCTView = 'View'; - ReactNativeCS.render(, 1); + const CSView = 'View'; + var props = ; + var state = ReactNativeCS.getInitialState({}); + var stateUpdater = function() {}; + ReactNativeCS.render({props, state, stateUpdater}); }); }); diff --git a/packages/react-native-renderer/src/ReactNativeTypes.js b/packages/react-native-renderer/src/ReactNativeTypes.js index 59316b637a..1fc356f2ba 100644 --- a/packages/react-native-renderer/src/ReactNativeTypes.js +++ b/packages/react-native-renderer/src/ReactNativeTypes.js @@ -5,6 +5,7 @@ * LICENSE file in the root directory of this source tree. * * @flow + * @providesModule ReactNativeTypes */ 'use strict'; diff --git a/scripts/circleci/check_modules.sh b/scripts/circleci/check_modules.sh index 02a52a9609..b71b4cd616 100755 --- a/scripts/circleci/check_modules.sh +++ b/scripts/circleci/check_modules.sh @@ -3,7 +3,9 @@ set -e # Make sure we don't introduce accidental @providesModule annotations. -EXPECTED='scripts/rollup/header.js' +EXPECTED='packages/react-cs-renderer/src/ReactNativeCSTypes.js +packages/react-native-renderer/src/ReactNativeTypes.js +scripts/rollup/header.js' ACTUAL=$(git grep -l @providesModule -- './*.js' ':!scripts/rollup/shims/*.js') if [ "$EXPECTED" != "$ACTUAL" ]; then diff --git a/scripts/flow/react-native-host-hooks.js b/scripts/flow/react-native-host-hooks.js index d2b5ea71cb..709aad056c 100644 --- a/scripts/flow/react-native-host-hooks.js +++ b/scripts/flow/react-native-host-hooks.js @@ -110,3 +110,12 @@ declare module 'RTManager' { declare module 'BatchedBridge' { declare function registerCallableModule(name: string, module: Object): void; } + +declare module 'CSComponent' { + declare type Element = any; + declare type Options = any; +} + +declare module 'CSStatefulComponent' { + declare function exports(spec: any): any; +} diff --git a/scripts/rollup/bundles.js b/scripts/rollup/bundles.js index 2771287723..ee9a14290b 100644 --- a/scripts/rollup/bundles.js +++ b/scripts/rollup/bundles.js @@ -154,7 +154,7 @@ const bundles = [ moduleType: RENDERER, entry: 'react-cs-renderer', global: 'ReactCSRenderer', - externals: [], + externals: ['CSStatefulComponent'], featureFlags: 'react-cs-renderer/src/ReactNativeCSFeatureFlags', },