Skip to content

Commit

Permalink
[CS] Implement Some Stuff (facebook#11390)
Browse files Browse the repository at this point in the history
* Implement CS first take

This is using a pure JS API. This should probably switch to native hooks
at some later point but I'll start ironing out issues at this level first.

* Use async scheduling by default

The scheduled callback gets called immediately in render with infinite
time for now. Later this will be per root and abortable.

* Fix up the type signature of the ReactNativeCSType export

* Add escape hatch for special cased children

Working around the fact that we can't map arbitrary children slots. Just
the "children" prop.

* Readd providesModule for ReactNativeCSTypes

* Fix lint

* Fix ReactNativeTypes providesModule and CI check

* Special case a parent instance that doesn't have a props object

CSCustom can be anything here. Ugly but whatevs.

* Don't forget to store stateUpdater so that we can trigger updates

* Fix test
  • Loading branch information
sebmarkbage authored Oct 28, 2017
1 parent cc54b6f commit 696908f
Show file tree
Hide file tree
Showing 8 changed files with 206 additions and 97 deletions.
239 changes: 156 additions & 83 deletions packages/react-cs-renderer/src/ReactNativeCS.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<Instance>};
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') {
Expand All @@ -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,
Expand All @@ -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(
Expand All @@ -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(
Expand All @@ -110,8 +141,8 @@ const ReactNativeCSFiberRenderer = ReactFiberReconciler({
return emptyObject;
},

getPublicInstance(instance) {
return instance;
getPublicInstance(instance: Instance) {
return instance.options.ref;
},

prepareForCommit(): void {},
Expand All @@ -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 {},
Expand All @@ -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.
Expand All @@ -153,72 +189,60 @@ const ReactNativeCSFiberRenderer = ReactFiberReconciler({
persistence: {
cloneInstance(
instance: Instance,
updatePayload: null | Object,
updatePayload: null | InstanceProps,
type: string,
oldProps: Props,
newProps: Props,
internalInstanceHandle: Object,
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<Instance | TextInstance> {
return [];
createContainerChildSet(container: Container): Container {
// We'll only ever have one instance in the container.
container.pendingChild = null;
return container;
},

appendChildToContainerChildSet(
childSet: Array<Instance | TextInstance>,
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<Instance | TextInstance>,
newChildren: Container,
): void {},

replaceContainerChildren(
container: Container,
newChildren: Array<Instance | TextInstance>,
newChildren: Container,
): void {},
},
});

const roots = new Map();

const ReactNativeCSFiber: ReactNativeCSType = {
render(element: React$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 = 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)
Expand All @@ -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);
27 changes: 17 additions & 10 deletions packages/react-cs-renderer/src/ReactNativeCSTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<any>,
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<ChildType> = {|
+children: $ReadOnlyArray<React$Element<ChildType>>,
|};

type StatelessComponent<Props> = React$StatelessFunctionalComponent<Props>;

type ClassComponent<Props, Instance> = Class<React$Component<Props> & Instance>;

export type ReactNativeCSType = <Props, Instance>(
props: Children<ClassComponent<Props, Instance> | StatelessComponent<Props>>,
options: Options<Instance> | void,
) => Element;
14 changes: 14 additions & 0 deletions packages/react-cs-renderer/src/__mocks__/CSStatefulComponent.js
Original file line number Diff line number Diff line change
@@ -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;
};
Loading

0 comments on commit 696908f

Please sign in to comment.