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
*/