@samitouri / QOS-React-2 / commits / 696908f496

[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

Sebastian Markbåge committed Oct 27, 2017 at 20:05 UTC 696908f4961ef47c4cb2ce2ecf990693d7e2e945
8 files changed +206 -97
packages/react-cs-renderer/src/ReactNativeCS.js
+156 -83
@@ -9,43 +9,44 @@
9
10 'use strict';
11
12 -import type {ReactNativeCSType} from './ReactNativeCSTypes';
13 -
12 +const {CSStatefulComponent} = require('CSStatefulComponent');
13 const ReactFiberReconciler = require('react-reconciler');
15 -// TODO: direct imports like some-package/src/* are bad. Fix me.
14 +const ReactVersion = require('shared/ReactVersion');
15 +
16 const {
17 injectInternals,
18 } = require('react-reconciler/src/ReactFiberDevToolsHook');
19 -const ReactGenericBatching = require('events/ReactGenericBatching');
20 -const ReactVersion = require('shared/ReactVersion');
21 -const emptyObject = require('fbjs/lib/emptyObject');
19
23 -export type Container = number;
24 -export type Instance = number;
25 -export type Props = Object;
26 -export type TextInstance = number;
20 +import type {ReactNodeList} from 'shared/ReactTypes';
21 +import type {ReactNativeCSType} from './ReactNativeCSTypes';
22
28 -function processProps(instance: number, props: Props): Object {
29 - const propsPayload = {};
30 - for (var key in props) {
31 - if (key === 'children') {
32 - // Skip special case.
33 - continue;
34 - }
35 - var value = props[key];
36 - if (typeof value === 'function') {
37 - value = {
38 - style: 'rt-event',
39 - event: key,
40 - tag: instance,
41 - };
42 - }
43 - propsPayload[key] = value;
44 - }
45 - return propsPayload;
46 -}
23 +const emptyObject = {};
24
48 -function arePropsEqual(oldProps: Props, newProps: Props): boolean {
25 +type Container = {
26 + pendingChild: null | Instance | TextInstance,
27 +};
28 +type InstanceProps = Props & {children: Array<Instance>};
29 +type Instance = {
30 + props: InstanceProps,
31 + options: {key: string, ref: null},
32 + data: {type: 'NATIVE', name: string},
33 +};
34 +type Props = Object;
35 +type TextInstance = Instance;
36 +
37 +// We currently don't actually return a new state. We only use state updaters to trigger a
38 +// rerender. Therefore our state updater is the identity functions. When we later deal
39 +// with sync scheduling and aborted renders, we will need to update the state in render.
40 +const identityUpdater = state => state;
41 +// We currently don't have a hook for aborting render. Will add one once it is in place
42 +// in React Native proper.
43 +const infiniteDeadline = {
44 + timeRemaining: function() {
45 + return Infinity;
46 + },
47 +};
48 +
49 +const arePropsEqual = (oldProps: Props, newProps: Props): boolean => {
50 var key;
51 for (key in newProps) {
52 if (key === 'children') {
@@ -66,13 +67,28 @@ function arePropsEqual(oldProps: Props, newProps: Props): boolean {
67 }
68 }
69 return true;
69 -}
70 +};
71 +
72 +// React doesn't expose its full keypath. To manage lifetime of instances, we instead use IDs.
73 +let nextComponentKey = 0;
74 +
75 +// Callback. Currently this is global. TODO: This should be per root.
76 +let scheduledCallback = null;
77 +// Updater. This is the CS updater we use to trigger the update. TODO: This should be per root.
78 +let scheduleUpdate = null;
79
80 const ReactNativeCSFiberRenderer = ReactFiberReconciler({
81 appendInitialChild(
82 parentInstance: Instance,
83 child: Instance | TextInstance,
75 - ): void {},
84 + ): void {
85 + if (parentInstance.props) {
86 + parentInstance.props.children.push(child);
87 + } else {
88 + // CSCustom
89 + (parentInstance: any).children.push(child);
90 + }
91 + },
92
93 createInstance(
94 type: string,
@@ -81,7 +97,21 @@ const ReactNativeCSFiberRenderer = ReactFiberReconciler({
97 hostContext: {},
98 internalInstanceHandle: Object,
99 ): Instance {
84 - return 0;
100 + let key = '' + nextComponentKey++;
101 + let ref = null; // TODO: Always create Ref object so that getPublicInstance can use it.
102 + // We need a new props object so that we can represent flattened children.
103 + let newProps = Object.assign({}, props);
104 + newProps.children = [];
105 + if (type === 'CSCustom') {
106 + // Special cased type that treats the props as the object.
107 + // Useful for custom children types like FlexItem.
108 + return newProps;
109 + }
110 + return {
111 + props: newProps,
112 + options: {key, ref},
113 + data: {type: 'NATIVE', name: type},
114 + };
115 },
116
117 createTextInstance(
@@ -90,7 +120,8 @@ const ReactNativeCSFiberRenderer = ReactFiberReconciler({
120 hostContext: {},
121 internalInstanceHandle: Object,
122 ): TextInstance {
93 - return 0;
123 + // Could auto-translate to CSText with some host context defined attributes.
124 + throw new Error('Not yet implemented.');
125 },
126
127 finalizeInitialChildren(
@@ -110,8 +141,8 @@ const ReactNativeCSFiberRenderer = ReactFiberReconciler({
141 return emptyObject;
142 },
143
113 - getPublicInstance(instance) {
114 - return instance;
144 + getPublicInstance(instance: Instance) {
145 + return instance.options.ref;
146 },
147
148 prepareForCommit(): void {},
@@ -123,11 +154,11 @@ const ReactNativeCSFiberRenderer = ReactFiberReconciler({
154 newProps: Props,
155 rootContainerInstance: Container,
156 hostContext: {},
126 - ): null | Object {
157 + ): null | InstanceProps {
158 if (arePropsEqual(oldProps, newProps)) {
159 return null;
160 }
130 - return processProps(instance, newProps);
161 + return Object.assign({}, newProps);
162 },
163
164 resetAfterCommit(): void {},
@@ -136,14 +167,19 @@ const ReactNativeCSFiberRenderer = ReactFiberReconciler({
167 return false;
168 },
169
139 - scheduleDeferredCallback: global.requestIdleCallback,
170 + scheduleDeferredCallback(callback) {
171 + scheduledCallback = callback;
172 + if (scheduleUpdate !== null) {
173 + scheduleUpdate(identityUpdater);
174 + }
175 + },
176
177 shouldSetTextContent(type: string, props: Props): boolean {
178 // TODO: Figure out when we should allow text content.
179 return false;
180 },
181
146 - useSyncScheduling: true,
182 + useSyncScheduling: false,
183
184 now(): number {
185 // TODO: Enable expiration by implementing this method.
@@ -153,7 +189,7 @@ const ReactNativeCSFiberRenderer = ReactFiberReconciler({
189 persistence: {
190 cloneInstance(
191 instance: Instance,
156 - updatePayload: null | Object,
192 + updatePayload: null | InstanceProps,
193 type: string,
194 oldProps: Props,
195 newProps: Props,
@@ -161,64 +197,52 @@ const ReactNativeCSFiberRenderer = ReactFiberReconciler({
197 keepChildren: boolean,
198 recyclableInstance: null | Instance,
199 ): Instance {
164 - return 0;
200 + let newInstanceProps = updatePayload;
201 + if (newInstanceProps === null) {
202 + newInstanceProps = Object.assign({}, newProps);
203 + }
204 + // We need a new props object so that we can represent flattened children.
205 + newInstanceProps.children = keepChildren ? instance.props.children : [];
206 + if (type === 'CSCustom') {
207 + return newInstanceProps;
208 + }
209 + return {
210 + props: newInstanceProps,
211 + options: instance.options,
212 + data: instance.data,
213 + };
214 },
215
167 - createContainerChildSet(
168 - container: Container,
169 - ): Array<Instance | TextInstance> {
170 - return [];
216 + createContainerChildSet(container: Container): Container {
217 + // We'll only ever have one instance in the container.
218 + container.pendingChild = null;
219 + return container;
220 },
221
222 appendChildToContainerChildSet(
174 - childSet: Array<Instance | TextInstance>,
223 + childSet: Container,
224 child: Instance | TextInstance,
176 - ): void {},
225 + ): void {
226 + if (childSet.pendingChild !== null) {
227 + throw new Error(
228 + 'CSReact does not support top level fragments. Wrap it in a primitve.',
229 + );
230 + }
231 + childSet.pendingChild = child;
232 + },
233
234 finalizeContainerChildren(
235 container: Container,
180 - newChildren: Array<Instance | TextInstance>,
236 + newChildren: Container,
237 ): void {},
238
239 replaceContainerChildren(
240 container: Container,
185 - newChildren: Array<Instance | TextInstance>,
241 + newChildren: Container,
242 ): void {},
243 },
244 });
245
190 -const roots = new Map();
191 -
192 -const ReactNativeCSFiber: ReactNativeCSType = {
193 - render(element: React$Element<any>, containerTag: any, callback: ?Function) {
194 - let root = roots.get(containerTag);
195 -
196 - if (!root) {
197 - // TODO (bvaughn): If we decide to keep the wrapper component,
198 - // We could create a wrapper for containerTag as well to reduce special casing.
199 - root = ReactNativeCSFiberRenderer.createContainer(containerTag, false);
200 - roots.set(containerTag, root);
201 - }
202 - ReactNativeCSFiberRenderer.updateContainer(element, root, null, callback);
203 -
204 - return ReactNativeCSFiberRenderer.getPublicRootInstance(root);
205 - },
206 -
207 - unmountComponentAtNode(containerTag: number) {
208 - const root = roots.get(containerTag);
209 - if (root) {
210 - // TODO: Is it safe to reset this now or should I wait since this unmount could be deferred?
211 - ReactNativeCSFiberRenderer.updateContainer(null, root, null, () => {
212 - roots.delete(containerTag);
213 - });
214 - }
215 - },
216 -
217 - unstable_batchedUpdates: ReactGenericBatching.batchedUpdates,
218 -
219 - flushSync: ReactNativeCSFiberRenderer.flushSync,
220 -};
221 -
246 injectInternals({
247 findHostInstanceByFiber: ReactNativeCSFiberRenderer.findHostInstance,
248 // This is an enum because we may add more (e.g. profiler build)
@@ -227,4 +251,53 @@ injectInternals({
251 rendererPackageName: 'react-cs-renderer',
252 });
253
230 -module.exports = ReactNativeCSFiber;
254 +type ReactCSProps = {children: ReactNodeList};
255 +type ReactCSState = {
256 + root: Object,
257 + container: {
258 + pendingChild: null | Instance | TextInstance,
259 + },
260 +};
261 +
262 +const ReactCS = CSStatefulComponent({
263 + getInitialState({props}: {props: ReactCSProps}): ReactCSState {
264 + let container = {
265 + pendingChild: null,
266 + };
267 + let root = ReactNativeCSFiberRenderer.createContainer(container, false);
268 + return {root, container};
269 + },
270 + render({
271 + props,
272 + state,
273 + stateUpdater,
274 + }: {
275 + props: ReactCSProps,
276 + state: ReactCSState,
277 + stateUpdater: (update: (oldState: ReactCSState) => ReactCSState) => void,
278 + }) {
279 + scheduleUpdate = stateUpdater;
280 + // TODO: For a props rerender updateContainer will schedule an additional state
281 + // update even though it is not necessary since we're already rendering.
282 + // We should only call scheduleUpdate for a React setState, not a top level
283 + // props update.
284 + ReactNativeCSFiberRenderer.updateContainer(
285 + props.children,
286 + state.root,
287 + null,
288 + null,
289 + );
290 + if (scheduledCallback) {
291 + const callback = scheduledCallback;
292 + scheduledCallback = null;
293 + callback(infiniteDeadline);
294 + }
295 + return state.container.pendingChild;
296 + },
297 + getInstance({state}: {state: ReactCSState}) {
298 + return ReactNativeCSFiberRenderer.getPublicRootInstance(state.root);
299 + },
300 + // TODO: Unmount hook. E.g. finalizer.
301 +});
302 +
303 +module.exports = (ReactCS: ReactNativeCSType);
packages/react-cs-renderer/src/ReactNativeCSTypes.js
+17 -10
@@ -5,19 +5,26 @@
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @flow
8 + * @providesModule ReactNativeCSTypes
9 */
10 'use strict';
11
12 /**
13 * Flat CS renderer bundles are too big for Flow to parse efficiently.
13 - * Provide minimal Flow typing for the high-level RN API and call it a day.
14 + * Provide minimal Flow typing for the high-level API and call it a day.
15 */
15 -export type ReactNativeCSType = {
16 - render(
17 - element: React$Element<any>,
18 - containerTag: any,
19 - callback: ?Function,
20 - ): any,
21 - unmountComponentAtNode(containerTag: number): any,
22 - unstable_batchedUpdates: any, // TODO (bvaughn) Add types
23 -};
16 +
17 +import type {Options, Element} from 'CSComponent';
18 +
19 +export type Children<ChildType> = {|
20 + +children: $ReadOnlyArray<React$Element<ChildType>>,
21 +|};
22 +
23 +type StatelessComponent<Props> = React$StatelessFunctionalComponent<Props>;
24 +
25 +type ClassComponent<Props, Instance> = Class<React$Component<Props> & Instance>;
26 +
27 +export type ReactNativeCSType = <Props, Instance>(
28 + props: Children<ClassComponent<Props, Instance> | StatelessComponent<Props>>,
29 + options: Options<Instance> | void,
30 +) => Element;
packages/react-cs-renderer/src/__mocks__/CSStatefulComponent.js new
+14
@@ -0,0 +1,14 @@
1 +/**
2 + * Copyright (c) 2013-present, Facebook, Inc.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + */
7 +
8 +'use strict';
9 +
10 +// Mock of the CS Hooks
11 +
12 +exports.CSStatefulComponent = function(spec) {
13 + return spec;
14 +};
packages/react-cs-renderer/src/__tests__/ReactNativeCS-test.js
+5 -2
@@ -25,7 +25,10 @@ describe('ReactNativeCS', () => {
25 });
26
27 it('should be able to create and render a native component', () => {
28 - const RCTView = 'View';
29 - ReactNativeCS.render(<RCTView foo="test" />, 1);
28 + const CSView = 'View';
29 + var props = <CSView foo="test" />;
30 + var state = ReactNativeCS.getInitialState({});
31 + var stateUpdater = function() {};
32 + ReactNativeCS.render({props, state, stateUpdater});
33 });
34 });
packages/react-native-renderer/src/ReactNativeTypes.js
+1
@@ -5,6 +5,7 @@
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @flow
8 + * @providesModule ReactNativeTypes
9 */
10 'use strict';
11
scripts/circleci/check_modules.sh
+3 -1
@@ -3,7 +3,9 @@
3 set -e
4
5 # Make sure we don't introduce accidental @providesModule annotations.
6 -EXPECTED='scripts/rollup/header.js'
6 +EXPECTED='packages/react-cs-renderer/src/ReactNativeCSTypes.js
7 +packages/react-native-renderer/src/ReactNativeTypes.js
8 +scripts/rollup/header.js'
9 ACTUAL=$(git grep -l @providesModule -- './*.js' ':!scripts/rollup/shims/*.js')
10
11 if [ "$EXPECTED" != "$ACTUAL" ]; then
scripts/flow/react-native-host-hooks.js
+9
@@ -110,3 +110,12 @@ declare module 'RTManager' {
110 declare module 'BatchedBridge' {
111 declare function registerCallableModule(name: string, module: Object): void;
112 }
113 +
114 +declare module 'CSComponent' {
115 + declare type Element = any;
116 + declare type Options<Instance> = any;
117 +}
118 +
119 +declare module 'CSStatefulComponent' {
120 + declare function exports(spec: any): any;
121 +}
scripts/rollup/bundles.js
+1 -1
@@ -154,7 +154,7 @@ const bundles = [
154 moduleType: RENDERER,
155 entry: 'react-cs-renderer',
156 global: 'ReactCSRenderer',
157 - externals: [],
157 + externals: ['CSStatefulComponent'],
158 featureFlags: 'react-cs-renderer/src/ReactNativeCSFeatureFlags',
159 },
160