Skip to content

Commit a54a228

Browse files
committed
[compiler] Allow ref access in useImperativeHandle
Summary: Code within the useImperativeHandle callback is not executed during render, much like useEffect. We can apply the same rules around ref access to this callback. ghstack-source-id: 8e52839 Pull Request resolved: #30913
1 parent d6249ba commit a54a228

File tree

5 files changed

+115
-2
lines changed

5 files changed

+115
-2
lines changed

compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,17 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
362362
returnValueKind: ValueKind.Mutable,
363363
}),
364364
],
365+
[
366+
'useImperativeHandle',
367+
addHook(DEFAULT_SHAPES, {
368+
positionalParams: [],
369+
restParam: Effect.Freeze,
370+
returnType: {kind: 'Primitive'},
371+
calleeEffect: Effect.Read,
372+
hookKind: 'useImperativeHandle',
373+
returnValueKind: ValueKind.Frozen,
374+
}),
375+
],
365376
[
366377
'useMemo',
367378
addHook(DEFAULT_SHAPES, {

compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ export type HookKind =
127127
| 'useMemo'
128128
| 'useCallback'
129129
| 'useTransition'
130+
| 'useImperativeHandle'
130131
| 'Custom';
131132

132133
/*

compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccesInRender.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,12 @@ function validateNoRefAccessInRenderImpl(
282282
break;
283283
}
284284
case 'MethodCall': {
285-
if (!isEffectHook(instr.value.property.identifier)) {
285+
if (
286+
!isHookThatAllowsRefValueAccess(
287+
fn,
288+
instr.value.property.identifier,
289+
)
290+
) {
286291
for (const operand of eachInstructionValueOperand(instr.value)) {
287292
const hookKind = getHookKindForType(
288293
fn.env,
@@ -307,7 +312,10 @@ function validateNoRefAccessInRenderImpl(
307312
case 'CallExpression': {
308313
const callee = instr.value.callee;
309314
const hookKind = getHookKindForType(fn.env, callee.identifier.type);
310-
const isUseEffect = isEffectHook(callee.identifier);
315+
const isUseEffect = isHookThatAllowsRefValueAccess(
316+
fn,
317+
callee.identifier,
318+
);
311319
let returnType: RefAccessType = {kind: 'None'};
312320
if (!isUseEffect) {
313321
// Report a more precise error when calling a local function that accesses a ref
@@ -499,3 +507,11 @@ function validateNoDirectRefValueAccess(
499507
});
500508
}
501509
}
510+
511+
function isHookThatAllowsRefValueAccess(
512+
fn: HIRFunction,
513+
identifier: Identifier,
514+
): boolean {
515+
const hookKind = getHookKindForType(fn.env, identifier.type);
516+
return isEffectHook(identifier) || hookKind === 'useImperativeHandle';
517+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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)
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
};

0 commit comments

Comments
 (0)