Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,7 @@ function validateNoRefAccessInRenderImpl(
}

const interpolatedAsJsx = new Set<IdentifierId>();
const usedAsEventHandlerProp = new Set<IdentifierId>();
for (const block of fn.body.blocks.values()) {
for (const instr of block.instructions) {
const {value} = instr;
Expand All @@ -363,6 +364,27 @@ function validateNoRefAccessInRenderImpl(
interpolatedAsJsx.add(child.identifier.id);
}
}
if (value.kind === 'JsxExpression') {
/*
* Track identifiers used as event handler props on built-in DOM elements.
* We only allow this optimization for native DOM elements because their
* event handlers are guaranteed by React to only execute in response to
* events, never during render. Custom components could technically call
* their onFoo props during render, which would violate ref access rules.
*/
if (value.tag.kind === 'BuiltinTag') {
for (const prop of value.props) {
if (
prop.kind === 'JsxAttribute' &&
prop.name.startsWith('on') &&
prop.name.length > 2 &&
prop.name[2] === prop.name[2].toUpperCase()
) {
usedAsEventHandlerProp.add(prop.place.identifier.id);
}
}
}
}
}
}
}
Expand Down Expand Up @@ -519,6 +541,9 @@ function validateNoRefAccessInRenderImpl(
*/
if (!didError) {
const isRefLValue = isUseRefType(instr.lvalue.identifier);
const isUsedAsEventHandler = usedAsEventHandlerProp.has(
instr.lvalue.identifier.id,
);
for (const operand of eachInstructionValueOperand(instr.value)) {
/**
* By default we check that function call operands are not refs,
Expand Down Expand Up @@ -560,6 +585,25 @@ function validateNoRefAccessInRenderImpl(
* render function which attempts to obey the rules.
*/
validateNoRefValueAccess(errors, env, operand);
} else if (isUsedAsEventHandler) {
/**
* Special case: the lvalue is used as an event handler prop
* on a built-in DOM element
*
* For example `<form onSubmit={handleSubmit(onSubmit)}>`. Here
* handleSubmit is wrapping onSubmit to create an event handler.
* Functions passed to event handler wrappers can safely access
* refs because built-in DOM event handlers are guaranteed by React
* to only execute in response to actual events, never during render.
*
* We only allow this for built-in DOM elements (not custom components)
* because custom components could technically call their onFoo props
* during render, which would violate ref access rules.
*
* We allow passing functions with ref access to these wrappers,
* but still validate that direct ref values aren't passed.
*/
validateNoDirectRefValueAccess(errors, operand, env);
} else {
validateNoRefPassedToFunction(
errors,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@

## Input

```javascript
// @validateRefAccessDuringRender
import {useRef} from 'react';

// Simulates react-hook-form's handleSubmit
function handleSubmit<T>(callback: (data: T) => void | Promise<void>) {
return (event: any) => {
event.preventDefault();
callback({} as T);
};
}

// Simulates an upload function
async function upload(file: any): Promise<{blob: {url: string}}> {
return {blob: {url: 'https://example.com/file.jpg'}};
}

interface SignatureRef {
toFile(): any;
}

function Component() {
const ref = useRef<SignatureRef>(null);

const onSubmit = async (value: any) => {
// This should be allowed: accessing ref.current in an async event handler
// that's wrapped and passed to onSubmit prop
let sigUrl: string;
if (value.hasSignature) {
const {blob} = await upload(ref.current?.toFile());
sigUrl = blob?.url || '';
} else {
sigUrl = value.signature;
}
console.log('Signature URL:', sigUrl);
};

return (
<form onSubmit={handleSubmit(onSubmit)}>
<input type="text" name="signature" />
<button type="submit">Submit</button>
</form>
);
}

export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{}],
};

```

## Code

```javascript
import { c as _c } from "react/compiler-runtime"; // @validateRefAccessDuringRender
import { useRef } from "react";

// Simulates react-hook-form's handleSubmit
function handleSubmit(callback) {
const $ = _c(2);
let t0;
if ($[0] !== callback) {
t0 = (event) => {
event.preventDefault();
callback({} as T);
};
$[0] = callback;
$[1] = t0;
} else {
t0 = $[1];
}
return t0;
}

// Simulates an upload function
async function upload(file) {
const $ = _c(1);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = { blob: { url: "https://example.com/file.jpg" } };
$[0] = t0;
} else {
t0 = $[0];
}
return t0;
}

interface SignatureRef {
toFile(): any;
}

function Component() {
const $ = _c(4);
const ref = useRef(null);

const onSubmit = async (value) => {
let sigUrl;
if (value.hasSignature) {
const { blob } = await upload(ref.current?.toFile());
sigUrl = blob?.url || "";
} else {
sigUrl = value.signature;
}

console.log("Signature URL:", sigUrl);
};

const t0 = handleSubmit(onSubmit);
let t1;
let t2;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = <input type="text" name="signature" />;
t2 = <button type="submit">Submit</button>;
$[0] = t1;
$[1] = t2;
} else {
t1 = $[0];
t2 = $[1];
}
let t3;
if ($[2] !== t0) {
t3 = (
<form onSubmit={t0}>
{t1}
{t2}
</form>
);
$[2] = t0;
$[3] = t3;
} else {
t3 = $[3];
}
return t3;
}

export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{}],
};

```

### Eval output
(kind: ok) <form><input type="text" name="signature"><button type="submit">Submit</button></form>
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// @validateRefAccessDuringRender
import {useRef} from 'react';

// Simulates react-hook-form's handleSubmit
function handleSubmit<T>(callback: (data: T) => void | Promise<void>) {
return (event: any) => {
event.preventDefault();
callback({} as T);
};
}

// Simulates an upload function
async function upload(file: any): Promise<{blob: {url: string}}> {
return {blob: {url: 'https://example.com/file.jpg'}};
}

interface SignatureRef {
toFile(): any;
}

function Component() {
const ref = useRef<SignatureRef>(null);

const onSubmit = async (value: any) => {
// This should be allowed: accessing ref.current in an async event handler
// that's wrapped and passed to onSubmit prop
let sigUrl: string;
if (value.hasSignature) {
const {blob} = await upload(ref.current?.toFile());
sigUrl = blob?.url || '';
} else {
sigUrl = value.signature;
}
console.log('Signature URL:', sigUrl);
};

return (
<form onSubmit={handleSubmit(onSubmit)}>
<input type="text" name="signature" />
<button type="submit">Submit</button>
</form>
);
}

export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{}],
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@

## Input

```javascript
// @validateRefAccessDuringRender
import {useRef} from 'react';

// Simulates react-hook-form's handleSubmit or similar event handler wrappers
function handleSubmit<T>(callback: (data: T) => void) {
return (event: any) => {
event.preventDefault();
callback({} as T);
};
}

function Component() {
const ref = useRef<HTMLInputElement>(null);

const onSubmit = (data: any) => {
// This should be allowed: accessing ref.current in an event handler
// that's wrapped by handleSubmit and passed to onSubmit prop
if (ref.current !== null) {
console.log(ref.current.value);
}
};

return (
<>
<input ref={ref} />
<form onSubmit={handleSubmit(onSubmit)}>
<button type="submit">Submit</button>
</form>
</>
);
}

export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{}],
};

```

## Code

```javascript
import { c as _c } from "react/compiler-runtime"; // @validateRefAccessDuringRender
import { useRef } from "react";

// Simulates react-hook-form's handleSubmit or similar event handler wrappers
function handleSubmit(callback) {
const $ = _c(2);
let t0;
if ($[0] !== callback) {
t0 = (event) => {
event.preventDefault();
callback({} as T);
};
$[0] = callback;
$[1] = t0;
} else {
t0 = $[1];
}
return t0;
}

function Component() {
const $ = _c(1);
const ref = useRef(null);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
const onSubmit = (data) => {
if (ref.current !== null) {
console.log(ref.current.value);
}
};

t0 = (
<>
<input ref={ref} />
<form onSubmit={handleSubmit(onSubmit)}>
<button type="submit">Submit</button>
</form>
</>
);
$[0] = t0;
} else {
t0 = $[0];
}
return t0;
}

export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{}],
};

```

### Eval output
(kind: ok) <input><form><button type="submit">Submit</button></form>
Loading