@samitouri / QOS-React-1 / commits / e78c9362c0

[compiler] Allow all hooks to take callbacks which access refs, but ban hooks from taking direct ref value arguments

Summary: This brings the behavior of ref mutation within hook callbacks into alignment with the behavior of global mutations--that is, we allow all hooks to take callbacks that may mutate a ref. This is potentially unsafe if the hook eagerly calls its callback, but the alternative is excessively limiting (and inconsistent with other enforcement). This also bans *directly* passing a ref.current value to a hook, which was previously allowed. ghstack-source-id: e66ce7123ecf4a905adab957970d0ee5d41245e0 Pull Request resolved: https://github.com/facebook/react/pull/30917

Mike Vitousek committed Sep 16, 2024 at 10:53 UTC e78c9362c014dccaed5ff193106e44d7d072dc32
9 files changed +238 -51
compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts
+11
@@ -364,6 +364,17 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
364 returnValueKind: ValueKind.Mutable,
365 }),
366 ],
367 + [
368 + 'useImperativeHandle',
369 + addHook(DEFAULT_SHAPES, {
370 + positionalParams: [],
371 + restParam: Effect.Freeze,
372 + returnType: {kind: 'Primitive'},
373 + calleeEffect: Effect.Read,
374 + hookKind: 'useImperativeHandle',
375 + returnValueKind: ValueKind.Frozen,
376 + }),
377 + ],
378 [
379 'useMemo',
380 addHook(DEFAULT_SHAPES, {
compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts
+1
@@ -127,6 +127,7 @@ export type HookKind =
127 | 'useMemo'
128 | 'useCallback'
129 | 'useTransition'
130 + | 'useImperativeHandle'
131 | 'Custom';
132
133 /*
compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccesInRender.ts
+27 -51
@@ -22,7 +22,6 @@ import {
22 eachTerminalOperand,
23 } from '../HIR/visitors';
24 import {Err, Ok, Result} from '../Utils/Result';
25 -import {isEffectHook} from './ValidateMemoizedEffectDependencies';
25
26 /**
27 * Validates that a function does not access a ref value during render. This includes a partial check
@@ -310,60 +309,37 @@ function validateNoRefAccessInRenderImpl(
309 });
310 break;
311 }
313 - case 'MethodCall': {
314 - if (!isEffectHook(instr.value.property.identifier)) {
315 - for (const operand of eachInstructionValueOperand(instr.value)) {
316 - const hookKind = getHookKindForType(
317 - fn.env,
318 - instr.value.property.identifier.type,
319 - );
320 - if (hookKind != null) {
321 - validateNoRefValueAccess(errors, env, operand);
322 - } else {
323 - validateNoRefAccess(errors, env, operand, operand.loc);
324 - }
325 - }
326 - }
327 - validateNoRefValueAccess(errors, env, instr.value.receiver);
328 - const methType = env.get(instr.value.property.identifier.id);
329 - let returnType: RefAccessType = {kind: 'None'};
330 - if (methType?.kind === 'Structure' && methType.fn !== null) {
331 - returnType = methType.fn.returnType;
332 - }
333 - env.set(instr.lvalue.identifier.id, returnType);
334 - break;
335 - }
312 + case 'MethodCall':
313 case 'CallExpression': {
337 - const callee = instr.value.callee;
314 + const callee =
315 + instr.value.kind === 'CallExpression'
316 + ? instr.value.callee
317 + : instr.value.property;
318 const hookKind = getHookKindForType(fn.env, callee.identifier.type);
339 - const isUseEffect = isEffectHook(callee.identifier);
319 let returnType: RefAccessType = {kind: 'None'};
341 - if (!isUseEffect) {
342 - // Report a more precise error when calling a local function that accesses a ref
343 - const fnType = env.get(instr.value.callee.identifier.id);
344 - if (fnType?.kind === 'Structure' && fnType.fn !== null) {
345 - returnType = fnType.fn.returnType;
346 - if (fnType.fn.readRefEffect) {
347 - errors.push({
348 - severity: ErrorSeverity.InvalidReact,
349 - reason:
350 - 'This function accesses a ref value (the `current` property), which may not be accessed during render. (https://react.dev/reference/react/useRef)',
351 - loc: callee.loc,
352 - description:
353 - callee.identifier.name !== null &&
354 - callee.identifier.name.kind === 'named'
355 - ? `Function \`${callee.identifier.name.value}\` accesses a ref`
356 - : null,
357 - suggestions: null,
358 - });
359 - }
320 + const fnType = env.get(callee.identifier.id);
321 + if (fnType?.kind === 'Structure' && fnType.fn !== null) {
322 + returnType = fnType.fn.returnType;
323 + if (fnType.fn.readRefEffect) {
324 + errors.push({
325 + severity: ErrorSeverity.InvalidReact,
326 + reason:
327 + 'This function accesses a ref value (the `current` property), which may not be accessed during render. (https://react.dev/reference/react/useRef)',
328 + loc: callee.loc,
329 + description:
330 + callee.identifier.name !== null &&
331 + callee.identifier.name.kind === 'named'
332 + ? `Function \`${callee.identifier.name.value}\` accesses a ref`
333 + : null,
334 + suggestions: null,
335 + });
336 }
361 - for (const operand of eachInstructionValueOperand(instr.value)) {
362 - if (hookKind != null) {
363 - validateNoRefValueAccess(errors, env, operand);
364 - } else {
365 - validateNoRefAccess(errors, env, operand, operand.loc);
366 - }
337 + }
338 + for (const operand of eachInstructionValueOperand(instr.value)) {
339 + if (hookKind != null) {
340 + validateNoDirectRefValueAccess(errors, operand, env);
341 + } else {
342 + validateNoRefAccess(errors, env, operand, operand.loc);
343 }
344 }
345 env.set(instr.lvalue.identifier.id, returnType);
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-ref-value.expect.md new
+34
@@ -0,0 +1,34 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import {useEffect, useRef} from 'react';
6 +
7 +function Component(props) {
8 + const ref = useRef();
9 + useEffect(() => {}, [ref.current]);
10 +}
11 +
12 +export const FIXTURE_ENTRYPOINT = {
13 + fn: Component,
14 + params: [],
15 +};
16 +
17 +```
18 +
19 +
20 +## Error
21 +
22 +```
23 + 3 | function Component(props) {
24 + 4 | const ref = useRef();
25 +> 5 | useEffect(() => {}, [ref.current]);
26 + | ^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (5:5)
27 +
28 +InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (5:5)
29 + 6 | }
30 + 7 |
31 + 8 | export const FIXTURE_ENTRYPOINT = {
32 +```
33 +
34 +
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-ref-value.js new
+11
@@ -0,0 +1,11 @@
1 +import {useEffect, useRef} from 'react';
2 +
3 +function Component(props) {
4 + const ref = useRef();
5 + useEffect(() => {}, [ref.current]);
6 +}
7 +
8 +export const FIXTURE_ENTRYPOINT = {
9 + fn: Component,
10 + params: [],
11 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hook-ref-callback.expect.md new
+54
@@ -0,0 +1,54 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import {useEffect, useRef} from 'react';
6 +
7 +function Component(props) {
8 + const ref = useRef();
9 + useFoo(() => {
10 + ref.current = 42;
11 + });
12 +}
13 +
14 +function useFoo(x) {}
15 +
16 +export const FIXTURE_ENTRYPOINT = {
17 + fn: Component,
18 + params: [],
19 +};
20 +
21 +```
22 +
23 +## Code
24 +
25 +```javascript
26 +import { c as _c } from "react/compiler-runtime";
27 +import { useEffect, useRef } from "react";
28 +
29 +function Component(props) {
30 + const $ = _c(1);
31 + const ref = useRef();
32 + let t0;
33 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
34 + t0 = () => {
35 + ref.current = 42;
36 + };
37 + $[0] = t0;
38 + } else {
39 + t0 = $[0];
40 + }
41 + useFoo(t0);
42 +}
43 +
44 +function useFoo(x) {}
45 +
46 +export const FIXTURE_ENTRYPOINT = {
47 + fn: Component,
48 + params: [],
49 +};
50 +
51 +```
52 +
53 +### Eval output
54 +(kind: ok)
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hook-ref-callback.js new
+15
@@ -0,0 +1,15 @@
1 +import {useEffect, useRef} from 'react';
2 +
3 +function Component(props) {
4 + const ref = useRef();
5 + useFoo(() => {
6 + ref.current = 42;
7 + });
8 +}
9 +
10 +function useFoo(x) {}
11 +
12 +export const FIXTURE_ENTRYPOINT = {
13 + fn: Component,
14 + params: [],
15 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useImperativeHandle-ref-mutate.expect.md new
+66
@@ -0,0 +1,66 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @flow
6 +
7 +import {useImperativeHandle, useRef} from 'react';
8 +
9 +component Component(prop: number) {
10 + const ref1 = useRef(null);
11 + const ref2 = useRef(1);
12 + useImperativeHandle(ref1, () => {
13 + const precomputed = prop + ref2.current;
14 + return {
15 + foo: () => prop + ref2.current + precomputed,
16 + };
17 + }, [prop]);
18 +}
19 +
20 +export const FIXTURE_ENTRYPOINT = {
21 + fn: Component,
22 + params: [{prop: 1}],
23 +};
24 +
25 +```
26 +
27 +## Code
28 +
29 +```javascript
30 +import { c as _c } from "react/compiler-runtime";
31 +
32 +import { useImperativeHandle, useRef } from "react";
33 +
34 +function Component(t0) {
35 + const $ = _c(3);
36 + const { prop } = t0;
37 + const ref1 = useRef(null);
38 + const ref2 = useRef(1);
39 + let t1;
40 + let t2;
41 + if ($[0] !== prop) {
42 + t1 = () => {
43 + const precomputed = prop + ref2.current;
44 + return { foo: () => prop + ref2.current + precomputed };
45 + };
46 +
47 + t2 = [prop];
48 + $[0] = prop;
49 + $[1] = t1;
50 + $[2] = t2;
51 + } else {
52 + t1 = $[1];
53 + t2 = $[2];
54 + }
55 + useImperativeHandle(ref1, t1, t2);
56 +}
57 +
58 +export const FIXTURE_ENTRYPOINT = {
59 + fn: Component,
60 + params: [{ prop: 1 }],
61 +};
62 +
63 +```
64 +
65 +### Eval output
66 +(kind: ok)
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useImperativeHandle-ref-mutate.js new
+19
@@ -0,0 +1,19 @@
1 +// @flow
2 +
3 +import {useImperativeHandle, useRef} from 'react';
4 +
5 +component Component(prop: number) {
6 + const ref1 = useRef(null);
7 + const ref2 = useRef(1);
8 + useImperativeHandle(ref1, () => {
9 + const precomputed = prop + ref2.current;
10 + return {
11 + foo: () => prop + ref2.current + precomputed,
12 + };
13 + }, [prop]);
14 +}
15 +
16 +export const FIXTURE_ENTRYPOINT = {
17 + fn: Component,
18 + params: [{prop: 1}],
19 +};