@samitouri / QOS-React-2 / commits / 989eaafa43

Support hooks/methods on the React namespace

Allows using hooks/methods off of the `React` namespace, for example `React.useState(sathya)`. Thanks to the previous PR we correctly handle things like validation of hooks called via propertyload syntax. The main change here is to teach the compiler about the `React` namespace. This is a bit of a hack since we treat it as a global, but we're transforming React code so this seems reasonable (?). There are a few additional touch-ups which I'll do in subsequent PRs to make review easier. For example, we need to teach our useMemo/useCallback flattening logic to also handle the case of `React.useMemo()` etc.

Joe Savona committed Oct 3, 2023 at 08:47 UTC 989eaafa434f55d00473d5c2159ac335c1f066f6
12 files changed +215 -77
compiler/packages/babel-plugin-react-forget/src/HIR/BuildHIR.ts
-14
@@ -2660,20 +2660,6 @@ function lowerMemberExpression(
2660 const object =
2661 loweredObject ?? lowerExpressionToTemporary(builder, objectNode);
2662
2663 - if (objectNode.isIdentifier() && objectNode.node.name === "React") {
2664 - builder.errors.push({
2665 - reason: `(BuildHIR::lowerMemberExpression) Handle loading properties from React namespace`,
2666 - severity: ErrorSeverity.Todo,
2667 - loc: propertyNode.node.loc ?? null,
2668 - suggestions: null,
2669 - });
2670 - return {
2671 - object,
2672 - property: propertyNode.toString(),
2673 - value: { kind: "UnsupportedNode", node: exprNode, loc: exprLoc },
2674 - };
2675 - }
2676 -
2663 if (!expr.node.computed) {
2664 if (!propertyNode.isIdentifier()) {
2665 builder.errors.push({
compiler/packages/babel-plugin-react-forget/src/HIR/Globals.ts
+4 -2
@@ -17,7 +17,7 @@ import {
17 addHook,
18 addObject,
19 } from "./ObjectShape";
20 -import { BuiltInType, FunctionType, PolyType } from "./Types";
20 +import { BuiltInType, PolyType } from "./Types";
21
22 /**
23 * This file exports types and defaults for JavaScript global objects.
@@ -234,7 +234,7 @@ const TYPED_GLOBALS: Array<[string, BuiltInType]> = [
234
235 // TODO(mofeiZ): We currently only store rest param effects for hooks
236 // until FeatureFlag `enableTreatHooksAsFunctions` is removed
237 -const BUILTIN_HOOKS: Array<[string, FunctionType]> = [
237 +const BUILTIN_HOOKS: Array<[string, BuiltInType]> = [
238 [
239 "useContext",
240 addHook(DEFAULT_SHAPES, [], {
@@ -314,6 +314,8 @@ const BUILTIN_HOOKS: Array<[string, FunctionType]> = [
314 ],
315 ];
316
317 +TYPED_GLOBALS.push(["React", addObject(DEFAULT_SHAPES, null, BUILTIN_HOOKS)]);
318 +
319 export type Global = BuiltInType | PolyType;
320 export type GlobalRegistry = Map<string, Global>;
321 export const DEFAULT_GLOBALS: GlobalRegistry = new Map(BUILTIN_HOOKS);
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/babel-existing-react-namespace-import.expect.md new
+64
@@ -0,0 +1,64 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import * as React from "react";
6 +import { calculateExpensiveNumber } from "shared-runtime";
7 +
8 +function Component(props) {
9 + const [x] = React.useState(0);
10 + const expensiveNumber = React.useMemo(() => calculateExpensiveNumber(x), [x]);
11 +
12 + return <div>{expensiveNumber}</div>;
13 +}
14 +
15 +export const FIXTURE_ENTRYPOINT = {
16 + fn: Component,
17 + params: [],
18 +};
19 +
20 +```
21 +
22 +## Code
23 +
24 +```javascript
25 +import { unstable_useMemoCache as useMemoCache } from "react";
26 +import * as React from "react";
27 +import { calculateExpensiveNumber } from "shared-runtime";
28 +
29 +function Component(props) {
30 + const $ = useMemoCache(5);
31 + const [x] = React.useState(0);
32 + const c_0 = $[0] !== x;
33 + let t0;
34 + let t1;
35 + if (c_0) {
36 + t0 = () => calculateExpensiveNumber(x);
37 + t1 = [x];
38 + $[0] = x;
39 + $[1] = t0;
40 + $[2] = t1;
41 + } else {
42 + t0 = $[1];
43 + t1 = $[2];
44 + }
45 + const expensiveNumber = React.useMemo(t0, t1);
46 + const c_3 = $[3] !== expensiveNumber;
47 + let t2;
48 + if (c_3) {
49 + t2 = <div>{expensiveNumber}</div>;
50 + $[3] = expensiveNumber;
51 + $[4] = t2;
52 + } else {
53 + t2 = $[4];
54 + }
55 + return t2;
56 +}
57 +
58 +export const FIXTURE_ENTRYPOINT = {
59 + fn: Component,
60 + params: [],
61 +};
62 +
63 +```
64 +
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/babel-existing-react-namespace-import.js renamed
+5 -6
@@ -1,4 +1,5 @@
1 import * as React from "react";
2 +import { calculateExpensiveNumber } from "shared-runtime";
3
4 function Component(props) {
5 const [x] = React.useState(0);
@@ -7,9 +8,7 @@ function Component(props) {
8 return <div>{expensiveNumber}</div>;
9 }
10
10 -function Component2(props) {
11 - const [x] = React.useState(0);
12 - const expensiveNumber = React.useMemo(() => calculateExpensiveNumber(x), [x]);
13 -
14 - return <div>{expensiveNumber}</div>;
15 -}
11 +export const FIXTURE_ENTRYPOINT = {
12 + fn: Component,
13 + params: [],
14 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.babel-existing-react-namespace-import.expect.md deleted
-32
@@ -1,32 +0,0 @@
1 -
2 -## Input
3 -
4 -```javascript
5 -import * as React from "react";
6 -
7 -function Component(props) {
8 - const [x] = React.useState(0);
9 - const expensiveNumber = React.useMemo(() => calculateExpensiveNumber(x), [x]);
10 -
11 - return <div>{expensiveNumber}</div>;
12 -}
13 -
14 -function Component2(props) {
15 - const [x] = React.useState(0);
16 - const expensiveNumber = React.useMemo(() => calculateExpensiveNumber(x), [x]);
17 -
18 - return <div>{expensiveNumber}</div>;
19 -}
20 -
21 -```
22 -
23 -
24 -## Error
25 -
26 -```
27 -[ReactForget] Todo: (BuildHIR::lowerMemberExpression) Handle loading properties from React namespace (4:4)
28 -
29 -[ReactForget] Todo: (BuildHIR::lowerMemberExpression) Handle loading properties from React namespace (5:5)
30 -```
31 -
32 -
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.hooks-with-React-namespace.expect.md deleted
-19
@@ -1,19 +0,0 @@
1 -
2 -## Input
3 -
4 -```javascript
5 -function Foo() {
6 - const [x, setX] = React.useState(1);
7 - return x;
8 -}
9 -
10 -```
11 -
12 -
13 -## Error
14 -
15 -```
16 -[ReactForget] Todo: (BuildHIR::lowerMemberExpression) Handle loading properties from React namespace (2:2)
17 -```
18 -
19 -
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.hooks-with-React-namespace.js deleted
-4
@@ -1,4 +0,0 @@
1 -function Foo() {
2 - const [x, setX] = React.useState(1);
3 - return x;
4 -}
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/hooks-with-React-namespace.expect.md new
+31
@@ -0,0 +1,31 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function Component() {
6 + const [x, setX] = React.useState(1);
7 + return x;
8 +}
9 +
10 +export const FIXTURE_ENTRYPOINT = {
11 + fn: Component,
12 + params: [],
13 +};
14 +
15 +```
16 +
17 +## Code
18 +
19 +```javascript
20 +function Component() {
21 + const [x] = React.useState(1);
22 + return x;
23 +}
24 +
25 +export const FIXTURE_ENTRYPOINT = {
26 + fn: Component,
27 + params: [],
28 +};
29 +
30 +```
31 +
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/hooks-with-React-namespace.js new
+9
@@ -0,0 +1,9 @@
1 +function Component() {
2 + const [x, setX] = React.useState(1);
3 + return x;
4 +}
5 +
6 +export const FIXTURE_ENTRYPOINT = {
7 + fn: Component,
8 + params: [],
9 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/react-namespace.expect.md new
+80
@@ -0,0 +1,80 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +const FooContext = React.createContext({ current: null });
6 +
7 +function Component(props) {
8 + const foo = React.useContext(FooContext);
9 + const ref = React.useRef();
10 + const [x, setX] = React.useState(false);
11 + const onClick = () => {
12 + setX(true);
13 + ref.current = true;
14 + foo.current = true;
15 + };
16 + return <div onClick={onClick}>{React.cloneElement(props.children)}</div>;
17 +}
18 +
19 +export const FIXTURE_ENTRYPOINT = {
20 + fn: Component,
21 + params: [{ children: <div>Hello</div> }],
22 +};
23 +
24 +```
25 +
26 +## Code
27 +
28 +```javascript
29 +import { unstable_useMemoCache as useMemoCache } from "react";
30 +const FooContext = React.createContext({ current: null });
31 +
32 +function Component(props) {
33 + const $ = useMemoCache(7);
34 + const foo = React.useContext(FooContext);
35 + const ref = React.useRef();
36 + const [x, setX] = React.useState(false);
37 + const c_0 = $[0] !== foo.current;
38 + let t0;
39 + if (c_0) {
40 + t0 = () => {
41 + setX(true);
42 + ref.current = true;
43 + foo.current = true;
44 + };
45 + $[0] = foo.current;
46 + $[1] = t0;
47 + } else {
48 + t0 = $[1];
49 + }
50 + const onClick = t0;
51 + const c_2 = $[2] !== props.children;
52 + let t1;
53 + if (c_2) {
54 + t1 = React.cloneElement(props.children);
55 + $[2] = props.children;
56 + $[3] = t1;
57 + } else {
58 + t1 = $[3];
59 + }
60 + const c_4 = $[4] !== onClick;
61 + const c_5 = $[5] !== t1;
62 + let t2;
63 + if (c_4 || c_5) {
64 + t2 = <div onClick={onClick}>{t1}</div>;
65 + $[4] = onClick;
66 + $[5] = t1;
67 + $[6] = t2;
68 + } else {
69 + t2 = $[6];
70 + }
71 + return t2;
72 +}
73 +
74 +export const FIXTURE_ENTRYPOINT = {
75 + fn: Component,
76 + params: [{ children: <div>Hello</div> }],
77 +};
78 +
79 +```
80 +
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/react-namespace.js new
+18
@@ -0,0 +1,18 @@
1 +const FooContext = React.createContext({ current: null });
2 +
3 +function Component(props) {
4 + const foo = React.useContext(FooContext);
5 + const ref = React.useRef();
6 + const [x, setX] = React.useState(false);
7 + const onClick = () => {
8 + setX(true);
9 + ref.current = true;
10 + foo.current = true;
11 + };
12 + return <div onClick={onClick}>{React.cloneElement(props.children)}</div>;
13 +}
14 +
15 +export const FIXTURE_ENTRYPOINT = {
16 + fn: Component,
17 + params: [{ children: <div>Hello</div> }],
18 +};
compiler/packages/sprout/src/shared-runtime.ts
+4
@@ -54,6 +54,10 @@ export function getNumber(): number {
54 return 4;
55 }
56
57 +export function calculateExpensiveNumber(x: number): number {
58 + return x;
59 +}
60 +
61 /**
62 * Functions that do not mutate their parameters
63 */