mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
[CS] Implement Some Stuff (#11390)
* 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
This commit is contained in:
+156
-83
@@ -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') {
|
||||
@@ -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<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)
|
||||
@@ -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);
|
||||
|
||||
+17
-10
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -25,7 +25,10 @@ describe('ReactNativeCS', () => {
|
||||
});
|
||||
|
||||
it('should be able to create and render a native component', () => {
|
||||
const RCTView = 'View';
|
||||
ReactNativeCS.render(<RCTView foo="test" />, 1);
|
||||
const CSView = 'View';
|
||||
var props = <CSView foo="test" />;
|
||||
var state = ReactNativeCS.getInitialState({});
|
||||
var stateUpdater = function() {};
|
||||
ReactNativeCS.render({props, state, stateUpdater});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow
|
||||
* @providesModule ReactNativeTypes
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
+9
@@ -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<Instance> = any;
|
||||
}
|
||||
|
||||
declare module 'CSStatefulComponent' {
|
||||
declare function exports(spec: any): any;
|
||||
}
|
||||
|
||||
@@ -154,7 +154,7 @@ const bundles = [
|
||||
moduleType: RENDERER,
|
||||
entry: 'react-cs-renderer',
|
||||
global: 'ReactCSRenderer',
|
||||
externals: [],
|
||||
externals: ['CSStatefulComponent'],
|
||||
featureFlags: 'react-cs-renderer/src/ReactNativeCSFeatureFlags',
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user