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.
This commit is contained in:
Joe Savona
2023-10-03 08:47:06 -07:00
parent b3d5e92667
commit 989eaafa43
12 changed files with 215 additions and 77 deletions
@@ -2660,20 +2660,6 @@ function lowerMemberExpression(
const object =
loweredObject ?? lowerExpressionToTemporary(builder, objectNode);
if (objectNode.isIdentifier() && objectNode.node.name === "React") {
builder.errors.push({
reason: `(BuildHIR::lowerMemberExpression) Handle loading properties from React namespace`,
severity: ErrorSeverity.Todo,
loc: propertyNode.node.loc ?? null,
suggestions: null,
});
return {
object,
property: propertyNode.toString(),
value: { kind: "UnsupportedNode", node: exprNode, loc: exprLoc },
};
}
if (!expr.node.computed) {
if (!propertyNode.isIdentifier()) {
builder.errors.push({
@@ -17,7 +17,7 @@ import {
addHook,
addObject,
} from "./ObjectShape";
import { BuiltInType, FunctionType, PolyType } from "./Types";
import { BuiltInType, PolyType } from "./Types";
/**
* This file exports types and defaults for JavaScript global objects.
@@ -234,7 +234,7 @@ const TYPED_GLOBALS: Array<[string, BuiltInType]> = [
// TODO(mofeiZ): We currently only store rest param effects for hooks
// until FeatureFlag `enableTreatHooksAsFunctions` is removed
const BUILTIN_HOOKS: Array<[string, FunctionType]> = [
const BUILTIN_HOOKS: Array<[string, BuiltInType]> = [
[
"useContext",
addHook(DEFAULT_SHAPES, [], {
@@ -314,6 +314,8 @@ const BUILTIN_HOOKS: Array<[string, FunctionType]> = [
],
];
TYPED_GLOBALS.push(["React", addObject(DEFAULT_SHAPES, null, BUILTIN_HOOKS)]);
export type Global = BuiltInType | PolyType;
export type GlobalRegistry = Map<string, Global>;
export const DEFAULT_GLOBALS: GlobalRegistry = new Map(BUILTIN_HOOKS);
@@ -0,0 +1,64 @@
## Input
```javascript
import * as React from "react";
import { calculateExpensiveNumber } from "shared-runtime";
function Component(props) {
const [x] = React.useState(0);
const expensiveNumber = React.useMemo(() => calculateExpensiveNumber(x), [x]);
return <div>{expensiveNumber}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [],
};
```
## Code
```javascript
import { unstable_useMemoCache as useMemoCache } from "react";
import * as React from "react";
import { calculateExpensiveNumber } from "shared-runtime";
function Component(props) {
const $ = useMemoCache(5);
const [x] = React.useState(0);
const c_0 = $[0] !== x;
let t0;
let t1;
if (c_0) {
t0 = () => calculateExpensiveNumber(x);
t1 = [x];
$[0] = x;
$[1] = t0;
$[2] = t1;
} else {
t0 = $[1];
t1 = $[2];
}
const expensiveNumber = React.useMemo(t0, t1);
const c_3 = $[3] !== expensiveNumber;
let t2;
if (c_3) {
t2 = <div>{expensiveNumber}</div>;
$[3] = expensiveNumber;
$[4] = t2;
} else {
t2 = $[4];
}
return t2;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [],
};
```
@@ -1,4 +1,5 @@
import * as React from "react";
import { calculateExpensiveNumber } from "shared-runtime";
function Component(props) {
const [x] = React.useState(0);
@@ -7,9 +8,7 @@ function Component(props) {
return <div>{expensiveNumber}</div>;
}
function Component2(props) {
const [x] = React.useState(0);
const expensiveNumber = React.useMemo(() => calculateExpensiveNumber(x), [x]);
return <div>{expensiveNumber}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [],
};
@@ -1,32 +0,0 @@
## Input
```javascript
import * as React from "react";
function Component(props) {
const [x] = React.useState(0);
const expensiveNumber = React.useMemo(() => calculateExpensiveNumber(x), [x]);
return <div>{expensiveNumber}</div>;
}
function Component2(props) {
const [x] = React.useState(0);
const expensiveNumber = React.useMemo(() => calculateExpensiveNumber(x), [x]);
return <div>{expensiveNumber}</div>;
}
```
## Error
```
[ReactForget] Todo: (BuildHIR::lowerMemberExpression) Handle loading properties from React namespace (4:4)
[ReactForget] Todo: (BuildHIR::lowerMemberExpression) Handle loading properties from React namespace (5:5)
```
@@ -1,19 +0,0 @@
## Input
```javascript
function Foo() {
const [x, setX] = React.useState(1);
return x;
}
```
## Error
```
[ReactForget] Todo: (BuildHIR::lowerMemberExpression) Handle loading properties from React namespace (2:2)
```
@@ -1,4 +0,0 @@
function Foo() {
const [x, setX] = React.useState(1);
return x;
}
@@ -0,0 +1,31 @@
## Input
```javascript
function Component() {
const [x, setX] = React.useState(1);
return x;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [],
};
```
## Code
```javascript
function Component() {
const [x] = React.useState(1);
return x;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [],
};
```
@@ -0,0 +1,9 @@
function Component() {
const [x, setX] = React.useState(1);
return x;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [],
};
@@ -0,0 +1,80 @@
## Input
```javascript
const FooContext = React.createContext({ current: null });
function Component(props) {
const foo = React.useContext(FooContext);
const ref = React.useRef();
const [x, setX] = React.useState(false);
const onClick = () => {
setX(true);
ref.current = true;
foo.current = true;
};
return <div onClick={onClick}>{React.cloneElement(props.children)}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ children: <div>Hello</div> }],
};
```
## Code
```javascript
import { unstable_useMemoCache as useMemoCache } from "react";
const FooContext = React.createContext({ current: null });
function Component(props) {
const $ = useMemoCache(7);
const foo = React.useContext(FooContext);
const ref = React.useRef();
const [x, setX] = React.useState(false);
const c_0 = $[0] !== foo.current;
let t0;
if (c_0) {
t0 = () => {
setX(true);
ref.current = true;
foo.current = true;
};
$[0] = foo.current;
$[1] = t0;
} else {
t0 = $[1];
}
const onClick = t0;
const c_2 = $[2] !== props.children;
let t1;
if (c_2) {
t1 = React.cloneElement(props.children);
$[2] = props.children;
$[3] = t1;
} else {
t1 = $[3];
}
const c_4 = $[4] !== onClick;
const c_5 = $[5] !== t1;
let t2;
if (c_4 || c_5) {
t2 = <div onClick={onClick}>{t1}</div>;
$[4] = onClick;
$[5] = t1;
$[6] = t2;
} else {
t2 = $[6];
}
return t2;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ children: <div>Hello</div> }],
};
```
@@ -0,0 +1,18 @@
const FooContext = React.createContext({ current: null });
function Component(props) {
const foo = React.useContext(FooContext);
const ref = React.useRef();
const [x, setX] = React.useState(false);
const onClick = () => {
setX(true);
ref.current = true;
foo.current = true;
};
return <div onClick={onClick}>{React.cloneElement(props.children)}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ children: <div>Hello</div> }],
};
@@ -54,6 +54,10 @@ export function getNumber(): number {
return 4;
}
export function calculateExpensiveNumber(x: number): number {
return x;
}
/**
* Functions that do not mutate their parameters
*/