Compare commits

..

12 Commits

Author SHA1 Message Date
Joe Savona
9ad4418715 [compiler] Add support for commonjs
We previously always generated import statements for any modules that had to be required, notably the `import {c} from 'react/compiler-runtime'` for the memo cache function. However, this obviously doesn't work when the source is using commonjs. Now we check the sourceType of the module and generate require() statements if the source type is 'script'.

I initially explored using https://babeljs.io/docs/babel-helper-module-imports, but the API design was unfortunately not flexible enough for our use-case. Specifically, our pipeline is as follows:
* Compile individual functions. Generate candidate imports, pre-allocating the local names for those imports.
* If the file is compiled successfully, actually add the imports to the program.

Ie we need to pre-allocate identifier names for the imports before we add them to the program — but that isn't supported by babel-helper-module-imports. So instead we generate our own require() calls if the sourceType is script.
2025-09-24 10:56:52 -07:00
Sebastian "Sebbie" Silbermann
58d17912e8 Fix failing React DevTools regression tests (#34585) 2025-09-24 19:08:13 +02:00
Joseph Savona
2c6d92fd80 [compiler] Name anonymous functions from inlined useCallbacks (#34586)
@eps1lon flagged this case. Inlined useCallback has an extra LoadLocal
indirection which caused us not to add a name. While I was there I added
some extra checks to make sure we don't generate names for a given node
twice (just in case).
2025-09-24 09:18:16 -07:00
Sebastian Markbåge
e233218359 Track "Animating" Entry for Gestures while the Gesture is Still On-going (#34548)
Stacked on #34546.

Same as #34538 but for gestures.

Includes various fixes.

This shows how it ends with a Transition when you release in the
committed state. Note how the Animation of the Gesture continues until
the Transition is done so that the handoff is seamless.

<img width="853" height="134" alt="Screenshot 2025-09-20 at 7 37 29 PM"
src="https://github.com/user-attachments/assets/6192a033-4bec-43b9-884b-77e3a6f00da6"
/>
2025-09-24 11:26:03 -04:00
Sebastian Markbåge
05b61f812a Add Gesture Track in Performance Tab (#34546) 2025-09-24 17:20:14 +02:00
Sebastian Markbåge
e0c421ab71 Include SyncLane in includesBlockingLane helper (#34543)
This helper weirdly doesn't include the sync lane.

Everywhere we use it we have to check the sync lane separately. We can
simplify things by simply including the sync lane.

This fixes a lack of optimization because we should not check the store
consistency for a `flushSync` render.


d91d28c8ba/packages/react-reconciler/src/ReactFiberHooks.js (L1691-L1693)
2025-09-24 09:34:35 -04:00
Ruslan Lesiutin
2ee6147510 [DevTools] Switch sourcemap-codec dependency (#34569)
[sourcemap-codec](https://www.npmjs.com/package/sourcemap-codec)
(deprecated) ->
[@jridgewell/sourcemap-codec](https://www.npmjs.com/package/@jridgewell/sourcemap-codec)

Validated that symbolication still works.
2025-09-24 06:11:53 -07:00
Jordan Brown
e02c173fa5 [lint] Allow useEffectEvent in useLayoutEffect and useInsertionEffect (#34492)
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34492).
* #34497
* __->__ #34492
2025-09-23 16:56:09 -04:00
Ruslan Lesiutin
24a2ba03fb [DevTools] fix: dedupe file fetch requests and define a timeout (#34566)
If there is a large owner stack, we could potentially spam multiple
fetch requests for the same source map. This adds a simple deduplication
logic, based on URL.

Also, this adds a timeout of 60 seconds to all fetch requests initiated
by fileFetcher content script.
2025-09-23 11:38:07 -07:00
Sebastian "Sebbie" Silbermann
012b371cde [DevTools] Handle LegacyHidden Fibers like Offscreen Fibers. (#34564) 2025-09-23 20:14:53 +02:00
Jack Pope
83c88ad470 Handle fabric root level fragment with compareDocumentPosition (#34533)
The root instance doesn't have a canonical property so we were not
returning a public instance that we can call compareDocumentPosition on
when a Fragment had no other host parent in Fabric. In this case we need
to get the ReactNativeElement from the ReactNativeDocument.

I've also added test coverage for this case in DOM for consistency,
though it was already working there because we use DOM elements as root.
This same test will be copied to RN using Fantom.
2025-09-23 10:56:43 -04:00
Sebastian "Sebbie" Silbermann
cad813ac1e Fix CI from stale merge (#34555) 2025-09-23 08:49:16 +02:00
65 changed files with 1007 additions and 1602 deletions

View File

@@ -2,4 +2,4 @@ import type { PluginOptions } from 
'babel-plugin-react-compiler/dist';
({
  //compilationMode: "all"
} satisfies Partial<PluginOptions>);
} satisfies PluginOptions);

View File

@@ -263,7 +263,7 @@ test('error is displayed when config has validation error', async ({page}) => {
({
compilationMode: "123"
} satisfies Partial<PluginOptions>);`,
} satisfies PluginOptions);`,
showInternals: false,
};
const hash = encodeStore(store);
@@ -293,7 +293,7 @@ test('disableMemoizationForDebugging flag works as expected', async ({
environment: {
disableMemoizationForDebugging: true
}
} satisfies Partial<PluginOptions>);`,
} satisfies PluginOptions);`,
showInternals: false,
};
const hash = encodeStore(store);

View File

@@ -240,7 +240,7 @@ export function addImportsToProgram(
programContext: ProgramContext,
): void {
const existingImports = getExistingImports(path);
const stmts: Array<t.ImportDeclaration> = [];
const stmts: Array<t.ImportDeclaration | t.VariableDeclaration> = [];
const sortedModules = [...programContext.imports.entries()].sort(([a], [b]) =>
a.localeCompare(b),
);
@@ -303,9 +303,29 @@ export function addImportsToProgram(
if (maybeExistingImports != null) {
maybeExistingImports.pushContainer('specifiers', importSpecifiers);
} else {
stmts.push(
t.importDeclaration(importSpecifiers, t.stringLiteral(moduleName)),
);
if (path.node.sourceType === 'module') {
stmts.push(
t.importDeclaration(importSpecifiers, t.stringLiteral(moduleName)),
);
} else {
stmts.push(
t.variableDeclaration('const', [
t.variableDeclarator(
t.objectPattern(
sortedImport.map(specifier => {
return t.objectProperty(
t.identifier(specifier.imported),
t.identifier(specifier.name),
);
}),
),
t.callExpression(t.identifier('require'), [
t.stringLiteral(moduleName),
]),
),
]),
);
}
}
}
path.unshiftContainer('body', stmts);

View File

@@ -103,7 +103,6 @@ import {validateNoFreezingKnownMutableFunctions} from '../Validation/ValidateNoF
import {inferMutationAliasingEffects} from '../Inference/InferMutationAliasingEffects';
import {inferMutationAliasingRanges} from '../Inference/InferMutationAliasingRanges';
import {validateNoDerivedComputationsInEffects} from '../Validation/ValidateNoDerivedComputationsInEffects';
import {validateNoDerivedComputationsInEffects_exp} from '../Validation/ValidateNoDerivedComputationsInEffects_exp';
import {nameAnonymousFunctions} from '../Transform/NameAnonymousFunctions';
export type CompilerPipelineValue =
@@ -276,10 +275,6 @@ function runWithEnvironment(
validateNoDerivedComputationsInEffects(hir);
}
if (env.config.validateNoDerivedComputationsInEffects_exp) {
validateNoDerivedComputationsInEffects_exp(hir);
}
if (env.config.validateNoSetStateInEffects) {
env.logErrors(validateNoSetStateInEffects(hir, env));
}

View File

@@ -334,12 +334,6 @@ export const EnvironmentConfigSchema = z.object({
*/
validateNoDerivedComputationsInEffects: z.boolean().default(false),
/**
* Experimental: Validates that effects are not used to calculate derived data which could instead be computed
* during render. Generates a custom error message for each type of violation.
*/
validateNoDerivedComputationsInEffects_exp: z.boolean().default(false),
/**
* Validates against creating JSX within a try block and recommends using an error boundary
* instead.

View File

@@ -19,7 +19,7 @@ export function nameAnonymousFunctions(fn: HIRFunction): void {
const parentName = fn.id;
const functions = nameAnonymousFunctionsImpl(fn);
function visit(node: Node, prefix: string): void {
if (node.generatedName != null) {
if (node.generatedName != null && node.fn.nameHint == null) {
/**
* Note that we don't generate a name for functions that already had one,
* so we'll only add the prefix to anonymous functions regardless of
@@ -70,6 +70,10 @@ function nameAnonymousFunctionsImpl(fn: HIRFunction): Array<Node> {
if (name != null && name.kind === 'named') {
names.set(lvalue.identifier.id, name.value);
}
const func = functions.get(value.place.identifier.id);
if (func != null) {
functions.set(lvalue.identifier.id, func);
}
break;
}
case 'PropertyLoad': {
@@ -106,6 +110,7 @@ function nameAnonymousFunctionsImpl(fn: HIRFunction): Array<Node> {
const variableName = value.lvalue.place.identifier.name;
if (
node != null &&
node.generatedName == null &&
variableName != null &&
variableName.kind === 'named'
) {
@@ -137,7 +142,7 @@ function nameAnonymousFunctionsImpl(fn: HIRFunction): Array<Node> {
continue;
}
const node = functions.get(arg.identifier.id);
if (node != null) {
if (node != null && node.generatedName == null) {
const generatedName =
fnArgCount > 1 ? `${calleeName}(arg${i})` : `${calleeName}()`;
node.generatedName = generatedName;
@@ -152,7 +157,7 @@ function nameAnonymousFunctionsImpl(fn: HIRFunction): Array<Node> {
continue;
}
const node = functions.get(attr.place.identifier.id);
if (node != null) {
if (node != null && node.generatedName == null) {
const elementName =
value.tag.kind === 'BuiltinTag'
? value.tag.name

View File

@@ -1,240 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {CompilerError, SourceLocation} from '..';
import {ErrorCategory} from '../CompilerError';
import {
ArrayExpression,
BlockId,
FunctionExpression,
HIRFunction,
IdentifierId,
isSetStateType,
isUseEffectHookType,
} from '../HIR';
import {
eachInstructionValueOperand,
eachTerminalOperand,
} from '../HIR/visitors';
/**
* Validates that useEffect is not used for derived computations which could/should
* be performed in render.
*
* See https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state
*
* Example:
*
* ```
* // 🔴 Avoid: redundant state and unnecessary Effect
* const [fullName, setFullName] = useState('');
* useEffect(() => {
* setFullName(firstName + ' ' + lastName);
* }, [firstName, lastName]);
* ```
*
* Instead use:
*
* ```
* // ✅ Good: calculated during rendering
* const fullName = firstName + ' ' + lastName;
* ```
*/
export function validateNoDerivedComputationsInEffects_exp(
fn: HIRFunction,
): void {
const candidateDependencies: Map<IdentifierId, ArrayExpression> = new Map();
const functions: Map<IdentifierId, FunctionExpression> = new Map();
const locals: Map<IdentifierId, IdentifierId> = new Map();
const errors = new CompilerError();
for (const block of fn.body.blocks.values()) {
for (const instr of block.instructions) {
const {lvalue, value} = instr;
if (value.kind === 'LoadLocal') {
locals.set(lvalue.identifier.id, value.place.identifier.id);
} else if (value.kind === 'ArrayExpression') {
candidateDependencies.set(lvalue.identifier.id, value);
} else if (value.kind === 'FunctionExpression') {
functions.set(lvalue.identifier.id, value);
} else if (
value.kind === 'CallExpression' ||
value.kind === 'MethodCall'
) {
const callee =
value.kind === 'CallExpression' ? value.callee : value.property;
if (
isUseEffectHookType(callee.identifier) &&
value.args.length === 2 &&
value.args[0].kind === 'Identifier' &&
value.args[1].kind === 'Identifier'
) {
const effectFunction = functions.get(value.args[0].identifier.id);
const deps = candidateDependencies.get(value.args[1].identifier.id);
if (
effectFunction != null &&
deps != null &&
deps.elements.length !== 0 &&
deps.elements.every(element => element.kind === 'Identifier')
) {
const dependencies: Array<IdentifierId> = deps.elements.map(dep => {
CompilerError.invariant(dep.kind === 'Identifier', {
reason: `Dependency is checked as a place above`,
description: null,
details: [
{
kind: 'error',
loc: value.loc,
message: 'this is checked as a place above',
},
],
});
return locals.get(dep.identifier.id) ?? dep.identifier.id;
});
validateEffect(
effectFunction.loweredFunc.func,
dependencies,
errors,
);
}
}
}
}
}
if (errors.hasAnyErrors()) {
throw errors;
}
}
function validateEffect(
effectFunction: HIRFunction,
effectDeps: Array<IdentifierId>,
errors: CompilerError,
): void {
for (const operand of effectFunction.context) {
if (isSetStateType(operand.identifier)) {
continue;
} else if (effectDeps.find(dep => dep === operand.identifier.id) != null) {
continue;
} else {
// Captured something other than the effect dep or setState
return;
}
}
for (const dep of effectDeps) {
if (
effectFunction.context.find(operand => operand.identifier.id === dep) ==
null
) {
// effect dep wasn't actually used in the function
return;
}
}
const seenBlocks: Set<BlockId> = new Set();
const values: Map<IdentifierId, Array<IdentifierId>> = new Map();
for (const dep of effectDeps) {
values.set(dep, [dep]);
}
const setStateLocations: Array<SourceLocation> = [];
for (const block of effectFunction.body.blocks.values()) {
for (const pred of block.preds) {
if (!seenBlocks.has(pred)) {
// skip if block has a back edge
return;
}
}
for (const phi of block.phis) {
const aggregateDeps: Set<IdentifierId> = new Set();
for (const operand of phi.operands.values()) {
const deps = values.get(operand.identifier.id);
if (deps != null) {
for (const dep of deps) {
aggregateDeps.add(dep);
}
}
}
if (aggregateDeps.size !== 0) {
values.set(phi.place.identifier.id, Array.from(aggregateDeps));
}
}
for (const instr of block.instructions) {
switch (instr.value.kind) {
case 'Primitive':
case 'JSXText':
case 'LoadGlobal': {
break;
}
case 'LoadLocal': {
const deps = values.get(instr.value.place.identifier.id);
if (deps != null) {
values.set(instr.lvalue.identifier.id, deps);
}
break;
}
case 'ComputedLoad':
case 'PropertyLoad':
case 'BinaryExpression':
case 'TemplateLiteral':
case 'CallExpression':
case 'MethodCall': {
const aggregateDeps: Set<IdentifierId> = new Set();
for (const operand of eachInstructionValueOperand(instr.value)) {
const deps = values.get(operand.identifier.id);
if (deps != null) {
for (const dep of deps) {
aggregateDeps.add(dep);
}
}
}
if (aggregateDeps.size !== 0) {
values.set(instr.lvalue.identifier.id, Array.from(aggregateDeps));
}
if (
instr.value.kind === 'CallExpression' &&
isSetStateType(instr.value.callee.identifier) &&
instr.value.args.length === 1 &&
instr.value.args[0].kind === 'Identifier'
) {
const deps = values.get(instr.value.args[0].identifier.id);
if (deps != null && new Set(deps).size === effectDeps.length) {
setStateLocations.push(instr.value.callee.loc);
} else {
// doesn't depend on any deps
return;
}
}
break;
}
default: {
return;
}
}
}
for (const operand of eachTerminalOperand(block.terminal)) {
if (values.has(operand.identifier.id)) {
//
return;
}
}
seenBlocks.add(block.id);
}
for (const loc of setStateLocations) {
errors.push({
category: ErrorCategory.EffectDerivationsOfState,
reason:
'Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)',
description: null,
loc,
suggestions: null,
});
}
}

View File

@@ -1,79 +0,0 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects_exp
import {useEffect, useState} from 'react';
function Component({value, enabled}) {
const [localValue, setLocalValue] = useState('');
useEffect(() => {
if (enabled) {
setLocalValue(value);
} else {
setLocalValue('disabled');
}
}, [value, enabled]);
return <div>{localValue}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 'test', enabled: true}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp
import { useEffect, useState } from "react";
function Component(t0) {
const $ = _c(6);
const { value, enabled } = t0;
const [localValue, setLocalValue] = useState("");
let t1;
let t2;
if ($[0] !== enabled || $[1] !== value) {
t1 = () => {
if (enabled) {
setLocalValue(value);
} else {
setLocalValue("disabled");
}
};
t2 = [value, enabled];
$[0] = enabled;
$[1] = value;
$[2] = t1;
$[3] = t2;
} else {
t1 = $[2];
t2 = $[3];
}
useEffect(t1, t2);
let t3;
if ($[4] !== localValue) {
t3 = <div>{localValue}</div>;
$[4] = localValue;
$[5] = t3;
} else {
t3 = $[5];
}
return t3;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ value: "test", enabled: true }],
};
```
### Eval output
(kind: ok) <div>test</div>

View File

@@ -1,21 +0,0 @@
// @validateNoDerivedComputationsInEffects_exp
import {useEffect, useState} from 'react';
function Component({value, enabled}) {
const [localValue, setLocalValue] = useState('');
useEffect(() => {
if (enabled) {
setLocalValue(value);
} else {
setLocalValue('disabled');
}
}, [value, enabled]);
return <div>{localValue}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 'test', enabled: true}],
};

View File

@@ -1,71 +0,0 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects_exp
import {useEffect, useState} from 'react';
export default function Component({input = 'empty'}) {
const [currInput, setCurrInput] = useState(input);
const localConst = 'local const';
useEffect(() => {
setCurrInput(input + localConst);
}, [input, localConst]);
return <div>{currInput}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{input: 'test'}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp
import { useEffect, useState } from "react";
export default function Component(t0) {
const $ = _c(5);
const { input: t1 } = t0;
const input = t1 === undefined ? "empty" : t1;
const [currInput, setCurrInput] = useState(input);
let t2;
let t3;
if ($[0] !== input) {
t2 = () => {
setCurrInput(input + "local const");
};
t3 = [input, "local const"];
$[0] = input;
$[1] = t2;
$[2] = t3;
} else {
t2 = $[1];
t3 = $[2];
}
useEffect(t2, t3);
let t4;
if ($[3] !== currInput) {
t4 = <div>{currInput}</div>;
$[3] = currInput;
$[4] = t4;
} else {
t4 = $[4];
}
return t4;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ input: "test" }],
};
```
### Eval output
(kind: ok) <div>testlocal const</div>

View File

@@ -1,18 +0,0 @@
// @validateNoDerivedComputationsInEffects_exp
import {useEffect, useState} from 'react';
export default function Component({input = 'empty'}) {
const [currInput, setCurrInput] = useState(input);
const localConst = 'local const';
useEffect(() => {
setCurrInput(input + localConst);
}, [input, localConst]);
return <div>{currInput}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{input: 'test'}],
};

View File

@@ -1,70 +0,0 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects_exp
import {useEffect, useState} from 'react';
function Component({shouldChange}) {
const [count, setCount] = useState(0);
useEffect(() => {
if (shouldChange) {
setCount(count + 1);
}
}, [count]);
return <div>{count}</div>;
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp
import { useEffect, useState } from "react";
function Component(t0) {
const $ = _c(7);
const { shouldChange } = t0;
const [count, setCount] = useState(0);
let t1;
if ($[0] !== count || $[1] !== shouldChange) {
t1 = () => {
if (shouldChange) {
setCount(count + 1);
}
};
$[0] = count;
$[1] = shouldChange;
$[2] = t1;
} else {
t1 = $[2];
}
let t2;
if ($[3] !== count) {
t2 = [count];
$[3] = count;
$[4] = t2;
} else {
t2 = $[4];
}
useEffect(t1, t2);
let t3;
if ($[5] !== count) {
t3 = <div>{count}</div>;
$[5] = count;
$[6] = t3;
} else {
t3 = $[6];
}
return t3;
}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -1,15 +0,0 @@
// @validateNoDerivedComputationsInEffects_exp
import {useEffect, useState} from 'react';
function Component({shouldChange}) {
const [count, setCount] = useState(0);
useEffect(() => {
if (shouldChange) {
setCount(count + 1);
}
}, [count]);
return <div>{count}</div>;
}

View File

@@ -1,108 +0,0 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects_exp
import {useEffect, useState} from 'react';
function Component({firstName}) {
const [lastName, setLastName] = useState('Doe');
const [fullName, setFullName] = useState('John');
const middleName = 'D.';
useEffect(() => {
setFullName(firstName + ' ' + middleName + ' ' + lastName);
}, [firstName, middleName, lastName]);
return (
<div>
<input value={lastName} onChange={e => setLastName(e.target.value)} />
<div>{fullName}</div>
</div>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{firstName: 'John'}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp
import { useEffect, useState } from "react";
function Component(t0) {
const $ = _c(12);
const { firstName } = t0;
const [lastName, setLastName] = useState("Doe");
const [fullName, setFullName] = useState("John");
let t1;
let t2;
if ($[0] !== firstName || $[1] !== lastName) {
t1 = () => {
setFullName(firstName + " " + "D." + " " + lastName);
};
t2 = [firstName, "D.", lastName];
$[0] = firstName;
$[1] = lastName;
$[2] = t1;
$[3] = t2;
} else {
t1 = $[2];
t2 = $[3];
}
useEffect(t1, t2);
let t3;
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
t3 = (e) => setLastName(e.target.value);
$[4] = t3;
} else {
t3 = $[4];
}
let t4;
if ($[5] !== lastName) {
t4 = <input value={lastName} onChange={t3} />;
$[5] = lastName;
$[6] = t4;
} else {
t4 = $[6];
}
let t5;
if ($[7] !== fullName) {
t5 = <div>{fullName}</div>;
$[7] = fullName;
$[8] = t5;
} else {
t5 = $[8];
}
let t6;
if ($[9] !== t4 || $[10] !== t5) {
t6 = (
<div>
{t4}
{t5}
</div>
);
$[9] = t4;
$[10] = t5;
$[11] = t6;
} else {
t6 = $[11];
}
return t6;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ firstName: "John" }],
};
```
### Eval output
(kind: ok) <div><input value="Doe"><div>John D. Doe</div></div>

View File

@@ -1,25 +0,0 @@
// @validateNoDerivedComputationsInEffects_exp
import {useEffect, useState} from 'react';
function Component({firstName}) {
const [lastName, setLastName] = useState('Doe');
const [fullName, setFullName] = useState('John');
const middleName = 'D.';
useEffect(() => {
setFullName(firstName + ' ' + middleName + ' ' + lastName);
}, [firstName, middleName, lastName]);
return (
<div>
<input value={lastName} onChange={e => setLastName(e.target.value)} />
<div>{fullName}</div>
</div>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{firstName: 'John'}],
};

View File

@@ -1,71 +0,0 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects_exp
import {useEffect, useState} from 'react';
function Component({value}) {
const [localValue, setLocalValue] = useState('');
useEffect(() => {
setLocalValue(value);
document.title = `Value: ${value}`;
}, [value]);
return <div>{localValue}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 'test'}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp
import { useEffect, useState } from "react";
function Component(t0) {
const $ = _c(5);
const { value } = t0;
const [localValue, setLocalValue] = useState("");
let t1;
let t2;
if ($[0] !== value) {
t1 = () => {
setLocalValue(value);
document.title = `Value: ${value}`;
};
t2 = [value];
$[0] = value;
$[1] = t1;
$[2] = t2;
} else {
t1 = $[1];
t2 = $[2];
}
useEffect(t1, t2);
let t3;
if ($[3] !== localValue) {
t3 = <div>{localValue}</div>;
$[3] = localValue;
$[4] = t3;
} else {
t3 = $[4];
}
return t3;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ value: "test" }],
};
```
### Eval output
(kind: ok) <div>test</div>

View File

@@ -1,18 +0,0 @@
// @validateNoDerivedComputationsInEffects_exp
import {useEffect, useState} from 'react';
function Component({value}) {
const [localValue, setLocalValue] = useState('');
useEffect(() => {
setLocalValue(value);
document.title = `Value: ${value}`;
}, [value]);
return <div>{localValue}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 'test'}],
};

View File

@@ -1,86 +0,0 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects_exp
import {useEffect, useState} from 'react';
function Component({propValue}) {
const [value, setValue] = useState(null);
function localFunction() {
console.log('local function');
}
useEffect(() => {
setValue(propValue);
localFunction();
}, [propValue]);
return <div>{value}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{propValue: 'test'}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp
import { useEffect, useState } from "react";
function Component(t0) {
const $ = _c(6);
const { propValue } = t0;
const [value, setValue] = useState(null);
let t1;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = function localFunction() {
console.log("local function");
};
$[0] = t1;
} else {
t1 = $[0];
}
const localFunction = t1;
let t2;
let t3;
if ($[1] !== propValue) {
t2 = () => {
setValue(propValue);
localFunction();
};
t3 = [propValue];
$[1] = propValue;
$[2] = t2;
$[3] = t3;
} else {
t2 = $[2];
t3 = $[3];
}
useEffect(t2, t3);
let t4;
if ($[4] !== value) {
t4 = <div>{value}</div>;
$[4] = value;
$[5] = t4;
} else {
t4 = $[5];
}
return t4;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ propValue: "test" }],
};
```
### Eval output
(kind: ok) <div>test</div>
logs: ['local function']

View File

@@ -1,22 +0,0 @@
// @validateNoDerivedComputationsInEffects_exp
import {useEffect, useState} from 'react';
function Component({propValue}) {
const [value, setValue] = useState(null);
function localFunction() {
console.log('local function');
}
useEffect(() => {
setValue(propValue);
localFunction();
}, [propValue]);
return <div>{value}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{propValue: 'test'}],
};

View File

@@ -1,47 +0,0 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects_exp
import {useEffect, useState} from 'react';
function Component({initialName}) {
const [name, setName] = useState('');
useEffect(() => {
setName(initialName);
}, [initialName]);
return (
<div>
<input value={name} onChange={e => setName(e.target.value)} />
</div>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{initialName: 'John'}],
};
```
## Error
```
Found 1 error:
Error: Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
error.derived-state-from-prop-setter-call-outside-effect-no-error.ts:8:4
6 |
7 | useEffect(() => {
> 8 | setName(initialName);
| ^^^^^^^ Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
9 | }, [initialName]);
10 |
11 | return (
```

View File

@@ -1,21 +0,0 @@
// @validateNoDerivedComputationsInEffects_exp
import {useEffect, useState} from 'react';
function Component({initialName}) {
const [name, setName] = useState('');
useEffect(() => {
setName(initialName);
}, [initialName]);
return (
<div>
<input value={name} onChange={e => setName(e.target.value)} />
</div>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{initialName: 'John'}],
};

View File

@@ -1,46 +0,0 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects_exp
import {useEffect, useState} from 'react';
function MockComponent({onSet}) {
return <div onClick={() => onSet('clicked')}>Mock Component</div>;
}
function Component({propValue}) {
const [value, setValue] = useState(null);
useEffect(() => {
setValue(propValue);
}, [propValue]);
return <MockComponent onSet={setValue} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{propValue: 'test'}],
};
```
## Error
```
Found 1 error:
Error: Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
error.derived-state-from-prop-setter-used-outside-effect-no-error.ts:11:4
9 | const [value, setValue] = useState(null);
10 | useEffect(() => {
> 11 | setValue(propValue);
| ^^^^^^^^ Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
12 | }, [propValue]);
13 |
14 | return <MockComponent onSet={setValue} />;
```

View File

@@ -1,20 +0,0 @@
// @validateNoDerivedComputationsInEffects_exp
import {useEffect, useState} from 'react';
function MockComponent({onSet}) {
return <div onClick={() => onSet('clicked')}>Mock Component</div>;
}
function Component({propValue}) {
const [value, setValue] = useState(null);
useEffect(() => {
setValue(propValue);
}, [propValue]);
return <MockComponent onSet={setValue} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{propValue: 'test'}],
};

View File

@@ -1,43 +0,0 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects_exp
import {useEffect, useState} from 'react';
function Component({propValue}) {
const [value, setValue] = useState(null);
useEffect(() => {
setValue(propValue);
globalCall();
}, [propValue]);
return <div>{value}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{propValue: 'test'}],
};
```
## Error
```
Found 1 error:
Error: Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
error.effect-with-global-function-call-no-error.ts:7:4
5 | const [value, setValue] = useState(null);
6 | useEffect(() => {
> 7 | setValue(propValue);
| ^^^^^^^^ Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
8 | globalCall();
9 | }, [propValue]);
10 |
```

View File

@@ -1,17 +0,0 @@
// @validateNoDerivedComputationsInEffects_exp
import {useEffect, useState} from 'react';
function Component({propValue}) {
const [value, setValue] = useState(null);
useEffect(() => {
setValue(propValue);
globalCall();
}, [propValue]);
return <div>{value}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{propValue: 'test'}],
};

View File

@@ -1,73 +0,0 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects_exp
import {useEffect, useState} from 'react';
function Component() {
const [firstName, setFirstName] = useState('Taylor');
const lastName = 'Swift';
// 🔴 Avoid: redundant state and unnecessary Effect
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
return <div>{fullName}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp
import { useEffect, useState } from "react";
function Component() {
const $ = _c(5);
const [firstName] = useState("Taylor");
const [fullName, setFullName] = useState("");
let t0;
let t1;
if ($[0] !== firstName) {
t0 = () => {
setFullName(firstName + " " + "Swift");
};
t1 = [firstName, "Swift"];
$[0] = firstName;
$[1] = t0;
$[2] = t1;
} else {
t0 = $[1];
t1 = $[2];
}
useEffect(t0, t1);
let t2;
if ($[3] !== fullName) {
t2 = <div>{fullName}</div>;
$[3] = fullName;
$[4] = t2;
} else {
t2 = $[4];
}
return t2;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [],
};
```
### Eval output
(kind: ok) <div>Taylor Swift</div>

View File

@@ -1,20 +0,0 @@
// @validateNoDerivedComputationsInEffects_exp
import {useEffect, useState} from 'react';
function Component() {
const [firstName, setFirstName] = useState('Taylor');
const lastName = 'Swift';
// 🔴 Avoid: redundant state and unnecessary Effect
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
return <div>{fullName}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [],
};

View File

@@ -1,72 +0,0 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects_exp
import {useEffect, useState} from 'react';
export default function Component(props) {
const [displayValue, setDisplayValue] = useState('');
useEffect(() => {
const computed = props.prefix + props.value + props.suffix;
setDisplayValue(computed);
}, [props.prefix, props.value, props.suffix]);
return <div>{displayValue}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{prefix: '[', value: 'test', suffix: ']'}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp
import { useEffect, useState } from "react";
export default function Component(props) {
const $ = _c(7);
const [displayValue, setDisplayValue] = useState("");
let t0;
let t1;
if ($[0] !== props.prefix || $[1] !== props.suffix || $[2] !== props.value) {
t0 = () => {
const computed = props.prefix + props.value + props.suffix;
setDisplayValue(computed);
};
t1 = [props.prefix, props.value, props.suffix];
$[0] = props.prefix;
$[1] = props.suffix;
$[2] = props.value;
$[3] = t0;
$[4] = t1;
} else {
t0 = $[3];
t1 = $[4];
}
useEffect(t0, t1);
let t2;
if ($[5] !== displayValue) {
t2 = <div>{displayValue}</div>;
$[5] = displayValue;
$[6] = t2;
} else {
t2 = $[6];
}
return t2;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ prefix: "[", value: "test", suffix: "]" }],
};
```
### Eval output
(kind: ok) <div>[test]</div>

View File

@@ -1,18 +0,0 @@
// @validateNoDerivedComputationsInEffects_exp
import {useEffect, useState} from 'react';
export default function Component(props) {
const [displayValue, setDisplayValue] = useState('');
useEffect(() => {
const computed = props.prefix + props.value + props.suffix;
setDisplayValue(computed);
}, [props.prefix, props.value, props.suffix]);
return <div>{displayValue}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{prefix: '[', value: 'test', suffix: ']'}],
};

View File

@@ -1,74 +0,0 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects_exp
import {useEffect, useState} from 'react';
export default function Component({props}) {
const [fullName, setFullName] = useState(
props.firstName + ' ' + props.lastName
);
useEffect(() => {
setFullName(props.firstName + ' ' + props.lastName);
}, [props.firstName, props.lastName]);
return <div>{fullName}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{props: {firstName: 'John', lastName: 'Doe'}}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp
import { useEffect, useState } from "react";
export default function Component(t0) {
const $ = _c(6);
const { props } = t0;
const [fullName, setFullName] = useState(
props.firstName + " " + props.lastName,
);
let t1;
let t2;
if ($[0] !== props.firstName || $[1] !== props.lastName) {
t1 = () => {
setFullName(props.firstName + " " + props.lastName);
};
t2 = [props.firstName, props.lastName];
$[0] = props.firstName;
$[1] = props.lastName;
$[2] = t1;
$[3] = t2;
} else {
t1 = $[2];
t2 = $[3];
}
useEffect(t1, t2);
let t3;
if ($[4] !== fullName) {
t3 = <div>{fullName}</div>;
$[4] = fullName;
$[5] = t3;
} else {
t3 = $[5];
}
return t3;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ props: { firstName: "John", lastName: "Doe" } }],
};
```
### Eval output
(kind: ok) <div>John Doe</div>

View File

@@ -1,19 +0,0 @@
// @validateNoDerivedComputationsInEffects_exp
import {useEffect, useState} from 'react';
export default function Component({props}) {
const [fullName, setFullName] = useState(
props.firstName + ' ' + props.lastName
);
useEffect(() => {
setFullName(props.firstName + ' ' + props.lastName);
}, [props.firstName, props.lastName]);
return <div>{fullName}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{props: {firstName: 'John', lastName: 'Doe'}}],
};

View File

@@ -3,8 +3,6 @@
```javascript
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function BadExample() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
@@ -12,7 +10,7 @@ function BadExample() {
// 🔴 Avoid: redundant state and unnecessary Effect
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
setFullName(capitalize(firstName + ' ' + lastName));
}, [firstName, lastName]);
return <div>{fullName}</div>;
@@ -28,14 +26,14 @@ Found 1 error:
Error: Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
error.invalid-derived-computation-in-effect.ts:11:4
9 | const [fullName, setFullName] = useState('');
10 | useEffect(() => {
> 11 | setFullName(firstName + ' ' + lastName);
error.invalid-derived-computation-in-effect.ts:9:4
7 | const [fullName, setFullName] = useState('');
8 | useEffect(() => {
> 9 | setFullName(capitalize(firstName + ' ' + lastName));
| ^^^^^^^^^^^ Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
12 | }, [firstName, lastName]);
13 |
14 | return <div>{fullName}</div>;
10 | }, [firstName, lastName]);
11 |
12 | return <div>{fullName}</div>;
```

View File

@@ -1,6 +1,4 @@
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function BadExample() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
@@ -8,7 +6,7 @@ function BadExample() {
// 🔴 Avoid: redundant state and unnecessary Effect
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
setFullName(capitalize(firstName + ' ' + lastName));
}, [firstName, lastName]);
return <div>{fullName}</div>;

View File

@@ -4,15 +4,19 @@
```javascript
// @enableNameAnonymousFunctions
import {useEffect} from 'react';
import {useCallback, useEffect} from 'react';
import {identity, Stringify, useIdentity} from 'shared-runtime';
import * as SharedRuntime from 'shared-runtime';
function Component(props) {
function named() {
const inner = () => props.named;
return inner();
const innerIdentity = identity(() => props.named);
return inner(innerIdentity());
}
const callback = useCallback(() => {
return 'ok';
}, []);
const namedVariable = function () {
return props.namedVariable;
};
@@ -30,6 +34,7 @@ function Component(props) {
return (
<>
{named()}
{callback()}
{namedVariable()}
{methodCall()}
{call()}
@@ -63,7 +68,7 @@ export const TODO_FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableNameAnonymousFunctions
import { useEffect } from "react";
import { useCallback, useEffect } from "react";
import { identity, Stringify, useIdentity } from "shared-runtime";
import * as SharedRuntime from "shared-runtime";
@@ -75,7 +80,12 @@ function Component(props) {
const inner = { "Component[named > inner]": () => props.named }[
"Component[named > inner]"
];
return inner();
const innerIdentity = identity(
{ "Component[named > identity()]": () => props.named }[
"Component[named > identity()]"
],
);
return inner(innerIdentity());
};
$[0] = props.named;
$[1] = t0;
@@ -83,6 +93,8 @@ function Component(props) {
t0 = $[1];
}
const named = t0;
const callback = _ComponentCallback;
let t1;
if ($[2] !== props.namedVariable) {
t1 = {
@@ -197,57 +209,62 @@ function Component(props) {
} else {
t9 = $[18];
}
let t10;
const t10 = callback();
let t11;
if ($[19] !== namedVariable) {
t10 = namedVariable();
t11 = namedVariable();
$[19] = namedVariable;
$[20] = t10;
$[20] = t11;
} else {
t10 = $[20];
}
const t11 = methodCall();
const t12 = call();
let t13;
if ($[21] !== hookArgument) {
t13 = hookArgument();
$[21] = hookArgument;
$[22] = t13;
} else {
t13 = $[22];
t11 = $[20];
}
const t12 = methodCall();
const t13 = call();
let t14;
if ($[21] !== hookArgument) {
t14 = hookArgument();
$[21] = hookArgument;
$[22] = t14;
} else {
t14 = $[22];
}
let t15;
if (
$[23] !== builtinElementAttr ||
$[24] !== namedElementAttr ||
$[25] !== t10 ||
$[26] !== t11 ||
$[27] !== t12 ||
$[28] !== t13 ||
$[25] !== t11 ||
$[26] !== t12 ||
$[27] !== t13 ||
$[28] !== t14 ||
$[29] !== t9
) {
t14 = (
t15 = (
<>
{t9}
{t10}
{t11}
{t12}
{t13}
{builtinElementAttr}
{namedElementAttr}
{t13}
{t14}
</>
);
$[23] = builtinElementAttr;
$[24] = namedElementAttr;
$[25] = t10;
$[26] = t11;
$[27] = t12;
$[28] = t13;
$[25] = t11;
$[26] = t12;
$[27] = t13;
$[28] = t14;
$[29] = t9;
$[30] = t14;
$[30] = t15;
} else {
t14 = $[30];
t15 = $[30];
}
return t14;
return t15;
}
function _ComponentCallback() {
return "ok";
}
export const TODO_FIXTURE_ENTRYPOINT = {

View File

@@ -1,14 +1,18 @@
// @enableNameAnonymousFunctions
import {useEffect} from 'react';
import {useCallback, useEffect} from 'react';
import {identity, Stringify, useIdentity} from 'shared-runtime';
import * as SharedRuntime from 'shared-runtime';
function Component(props) {
function named() {
const inner = () => props.named;
return inner();
const innerIdentity = identity(() => props.named);
return inner(innerIdentity());
}
const callback = useCallback(() => {
return 'ok';
}, []);
const namedVariable = function () {
return props.namedVariable;
};
@@ -26,6 +30,7 @@ function Component(props) {
return (
<>
{named()}
{callback()}
{namedVariable()}
{methodCall()}
{call()}

View File

@@ -0,0 +1,52 @@
## Input
```javascript
// @script
const React = require('react');
function Component(props) {
return <div>{props.name}</div>;
}
// To work with snap evaluator
exports = {
FIXTURE_ENTRYPOINT: {
fn: Component,
params: [{name: 'React Compiler'}],
},
};
```
## Code
```javascript
const { c: _c } = require("react/compiler-runtime"); // @script
const React = require("react");
function Component(props) {
const $ = _c(2);
let t0;
if ($[0] !== props.name) {
t0 = <div>{props.name}</div>;
$[0] = props.name;
$[1] = t0;
} else {
t0 = $[1];
}
return t0;
}
// To work with snap evaluator
exports = {
FIXTURE_ENTRYPOINT: {
fn: Component,
params: [{ name: "React Compiler" }],
},
};
```
### Eval output
(kind: ok) <div>React Compiler</div>

View File

@@ -0,0 +1,14 @@
// @script
const React = require('react');
function Component(props) {
return <div>{props.name}</div>;
}
// To work with snap evaluator
exports = {
FIXTURE_ENTRYPOINT: {
fn: Component,
params: [{name: 'React Compiler'}],
},
};

View File

@@ -31,10 +31,15 @@ import prettier from 'prettier';
import SproutTodoFilter from './SproutTodoFilter';
import {isExpectError} from './fixture-utils';
import {makeSharedRuntimeTypeProvider} from './sprout/shared-runtime-type-provider';
export function parseLanguage(source: string): 'flow' | 'typescript' {
return source.indexOf('@flow') !== -1 ? 'flow' : 'typescript';
}
export function parseSourceType(source: string): 'script' | 'module' {
return source.indexOf('@script') !== -1 ? 'script' : 'module';
}
/**
* Parse react compiler plugin + environment options from test fixture. Note
* that although this primarily uses `Environment:parseConfigPragma`, it also
@@ -98,6 +103,7 @@ export function parseInput(
input: string,
filename: string,
language: 'flow' | 'typescript',
sourceType: 'module' | 'script',
): BabelCore.types.File {
// Extract the first line to quickly check for custom test directives
if (language === 'flow') {
@@ -105,14 +111,14 @@ export function parseInput(
babel: true,
flow: 'all',
sourceFilename: filename,
sourceType: 'module',
sourceType,
enableExperimentalComponentSyntax: true,
});
} else {
return BabelParser.parse(input, {
sourceFilename: filename,
plugins: ['typescript', 'jsx'],
sourceType: 'module',
sourceType,
});
}
}
@@ -221,11 +227,12 @@ export async function transformFixtureInput(
const firstLine = input.substring(0, input.indexOf('\n'));
const language = parseLanguage(firstLine);
const sourceType = parseSourceType(firstLine);
// Preserve file extension as it determines typescript's babel transform
// mode (e.g. stripping types, parsing rules for brackets)
const filename =
path.basename(fixturePath) + (language === 'typescript' ? '.ts' : '');
const inputAst = parseInput(input, filename, language);
const inputAst = parseInput(input, filename, language, sourceType);
// Give babel transforms an absolute path as relative paths get prefixed
// with `cwd`, which is different across machines
const virtualFilepath = '/' + filename;

View File

@@ -298,7 +298,10 @@ export function doEval(source: string): EvaluatorResult {
return {
kind: 'UnexpectedError',
value:
'Unexpected error during eval, possible syntax error?\n' + e.message,
'Unexpected error during eval, possible syntax error?\n' +
e.message +
'\n\nsource:\n' +
source,
logs,
};
} finally {

View File

@@ -1430,6 +1430,72 @@ if (__EXPERIMENTAL__) {
}
`,
},
{
code: normalizeIndent`
// Valid because functions created with useEffectEvent can be called in useLayoutEffect.
function MyComponent({ theme }) {
const onClick = useEffectEvent(() => {
showNotification(theme);
});
useLayoutEffect(() => {
onClick();
});
React.useLayoutEffect(() => {
onClick();
});
}
`,
},
{
code: normalizeIndent`
// Valid because functions created with useEffectEvent can be called in useInsertionEffect.
function MyComponent({ theme }) {
const onClick = useEffectEvent(() => {
showNotification(theme);
});
useInsertionEffect(() => {
onClick();
});
React.useInsertionEffect(() => {
onClick();
});
}
`,
},
{
code: normalizeIndent`
// Valid because functions created with useEffectEvent can be passed by reference in useLayoutEffect
// and useInsertionEffect.
function MyComponent({ theme }) {
const onClick = useEffectEvent(() => {
showNotification(theme);
});
const onClick2 = useEffectEvent(() => {
debounce(onClick);
debounce(() => onClick());
debounce(() => { onClick() });
deboucne(() => debounce(onClick));
});
useLayoutEffect(() => {
let id = setInterval(() => onClick(), 100);
return () => clearInterval(onClick);
}, []);
React.useLayoutEffect(() => {
let id = setInterval(() => onClick(), 100);
return () => clearInterval(onClick);
}, []);
useInsertionEffect(() => {
let id = setInterval(() => onClick(), 100);
return () => clearInterval(onClick);
}, []);
React.useInsertionEffect(() => {
let id = setInterval(() => onClick(), 100);
return () => clearInterval(onClick);
}, []);
return null;
}
`,
},
];
allTests.invalid = [
...allTests.invalid,

View File

@@ -147,8 +147,8 @@ function getNodeWithoutReactNamespace(
return node;
}
function isUseEffectIdentifier(node: Node): boolean {
return node.type === 'Identifier' && node.name === 'useEffect';
function isEffectIdentifier(node: Node): boolean {
return node.type === 'Identifier' && (node.name === 'useEffect' || node.name === 'useLayoutEffect' || node.name === 'useInsertionEffect');
}
function isUseEffectEventIdentifier(node: Node): boolean {
if (__EXPERIMENTAL__) {
@@ -726,7 +726,7 @@ const rule = {
// Check all `useEffect` and `React.useEffect`, `useEffectEvent`, and `React.useEffectEvent`
const nodeWithoutNamespace = getNodeWithoutReactNamespace(node.callee);
if (
(isUseEffectIdentifier(nodeWithoutNamespace) ||
(isEffectIdentifier(nodeWithoutNamespace) ||
isUseEffectEventIdentifier(nodeWithoutNamespace)) &&
node.arguments.length > 0
) {

View File

@@ -28,6 +28,7 @@
"@babel/plugin-transform-modules-commonjs": "^7.10.4",
"@babel/plugin-transform-react-jsx-source": "^7.10.5",
"@babel/preset-react": "^7.10.4",
"@jridgewell/sourcemap-codec": "1.5.5",
"acorn-jsx": "^5.2.0",
"archiver": "^3.0.0",
"babel-core": "^7.0.0-bridge",
@@ -60,7 +61,6 @@
"raw-loader": "^3.1.0",
"rimraf": "^5.0.1",
"source-map-js": "^0.6.2",
"sourcemap-codec": "^1.4.8",
"style-loader": "^0.23.1",
"webpack": "^5.82.1",
"webpack-cli": "^5.1.1",

View File

@@ -23,7 +23,7 @@ function fetchResource(url) {
});
};
fetch(url, {cache: 'force-cache'}).then(
fetch(url, {cache: 'force-cache', signal: AbortSignal.timeout(60000)}).then(
response => {
if (response.ok) {
response

View File

@@ -78,6 +78,18 @@ const fetchFromNetworkCache = (url, resolve, reject) => {
});
};
const pendingFetchRequests = new Set();
function pendingFetchRequestsCleanup({payload, source}) {
if (source === 'react-devtools-background') {
switch (payload?.type) {
case 'fetch-file-with-cache-complete':
case 'fetch-file-with-cache-error':
pendingFetchRequests.delete(payload.url);
}
}
}
chrome.runtime.onMessage.addListener(pendingFetchRequestsCleanup);
const fetchFromPage = async (url, resolve, reject) => {
debugLog('[main] fetchFromPage()', url);
@@ -97,7 +109,11 @@ const fetchFromPage = async (url, resolve, reject) => {
}
chrome.runtime.onMessage.addListener(onPortMessage);
if (pendingFetchRequests.has(url)) {
return;
}
pendingFetchRequests.add(url);
chrome.runtime.sendMessage({
source: 'devtools-page',
payload: {

View File

@@ -22,8 +22,8 @@
"test:e2e": "playwright test --config=playwright.config.js"
},
"dependencies": {
"source-map-js": "^0.6.2",
"sourcemap-codec": "^1.4.8"
"@jridgewell/sourcemap-codec": "1.5.5",
"source-map-js": "^0.6.2"
},
"devDependencies": {
"@babel/core": "^7.11.1",

View File

@@ -130,24 +130,28 @@ describe('Timeline profiler', () => {
// @reactVersion <= 18.2
// @reactVersion >= 18.0
it('should mark sync render without suspends or state updates', () => {
utils.act(() => store.profilerStore.startProfiling());
legacyRender(<div />);
utils.act(() => store.profilerStore.stopProfiling());
expect(registeredMarks).toMatchInlineSnapshot(`
[
"--schedule-render-1",
"--render-start-1",
"--render-stop",
"--commit-start-1",
"--react-version-<filtered-version>",
"--profiler-version-1",
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
"--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
"--layout-effects-start-1",
"--layout-effects-stop",
"--commit-stop",
]
`);
[
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
"--schedule-render-1",
"--render-start-1",
"--render-stop",
"--commit-start-1",
"--react-version-<filtered-version>",
"--profiler-version-1",
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
"--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
"--layout-effects-start-1",
"--layout-effects-stop",
"--commit-stop",
]
`);
});
// TODO(hoxyq): investigate why running this test with React 18 fails
@@ -260,46 +264,50 @@ describe('Timeline profiler', () => {
throw Error('Expected error');
}
utils.act(() => store.profilerStore.startProfiling());
legacyRender(
<ErrorBoundary>
<ExampleThatThrows />
</ErrorBoundary>,
);
utils.act(() => store.profilerStore.stopProfiling());
expect(registeredMarks).toMatchInlineSnapshot(`
[
"--schedule-render-1",
"--render-start-1",
"--component-render-start-ErrorBoundary",
"--component-render-stop",
"--component-render-start-ExampleThatThrows",
"--component-render-start-ExampleThatThrows",
"--component-render-stop",
"--error-ExampleThatThrows-mount-Expected error",
"--render-stop",
"--commit-start-1",
"--react-version-<filtered-version>",
"--profiler-version-1",
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
"--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
"--layout-effects-start-1",
"--schedule-state-update-1-ErrorBoundary",
"--layout-effects-stop",
"--commit-stop",
"--render-start-1",
"--component-render-start-ErrorBoundary",
"--component-render-stop",
"--render-stop",
"--commit-start-1",
"--react-version-<filtered-version>",
"--profiler-version-1",
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
"--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
"--commit-stop",
]
`);
[
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
"--schedule-render-1",
"--render-start-1",
"--component-render-start-ErrorBoundary",
"--component-render-stop",
"--component-render-start-ExampleThatThrows",
"--component-render-start-ExampleThatThrows",
"--component-render-stop",
"--error-ExampleThatThrows-mount-Expected error",
"--render-stop",
"--commit-start-1",
"--react-version-<filtered-version>",
"--profiler-version-1",
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
"--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
"--layout-effects-start-1",
"--schedule-state-update-1-ErrorBoundary",
"--layout-effects-stop",
"--commit-stop",
"--render-start-1",
"--component-render-start-ErrorBoundary",
"--component-render-stop",
"--render-stop",
"--commit-start-1",
"--react-version-<filtered-version>",
"--profiler-version-1",
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
"--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
"--commit-stop",
]
`);
});
});
@@ -1095,24 +1103,28 @@ describe('Timeline profiler', () => {
// @reactVersion <= 18.2
// @reactVersion >= 18.0
it('regression test SyncLane', () => {
utils.act(() => store.profilerStore.startProfiling());
legacyRender(<div />);
utils.act(() => store.profilerStore.stopProfiling());
expect(registeredMarks).toMatchInlineSnapshot(`
[
"--schedule-render-1",
"--render-start-1",
"--render-stop",
"--commit-start-1",
"--react-version-<filtered-version>",
"--profiler-version-1",
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
"--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
"--layout-effects-start-1",
"--layout-effects-stop",
"--commit-stop",
]
`);
[
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
"--schedule-render-1",
"--render-start-1",
"--render-stop",
"--commit-start-1",
"--react-version-<filtered-version>",
"--profiler-version-1",
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
"--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
"--layout-effects-start-1",
"--layout-effects-stop",
"--commit-stop",
]
`);
});
});
@@ -1432,19 +1444,19 @@ describe('Timeline profiler', () => {
expect(timelineData.suspenseEvents).toHaveLength(1);
const suspenseEvent = timelineData.suspenseEvents[0];
expect(suspenseEvent).toMatchInlineSnapshot(`
{
"componentName": "Example",
"depth": 0,
"duration": 10,
"id": "0",
"phase": "mount",
"promiseName": "",
"resolution": "resolved",
"timestamp": 10,
"type": "suspense",
"warning": null,
}
`);
{
"componentName": "Example",
"depth": 0,
"duration": 0,
"id": "0",
"phase": "mount",
"promiseName": "",
"resolution": "unresolved",
"timestamp": 10,
"type": "suspense",
"warning": null,
}
`);
// There should be two batches of renders: Suspeneded and resolved.
expect(timelineData.batchUIDToMeasuresMap.size).toBe(2);
@@ -1490,19 +1502,19 @@ describe('Timeline profiler', () => {
expect(timelineData.suspenseEvents).toHaveLength(1);
const suspenseEvent = timelineData.suspenseEvents[0];
expect(suspenseEvent).toMatchInlineSnapshot(`
{
"componentName": "Example",
"depth": 0,
"duration": 10,
"id": "0",
"phase": "mount",
"promiseName": "",
"resolution": "rejected",
"timestamp": 10,
"type": "suspense",
"warning": null,
}
`);
{
"componentName": "Example",
"depth": 0,
"duration": 0,
"id": "0",
"phase": "mount",
"promiseName": "",
"resolution": "unresolved",
"timestamp": 10,
"type": "suspense",
"warning": null,
}
`);
// There should be two batches of renders: Suspeneded and resolved.
expect(timelineData.batchUIDToMeasuresMap.size).toBe(2);

View File

@@ -111,9 +111,31 @@ describe('Timeline profiler', () => {
ReactDOMClient = require('react-dom/client');
Scheduler = require('scheduler');
const InternalTestUtils = require('internal-test-utils');
assertLog = InternalTestUtils.assertLog;
waitFor = InternalTestUtils.waitFor;
if (typeof Scheduler.log !== 'function') {
// backwards compat for older scheduler versions
Scheduler.log = Scheduler.unstable_yieldValue;
Scheduler.unstable_clearLog = Scheduler.unstable_clearYields;
const InternalTestUtils = require('internal-test-utils');
assertLog = InternalTestUtils.assertLog;
// polyfill waitFor as Scheduler.toFlushAndYieldThrough
waitFor = expectedYields => {
let actualYields = Scheduler.unstable_clearYields();
if (actualYields.length !== 0) {
throw new Error(
'Log of yielded values is not empty. ' +
'Call expect(Scheduler).toHaveYielded(...) first.',
);
}
Scheduler.unstable_flushNumberOfYields(expectedYields.length);
actualYields = Scheduler.unstable_clearYields();
expect(actualYields).toEqual(expectedYields);
};
} else {
const InternalTestUtils = require('internal-test-utils');
assertLog = InternalTestUtils.assertLog;
waitFor = InternalTestUtils.waitFor;
}
setPerformanceMock =
require('react-devtools-shared/src/backend/profilingHooks').setPerformanceMock_ONLY_FOR_TESTING;

View File

@@ -725,14 +725,14 @@ describe('ProfilingCache', () => {
const commitData = store.profilerStore.getDataForRoot(rootID).commitData;
expect(commitData).toHaveLength(2);
const isLegacySuspense = React.version.startsWith('17');
if (isLegacySuspense) {
if (React.version.startsWith('17')) {
// React 17 will mount all children until it suspends in a LegacyHidden
// The ID gap is from the Fiber for <Async> that's in the disconnected tree.
expect(commitData[0].fiberActualDurations).toMatchInlineSnapshot(`
Map {
1 => 15,
2 => 15,
3 => 5,
4 => 3,
5 => 2,
}
`);
@@ -741,7 +741,6 @@ describe('ProfilingCache', () => {
1 => 0,
2 => 10,
3 => 3,
4 => 3,
5 => 2,
}
`);

View File

@@ -19,8 +19,6 @@ describe('commit tree', () => {
let Scheduler;
let store: Store;
let utils;
const isLegacySuspense =
React.version.startsWith('16') || React.version.startsWith('17');
beforeEach(() => {
utils = require('./utils');
@@ -186,24 +184,13 @@ describe('commit tree', () => {
utils.act(() => store.profilerStore.startProfiling());
utils.act(() => legacyRender(<App renderChildren={true} />));
await Promise.resolve();
if (isLegacySuspense) {
expect(store).toMatchInlineSnapshot(`
[root]
<App>
▾ <Suspense>
<Lazy>
[suspense-root] rects={null}
<Suspense name="App" rects={null}>
`);
} else {
expect(store).toMatchInlineSnapshot(`
[root]
▾ <App>
<Suspense>
[suspense-root] rects={null}
<Suspense name="App" rects={null}>
`);
}
expect(store).toMatchInlineSnapshot(`
[root]
▾ <App>
<Suspense>
[suspense-root] rects={null}
<Suspense name="App" rects={null}>
`);
utils.act(() => legacyRender(<App renderChildren={true} />));
expect(store).toMatchInlineSnapshot(`
[root]
@@ -231,13 +218,7 @@ describe('commit tree', () => {
);
}
expect(commitTrees[0].nodes.size).toBe(
isLegacySuspense
? // <Root> + <App> + <Suspense> + <Lazy>
4
: // <Root> + <App> + <Suspense>
3,
);
expect(commitTrees[0].nodes.size).toBe(3);
expect(commitTrees[1].nodes.size).toBe(4); // <Root> + <App> + <Suspense> + <LazyInnerComponent>
expect(commitTrees[2].nodes.size).toBe(2); // <Root> + <App>
});
@@ -291,24 +272,13 @@ describe('commit tree', () => {
it('should support Lazy components that are unmounted before resolving (legacy render)', async () => {
utils.act(() => store.profilerStore.startProfiling());
utils.act(() => legacyRender(<App renderChildren={true} />));
if (isLegacySuspense) {
expect(store).toMatchInlineSnapshot(`
[root]
<App>
▾ <Suspense>
<Lazy>
[suspense-root] rects={null}
<Suspense name="App" rects={null}>
`);
} else {
expect(store).toMatchInlineSnapshot(`
[root]
▾ <App>
<Suspense>
[suspense-root] rects={null}
<Suspense name="App" rects={null}>
`);
}
expect(store).toMatchInlineSnapshot(`
[root]
▾ <App>
<Suspense>
[suspense-root] rects={null}
<Suspense name="App" rects={null}>
`);
utils.act(() => legacyRender(<App renderChildren={false} />));
expect(store).toMatchInlineSnapshot(`
[root]
@@ -327,13 +297,7 @@ describe('commit tree', () => {
);
}
expect(commitTrees[0].nodes.size).toBe(
isLegacySuspense
? // <Root> + <App> + <Suspense> + <Lazy>
4
: // <Root> + <App> + <Suspense>
3,
);
expect(commitTrees[0].nodes.size).toBe(3);
expect(commitTrees[1].nodes.size).toBe(2); // <Root> + <App>
});

View File

@@ -2828,7 +2828,7 @@ describe('Store', () => {
`);
});
// @reactVersion >= 18.0
// @reactVersion >= 17.0
it('can reconcile Suspense in fallback positions', async () => {
let resolveFallback;
const fallbackPromise = new Promise(resolve => {
@@ -2907,7 +2907,7 @@ describe('Store', () => {
`);
});
// @reactVersion >= 18.0
// @reactVersion >= 17.0
it('can reconcile resuspended Suspense with Suspense in fallback positions', async () => {
let resolveHeadFallback;
let resolveHeadContent;

View File

@@ -460,10 +460,10 @@ export function getInternalReactConstants(version: string): {
IncompleteFunctionComponent: 28,
IndeterminateComponent: 2, // removed in 19.0.0
LazyComponent: 16,
LegacyHiddenComponent: 23,
LegacyHiddenComponent: 23, // Does not exist in 18+ OSS but exists in fb builds
MemoComponent: 14,
Mode: 8,
OffscreenComponent: 22, // Experimental
OffscreenComponent: 22, // Experimental in 17. Stable in 18+
Profiler: 12,
ScopeComponent: 21, // Experimental
SimpleMemoComponent: 15,
@@ -3057,13 +3057,23 @@ export function attach(
}
}
function isHiddenOffscreen(fiber: Fiber): boolean {
switch (fiber.tag) {
case LegacyHiddenComponent:
// fallthrough since all published implementations currently implement the same state as Offscreen.
case OffscreenComponent:
return fiber.memoizedState !== null;
default:
return false;
}
}
function unmountRemainingChildren() {
if (
reconcilingParent !== null &&
(reconcilingParent.kind === FIBER_INSTANCE ||
reconcilingParent.kind === FILTERED_FIBER_INSTANCE) &&
reconcilingParent.data.tag === OffscreenComponent &&
reconcilingParent.data.memoizedState !== null &&
isHiddenOffscreen(reconcilingParent.data) &&
!isInDisconnectedSubtree
) {
// This is a hidden offscreen, we need to execute this in the context of a disconnected subtree.
@@ -3170,8 +3180,7 @@ export function attach(
if (
(parent.kind === FIBER_INSTANCE ||
parent.kind === FILTERED_FIBER_INSTANCE) &&
parent.data.tag === OffscreenComponent &&
parent.data.memoizedState !== null
isHiddenOffscreen(parent.data)
) {
// We're inside a hidden offscreen Fiber. We're in a disconnected tree.
return;
@@ -3819,7 +3828,9 @@ export function attach(
(reconcilingParent !== null &&
reconcilingParent.kind === VIRTUAL_INSTANCE) ||
fiber.tag === SuspenseComponent ||
fiber.tag === OffscreenComponent // Use to keep resuspended instances alive inside a SuspenseComponent.
// Use to keep resuspended instances alive inside a SuspenseComponent.
fiber.tag === OffscreenComponent ||
fiber.tag === LegacyHiddenComponent
) {
// If the parent is a Virtual Instance and we filtered this Fiber we include a
// hidden node. We also include this if it's a Suspense boundary so we can track those
@@ -3939,7 +3950,7 @@ export function attach(
trackDebugInfoFromHostComponent(nearestInstance, fiber);
}
if (fiber.tag === OffscreenComponent && fiber.memoizedState !== null) {
if (isHiddenOffscreen(fiber)) {
// If an Offscreen component is hidden, mount its children as disconnected.
const stashedDisconnected = isInDisconnectedSubtree;
isInDisconnectedSubtree = true;
@@ -4261,7 +4272,7 @@ export function attach(
while (child !== null) {
if (child.kind === FILTERED_FIBER_INSTANCE) {
const fiber = child.data;
if (fiber.tag === OffscreenComponent && fiber.memoizedState !== null) {
if (isHiddenOffscreen(fiber)) {
// The children of this Offscreen are hidden so they don't get added.
} else {
addUnfilteredChildrenIDs(child, nextChildren);
@@ -4888,9 +4899,8 @@ export function attach(
const nextDidTimeOut =
isLegacySuspense && nextFiber.memoizedState !== null;
const isOffscreen = nextFiber.tag === OffscreenComponent;
const prevWasHidden = isOffscreen && prevFiber.memoizedState !== null;
const nextIsHidden = isOffscreen && nextFiber.memoizedState !== null;
const prevWasHidden = isHiddenOffscreen(prevFiber);
const nextIsHidden = isHiddenOffscreen(nextFiber);
if (isLegacySuspense) {
if (
@@ -5245,8 +5255,7 @@ export function attach(
if (
(child.kind === FIBER_INSTANCE ||
child.kind === FILTERED_FIBER_INSTANCE) &&
child.data.tag === OffscreenComponent &&
child.data.memoizedState !== null
isHiddenOffscreen(child.data)
) {
// This instance's children are already disconnected.
} else {
@@ -5275,8 +5284,7 @@ export function attach(
if (
(child.kind === FIBER_INSTANCE ||
child.kind === FILTERED_FIBER_INSTANCE) &&
child.data.tag === OffscreenComponent &&
child.data.memoizedState !== null
isHiddenOffscreen(child.data)
) {
// This instance's children should remain disconnected.
} else {

View File

@@ -7,7 +7,7 @@
* @flow
*/
import {withSyncPerfMeasurements} from 'react-devtools-shared/src/PerformanceLoggingUtils';
import {decode} from 'sourcemap-codec';
import {decode} from '@jridgewell/sourcemap-codec';
import type {
IndexSourceMap,
@@ -47,7 +47,7 @@ export default function SourceMapConsumer(
function BasicSourceMapConsumer(sourceMapJSON: BasicSourceMap) {
const decodedMappings: Mappings = withSyncPerfMeasurements(
'Decoding source map mappings with sourcemap-codec',
'Decoding source map mappings with @jridgewell/sourcemap-codec',
() => decode(sourceMapJSON.mappings),
);

View File

@@ -14,7 +14,7 @@ const babel = require('@rollup/plugin-babel').babel;
const commonjs = require('@rollup/plugin-commonjs');
const jsx = require('acorn-jsx');
const rollupResolve = require('@rollup/plugin-node-resolve').nodeResolve;
const {encode, decode} = require('sourcemap-codec');
const {encode, decode} = require('@jridgewell/sourcemap-codec');
const {generateEncodedHookMap} = require('../generateHookMap');
const {parse} = require('@babel/parser');

View File

@@ -8,7 +8,7 @@
*/
import {getHookNamesMappingFromAST} from './astUtils';
import {encode, decode} from 'sourcemap-codec';
import {encode, decode} from '@jridgewell/sourcemap-codec';
// Missing types in @babel/types
type File = any;

View File

@@ -2320,6 +2320,9 @@ export function startViewTransition(
mutationCallback();
layoutCallback();
// Skip afterMutationCallback(). We don't need it since we're not animating.
if (enableProfilerTimer) {
finishedAnimation();
}
spawnedWorkCallback();
// Skip passiveCallback(). Spawned work will schedule a task.
return null;
@@ -2509,6 +2512,7 @@ export function startGestureTransition(
mutationCallback: () => void,
animateCallback: () => void,
errorCallback: mixed => void,
finishedAnimation: () => void, // Profiling-only
): null | RunningViewTransition {
const ownerDocument: Document =
rootContainer.nodeType === DOCUMENT_NODE
@@ -2723,6 +2727,12 @@ export function startGestureTransition(
// $FlowFixMe[prop-missing]
ownerDocument.__reactViewTransition = null;
}
if (enableProfilerTimer) {
// Signal that the Transition was unable to continue. We do that here
// instead of when we stop the running View Transition to ensure that
// we cover cases when something else stops it early.
finishedAnimation();
}
});
return transition;
} catch (x) {
@@ -2735,6 +2745,9 @@ export function startGestureTransition(
// Run through the sequence to put state back into a consistent state.
mutationCallback();
animateCallback();
if (enableProfilerTimer) {
finishedAnimation();
}
return null;
}
}

View File

@@ -1613,6 +1613,116 @@ describe('FragmentRefs', () => {
);
});
// @gate enableFragmentRefs
it('compares a root-level Fragment', async () => {
const fragmentRef = React.createRef();
const emptyFragmentRef = React.createRef();
const childRef = React.createRef();
const siblingPrecedingRef = React.createRef();
const siblingFollowingRef = React.createRef();
const root = ReactDOMClient.createRoot(container);
function Test() {
return (
<Fragment>
<div ref={siblingPrecedingRef} />
<Fragment ref={fragmentRef}>
<div ref={childRef} />
</Fragment>
<Fragment ref={emptyFragmentRef} />
<div ref={siblingFollowingRef} />
</Fragment>
);
}
await act(() => root.render(<Test />));
const fragmentInstance = fragmentRef.current;
if (fragmentInstance == null) {
throw new Error('Expected fragment instance to be non-null');
}
const emptyFragmentInstance = emptyFragmentRef.current;
if (emptyFragmentInstance == null) {
throw new Error('Expected empty fragment instance to be non-null');
}
expectPosition(
fragmentInstance.compareDocumentPosition(childRef.current),
{
preceding: false,
following: false,
contains: false,
containedBy: true,
disconnected: false,
implementationSpecific: false,
},
);
expectPosition(
fragmentInstance.compareDocumentPosition(siblingPrecedingRef.current),
{
preceding: true,
following: false,
contains: false,
containedBy: false,
disconnected: false,
implementationSpecific: false,
},
);
expectPosition(
fragmentInstance.compareDocumentPosition(siblingFollowingRef.current),
{
preceding: false,
following: true,
contains: false,
containedBy: false,
disconnected: false,
implementationSpecific: false,
},
);
expectPosition(
emptyFragmentInstance.compareDocumentPosition(childRef.current),
{
preceding: true,
following: false,
contains: false,
containedBy: false,
disconnected: false,
implementationSpecific: true,
},
);
expectPosition(
emptyFragmentInstance.compareDocumentPosition(
siblingPrecedingRef.current,
),
{
preceding: true,
following: false,
contains: false,
containedBy: false,
disconnected: false,
implementationSpecific: true,
},
);
expectPosition(
emptyFragmentInstance.compareDocumentPosition(
siblingFollowingRef.current,
),
{
preceding: false,
following: true,
contains: false,
containedBy: false,
disconnected: false,
implementationSpecific: true,
},
);
});
describe('with portals', () => {
// @gate enableFragmentRefs
it('handles portaled elements', async () => {

View File

@@ -25,7 +25,6 @@ import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
import {HostText} from 'react-reconciler/src/ReactWorkTags';
import {
getFragmentParentHostFiber,
getInstanceFromHostFiber,
traverseFragmentInstance,
} from 'react-reconciler/src/ReactFiberTreeReflection';
@@ -303,6 +302,13 @@ export function getPublicInstance(instance: Instance): null | PublicInstance {
return instance.canonical.publicInstance;
}
// Handle root containers
if (instance.containerInfo != null) {
if (instance.containerInfo.publicInstance != null) {
return instance.containerInfo.publicInstance;
}
}
// For compatibility with the legacy renderer, in case it's used with Fabric
// in the same app.
// $FlowExpectedError[prop-missing]
@@ -347,9 +353,8 @@ export function getPublicInstanceFromInternalInstanceHandle(
}
function getPublicInstanceFromHostFiber(fiber: Fiber): PublicInstance {
const instance = getInstanceFromHostFiber<Instance>(fiber);
const publicInstance = getPublicInstance(instance);
if (publicInstance == null) {
const publicInstance = getPublicInstance(fiber.stateNode);
if (publicInstance === null) {
throw new Error('Expected to find a host node. This is a bug in React.');
}
return publicInstance;
@@ -698,11 +703,11 @@ FragmentInstance.prototype.compareDocumentPosition = function (
if (parentHostFiber === null) {
return Node.DOCUMENT_POSITION_DISCONNECTED;
}
const parentHostInstance = getPublicInstanceFromHostFiber(parentHostFiber);
const children: Array<Fiber> = [];
traverseFragmentInstance(this._fragmentFiber, collectChildren, children);
if (children.length === 0) {
return compareDocumentPositionForEmptyFragment(
const parentHostInstance = getPublicInstanceFromHostFiber(parentHostFiber);
return compareDocumentPositionForEmptyFragment<PublicInstance>(
this._fragmentFiber,
parentHostInstance,
otherNode,

View File

@@ -35,6 +35,8 @@ import {
} from 'react-reconciler/src/ReactEventPriorities';
import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
import {enableProfilerTimer} from 'shared/ReactFeatureFlags';
import {REACT_CONTEXT_TYPE} from 'shared/ReactSymbols';
import type {ReactContext} from 'shared/ReactTypes';
@@ -680,6 +682,9 @@ export function startViewTransition(
layoutCallback();
// Skip afterMutationCallback(). We don't need it since we're not animating.
spawnedWorkCallback();
if (enableProfilerTimer) {
finishedAnimation();
}
// Skip passiveCallback(). Spawned work will schedule a task.
return null;
}
@@ -696,9 +701,13 @@ export function startGestureTransition(
mutationCallback: () => void,
animateCallback: () => void,
errorCallback: mixed => void,
finishedAnimation: () => void, // Profiling-only
): null | RunningViewTransition {
mutationCallback();
animateCallback();
if (enableProfilerTimer) {
finishedAnimation();
}
return null;
}

View File

@@ -77,6 +77,12 @@ import {
getViewTransitionClassName,
} from './ReactFiberViewTransitionComponent';
import {
enableProfilerTimer,
enableComponentPerformanceTrack,
} from 'shared/ReactFeatureFlags';
import {trackAnimatingTask} from './ReactProfilerTimer';
let didWarnForRootClone = false;
// Used during the apply phase to track whether a parent ViewTransition component
@@ -101,6 +107,7 @@ function applyViewTransitionToClones(
name: string,
className: ?string,
clones: Array<Instance>,
fiber: Fiber,
): void {
// This gets called when we have found a pair, but after the clone in created. The clone is
// created by the insertion side. If the insertion side if found before the deletion side
@@ -117,6 +124,11 @@ function applyViewTransitionToClones(
className,
);
}
if (enableProfilerTimer && enableComponentPerformanceTrack) {
if (fiber._debugTask != null) {
trackAnimatingTask(fiber._debugTask);
}
}
}
function trackDeletedPairViewTransitions(deletion: Fiber): void {
@@ -171,7 +183,7 @@ function trackDeletedPairViewTransitions(deletion: Fiber): void {
// If we have clones that means that we've already visited this
// ViewTransition boundary before and we can now apply the name
// to those clones. Otherwise, we have to wait until we clone it.
applyViewTransitionToClones(name, className, clones);
applyViewTransitionToClones(name, className, clones, child);
}
}
if (pairs.size === 0) {
@@ -221,7 +233,7 @@ function trackEnterViewTransitions(deletion: Fiber): void {
// If we have clones that means that we've already visited this
// ViewTransition boundary before and we can now apply the name
// to those clones. Otherwise, we have to wait until we clone it.
applyViewTransitionToClones(name, className, clones);
applyViewTransitionToClones(name, className, clones, deletion);
}
}
}
@@ -266,7 +278,7 @@ function applyAppearingPairViewTransition(child: Fiber): void {
// If there are no clones at this point, that should mean that there are no
// HostComponent children in this ViewTransition.
if (clones !== null) {
applyViewTransitionToClones(name, className, clones);
applyViewTransitionToClones(name, className, clones, child);
}
}
}
@@ -296,7 +308,7 @@ function applyExitViewTransition(placement: Fiber): void {
// If there are no clones at this point, that should mean that there are no
// HostComponent children in this ViewTransition.
if (clones !== null) {
applyViewTransitionToClones(name, className, clones);
applyViewTransitionToClones(name, className, clones, placement);
}
}
}
@@ -314,7 +326,7 @@ function applyNestedViewTransition(child: Fiber): void {
// If there are no clones at this point, that should mean that there are no
// HostComponent children in this ViewTransition.
if (clones !== null) {
applyViewTransitionToClones(name, className, clones);
applyViewTransitionToClones(name, className, clones, child);
}
}
}
@@ -346,7 +358,7 @@ function applyUpdateViewTransition(current: Fiber, finishedWork: Fiber): void {
// If there are no clones at this point, that should mean that there are no
// HostComponent children in this ViewTransition.
if (clones !== null) {
applyViewTransitionToClones(oldName, className, clones);
applyViewTransitionToClones(oldName, className, clones, finishedWork);
}
}

View File

@@ -28,6 +28,7 @@ import {
retryLaneExpirationMs,
disableLegacyMode,
enableDefaultTransitionIndicator,
enableGestureTransition,
} from 'shared/ReactFeatureFlags';
import {isDevToolsPresent} from './ReactFiberDevToolsHook';
import {clz32} from './clz32';
@@ -611,10 +612,6 @@ export function includesSyncLane(lanes: Lanes): boolean {
return (lanes & (SyncLane | SyncHydrationLane)) !== NoLanes;
}
export function isSyncLane(lanes: Lanes): boolean {
return (lanes & (SyncLane | SyncHydrationLane)) !== NoLanes;
}
export function includesNonIdleWork(lanes: Lanes): boolean {
return (lanes & NonIdleLanes) !== NoLanes;
}
@@ -681,6 +678,8 @@ export function includesLoadingIndicatorLanes(lanes: Lanes): boolean {
export function includesBlockingLane(lanes: Lanes): boolean {
const SyncDefaultLanes =
SyncHydrationLane |
SyncLane |
InputContinuousHydrationLane |
InputContinuousLane |
DefaultHydrationLane |
@@ -697,10 +696,13 @@ export function includesExpiredLane(root: FiberRoot, lanes: Lanes): boolean {
export function isBlockingLane(lane: Lane): boolean {
const SyncDefaultLanes =
SyncHydrationLane |
SyncLane |
InputContinuousHydrationLane |
InputContinuousLane |
DefaultHydrationLane |
DefaultLane;
DefaultLane |
GestureLane;
return (lane & SyncDefaultLanes) !== NoLanes;
}
@@ -709,6 +711,9 @@ export function isTransitionLane(lane: Lane): boolean {
}
export function isGestureRender(lanes: Lanes): boolean {
if (!enableGestureTransition) {
return false;
}
// This should render only the one lane.
return lanes === GestureLane;
}
@@ -1270,11 +1275,13 @@ export function getGroupNameOfHighestPriorityLane(lanes: Lanes): string {
InputContinuousHydrationLane |
InputContinuousLane |
DefaultHydrationLane |
DefaultLane |
GestureLane)
DefaultLane)
) {
return 'Blocking';
}
if (lanes & GestureLane) {
return 'Gesture';
}
if (lanes & (TransitionHydrationLane | TransitionLanes)) {
return 'Transition';
}

View File

@@ -33,7 +33,10 @@ import {
addObjectDiffToProperties,
} from 'shared/ReactPerformanceTrackProperties';
import {enableProfilerTimer} from 'shared/ReactFeatureFlags';
import {
enableProfilerTimer,
enableGestureTransition,
} from 'shared/ReactFeatureFlags';
const supportsUserTiming =
enableProfilerTimer &&
@@ -68,6 +71,16 @@ export function markAllLanesInOrder() {
LANES_TRACK_GROUP,
'primary-light',
);
if (enableGestureTransition) {
console.timeStamp(
'Gesture Track',
0.003,
0.003,
'Gesture',
LANES_TRACK_GROUP,
'primary-light',
);
}
console.timeStamp(
'Transition Track',
0.003,
@@ -739,6 +752,111 @@ export function logBlockingStart(
}
}
export function logGestureStart(
updateTime: number,
eventTime: number,
eventType: null | string,
eventIsRepeat: boolean,
isPingedUpdate: boolean,
renderStartTime: number,
debugTask: null | ConsoleTask, // DEV-only
updateMethodName: null | string,
updateComponentName: null | string,
): void {
if (supportsUserTiming) {
currentTrack = 'Gesture';
// Clamp start times
if (updateTime > 0) {
if (updateTime > renderStartTime) {
updateTime = renderStartTime;
}
} else {
updateTime = renderStartTime;
}
if (eventTime > 0) {
if (eventTime > updateTime) {
eventTime = updateTime;
}
} else {
eventTime = updateTime;
}
if (updateTime > eventTime && eventType !== null) {
// Log the time from the event timeStamp until we started a gesture.
const color = eventIsRepeat ? 'secondary-light' : 'warning';
if (__DEV__ && debugTask) {
debugTask.run(
console.timeStamp.bind(
console,
eventIsRepeat ? 'Consecutive' : 'Event: ' + eventType,
eventTime,
updateTime,
currentTrack,
LANES_TRACK_GROUP,
color,
),
);
} else {
console.timeStamp(
eventIsRepeat ? 'Consecutive' : 'Event: ' + eventType,
eventTime,
updateTime,
currentTrack,
LANES_TRACK_GROUP,
color,
);
}
}
if (renderStartTime > updateTime) {
// Log the time from when we called setState until we started rendering.
const label = isPingedUpdate
? 'Promise Resolved'
: renderStartTime - updateTime > 5
? 'Gesture Blocked'
: 'Gesture';
if (__DEV__) {
const properties = [];
if (updateComponentName != null) {
properties.push(['Component name', updateComponentName]);
}
if (updateMethodName != null) {
properties.push(['Method name', updateMethodName]);
}
const measureOptions = {
start: updateTime,
end: renderStartTime,
detail: {
devtools: {
properties,
track: currentTrack,
trackGroup: LANES_TRACK_GROUP,
color: 'primary-light',
},
},
};
if (debugTask) {
debugTask.run(
// $FlowFixMe[method-unbinding]
performance.measure.bind(performance, label, measureOptions),
);
} else {
performance.measure(label, measureOptions);
}
} else {
console.timeStamp(
label,
updateTime,
renderStartTime,
currentTrack,
LANES_TRACK_GROUP,
'primary-light',
);
}
}
}
}
export function logTransitionStart(
startTime: number,
updateTime: number,

View File

@@ -71,6 +71,7 @@ import {
} from './Scheduler';
import {
logBlockingStart,
logGestureStart,
logTransitionStart,
logRenderPhase,
logInterruptedRenderPhase,
@@ -282,6 +283,16 @@ import {
blockingEventType,
blockingEventIsRepeat,
blockingSuspendedTime,
gestureClampTime,
gestureUpdateTime,
gestureUpdateTask,
gestureUpdateType,
gestureUpdateMethodName,
gestureUpdateComponentName,
gestureEventTime,
gestureEventType,
gestureEventIsRepeat,
gestureSuspendedTime,
transitionClampTime,
transitionStartTime,
transitionUpdateTime,
@@ -294,8 +305,11 @@ import {
transitionEventIsRepeat,
transitionSuspendedTime,
clearBlockingTimers,
clearGestureTimers,
clearGestureUpdates,
clearTransitionTimers,
clampBlockingTimers,
clampGestureTimers,
clampTransitionTimers,
clampRetryTimers,
clampIdleTimers,
@@ -1898,7 +1912,9 @@ function resetWorkInProgressStack() {
function finalizeRender(lanes: Lanes, finalizationTime: number): void {
if (enableProfilerTimer && enableComponentPerformanceTrack) {
if (includesSyncLane(lanes) || includesBlockingLane(lanes)) {
if (isGestureRender(lanes)) {
clampGestureTimers(finalizationTime);
} else if (includesBlockingLane(lanes)) {
clampBlockingTimers(finalizationTime);
}
if (includesTransitionLane(lanes)) {
@@ -1963,7 +1979,53 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
const previousUpdateTask = workInProgressUpdateTask;
workInProgressUpdateTask = null;
if (includesSyncLane(lanes) || includesBlockingLane(lanes)) {
if (isGestureRender(lanes)) {
workInProgressUpdateTask = gestureUpdateTask;
const clampedUpdateTime =
gestureUpdateTime >= 0 && gestureUpdateTime < gestureClampTime
? gestureClampTime
: gestureUpdateTime;
const clampedEventTime =
gestureEventTime >= 0 && gestureEventTime < gestureClampTime
? gestureClampTime
: gestureEventTime;
const clampedRenderStartTime =
// Clamp the suspended time to the first event/update.
clampedEventTime >= 0
? clampedEventTime
: clampedUpdateTime >= 0
? clampedUpdateTime
: renderStartTime;
if (gestureSuspendedTime >= 0) {
setCurrentTrackFromLanes(GestureLane);
logSuspendedWithDelayPhase(
gestureSuspendedTime,
clampedRenderStartTime,
lanes,
workInProgressUpdateTask,
);
} else if (isGestureRender(animatingLanes)) {
// If this lane is still animating, log the time from previous render finishing to now as animating.
setCurrentTrackFromLanes(GestureLane);
logAnimatingPhase(
gestureClampTime,
clampedRenderStartTime,
animatingTask,
);
}
logGestureStart(
clampedUpdateTime,
clampedEventTime,
gestureEventType,
gestureEventIsRepeat,
gestureUpdateType === PINGED_UPDATE,
renderStartTime,
gestureUpdateTask,
gestureUpdateMethodName,
gestureUpdateComponentName,
);
clearGestureTimers();
} else if (includesBlockingLane(lanes)) {
workInProgressUpdateTask = blockingUpdateTask;
const clampedUpdateTime =
blockingUpdateTime >= 0 && blockingUpdateTime < blockingClampTime
@@ -1988,7 +2050,7 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
previousUpdateTask,
);
} else if (
includesSyncLane(animatingLanes) ||
!isGestureRender(animatingLanes) &&
includesBlockingLane(animatingLanes)
) {
// If this lane is still animating, log the time from previous render finishing to now as animating.
@@ -3464,6 +3526,11 @@ function commitRoot(
// Gestures don't clear their lanes while the gesture is still active but it
// might not be scheduled to do any more renders and so we shouldn't schedule
// any more gesture lane work until a new gesture is scheduled.
if (enableProfilerTimer && (remainingLanes & GestureLane) !== NoLanes) {
// We need to clear any updates scheduled so that we can treat future updates
// as the cause of the render.
clearGestureUpdates();
}
remainingLanes &= ~GestureLane;
}
@@ -3719,14 +3786,12 @@ function finishedViewTransition(lanes: Lanes): void {
// If an affected track isn't in the middle of rendering or committing, log from the previous
// finished render until the end of the animation.
if (
(includesSyncLane(lanes) || includesBlockingLane(lanes)) &&
!includesSyncLane(workInProgressRootRenderLanes) &&
!includesBlockingLane(workInProgressRootRenderLanes) &&
!includesSyncLane(pendingEffectsLanes) &&
!includesBlockingLane(pendingEffectsLanes)
isGestureRender(lanes) &&
!isGestureRender(workInProgressRootRenderLanes) &&
!isGestureRender(pendingEffectsLanes)
) {
setCurrentTrackFromLanes(SyncLane);
logAnimatingPhase(blockingClampTime, now(), task);
setCurrentTrackFromLanes(GestureLane);
logAnimatingPhase(gestureClampTime, now(), task);
}
if (
includesTransitionLane(lanes) &&
@@ -4189,6 +4254,10 @@ function commitGestureOnRoot(
}
deleteScheduledGesture(root, finishedGesture);
if (enableProfilerTimer && enableComponentPerformanceTrack) {
startAnimating(pendingEffectsLanes);
}
const prevTransition = ReactSharedInternals.T;
ReactSharedInternals.T = null;
const previousPriority = getCurrentUpdatePriority();
@@ -4216,6 +4285,10 @@ function commitGestureOnRoot(
flushGestureMutations,
flushGestureAnimations,
reportViewTransitionError,
enableProfilerTimer
? // This callback fires after "pendingEffects" so we need to snapshot the arguments.
finishedViewTransition.bind(null, pendingEffectsLanes)
: (null: any),
);
}
@@ -4258,6 +4331,23 @@ function flushGestureAnimations(): void {
if (pendingEffectsStatus !== PENDING_GESTURE_ANIMATION_PHASE) {
return;
}
const lanes = pendingEffectsLanes;
if (enableProfilerTimer && enableComponentPerformanceTrack) {
// Update the new commitEndTime to when we started the animation.
recordCommitEndTime();
logStartViewTransitionYieldPhase(
pendingEffectsRenderEndTime,
commitEndTime,
pendingDelayedCommitReason === ABORTED_VIEW_TRANSITION_COMMIT,
animatingTask,
);
if (pendingDelayedCommitReason !== ABORTED_VIEW_TRANSITION_COMMIT) {
pendingDelayedCommitReason = ANIMATION_STARTED_COMMIT;
}
}
pendingEffectsStatus = NO_PENDING_EFFECTS;
const root = pendingEffectsRoot;
const finishedWork = pendingFinishedWork;
@@ -4282,6 +4372,10 @@ function flushGestureAnimations(): void {
ReactSharedInternals.T = prevTransition;
}
if (enableProfilerTimer && enableComponentPerformanceTrack) {
finalizeRender(lanes, commitEndTime);
}
// Now that we've rendered this lane. Start working on the next lane.
ensureRootIsScheduled(root);
}

View File

@@ -18,10 +18,9 @@ import type {CapturedValue} from './ReactCapturedValue';
import {
isTransitionLane,
isBlockingLane,
isSyncLane,
isGestureRender,
includesTransitionLane,
includesBlockingLane,
includesSyncLane,
NoLanes,
} from './ReactFiberLane';
@@ -76,6 +75,18 @@ export let blockingEventTime: number = -1.1; // Event timeStamp of the first set
export let blockingEventType: null | string = null; // Event type of the first setState.
export let blockingEventIsRepeat: boolean = false;
export let blockingSuspendedTime: number = -1.1;
export let gestureClampTime: number = -0;
export let gestureUpdateTime: number = -1.1; // First setOptimistic scheduled inside startGestureTransition.
export let gestureUpdateTask: null | ConsoleTask = null; // First sync setState's stack trace.
export let gestureUpdateType: UpdateType = 0;
export let gestureUpdateMethodName: null | string = null; // The name of the method that caused first gesture update.
export let gestureUpdateComponentName: null | string = null; // The name of the component where first gesture update happened.
export let gestureEventTime: number = -1.1; // Event timeStamp of the first setState.
export let gestureEventType: null | string = null; // Event type of the first setState.
export let gestureEventIsRepeat: boolean = false;
export let gestureSuspendedTime: number = -1.1;
// TODO: This should really be one per Transition lane.
export let transitionClampTime: number = -0;
export let transitionStartTime: number = -1.1; // First startTransition call before setState.
@@ -114,7 +125,26 @@ export function startUpdateTimerByLane(
if (!enableProfilerTimer || !enableComponentPerformanceTrack) {
return;
}
if (isSyncLane(lane) || isBlockingLane(lane)) {
if (isGestureRender(lane)) {
if (gestureUpdateTime < 0) {
gestureUpdateTime = now();
gestureUpdateTask = createTask(method);
gestureUpdateMethodName = method;
if (__DEV__ && fiber != null) {
gestureUpdateComponentName = getComponentNameFromFiber(fiber);
}
const newEventTime = resolveEventTimeStamp();
const newEventType = resolveEventType();
if (
newEventTime !== gestureEventTime ||
newEventType !== gestureEventType
) {
gestureEventIsRepeat = false;
}
gestureEventTime = newEventTime;
gestureEventType = newEventType;
}
} else if (isBlockingLane(lane)) {
if (blockingUpdateTime < 0) {
blockingUpdateTime = now();
blockingUpdateTask = createTask(method);
@@ -220,7 +250,13 @@ export function startPingTimerByLanes(lanes: Lanes): void {
// Mark the update time and clamp anything before it because we don't want
// to show the event time for pings but we also don't want to clear it
// because we still need to track if this was a repeat.
if (includesSyncLane(lanes) || includesBlockingLane(lanes)) {
if (isGestureRender(lanes)) {
if (gestureUpdateTime < 0) {
gestureClampTime = gestureUpdateTime = now();
gestureUpdateTask = createTask('Promise Resolved');
gestureUpdateType = PINGED_UPDATE;
}
} else if (includesBlockingLane(lanes)) {
if (blockingUpdateTime < 0) {
blockingClampTime = blockingUpdateTime = now();
blockingUpdateTask = createTask('Promise Resolved');
@@ -239,7 +275,9 @@ export function trackSuspendedTime(lanes: Lanes, renderEndTime: number) {
if (!enableProfilerTimer || !enableComponentPerformanceTrack) {
return;
}
if (includesSyncLane(lanes) || includesBlockingLane(lanes)) {
if (isGestureRender(lanes)) {
gestureSuspendedTime = renderEndTime;
} else if (includesBlockingLane(lanes)) {
blockingSuspendedTime = renderEndTime;
} else if (includesTransitionLane(lanes)) {
transitionSuspendedTime = renderEndTime;
@@ -293,6 +331,28 @@ export function clearTransitionTimers(): void {
transitionClampTime = now();
}
export function hasScheduledGestureTransitionWork(): boolean {
// If we have call setOptimistic on a gesture
return gestureUpdateTime > -1;
}
export function clearGestureTimers(): void {
gestureUpdateTime = -1.1;
gestureUpdateType = 0;
gestureSuspendedTime = -1.1;
gestureEventIsRepeat = true;
gestureClampTime = now();
}
export function clearGestureUpdates(): void {
// Same as clearGestureTimers but doesn't reset the clamp time because we didn't
// actually emit a render.
gestureUpdateTime = -1.1;
gestureUpdateType = 0;
gestureSuspendedTime = -1.1;
gestureEventIsRepeat = true;
}
export function clampBlockingTimers(finalTime: number): void {
if (!enableProfilerTimer || !enableComponentPerformanceTrack) {
return;
@@ -303,6 +363,16 @@ export function clampBlockingTimers(finalTime: number): void {
blockingClampTime = finalTime;
}
export function clampGestureTimers(finalTime: number): void {
if (!enableProfilerTimer || !enableComponentPerformanceTrack) {
return;
}
// If we had new updates come in while we were still rendering or committing, we don't want
// those update times to create overlapping tracks in the performance timeline so we clamp
// them to the end of the commit phase.
gestureClampTime = finalTime;
}
export function clampTransitionTimers(finalTime: number): void {
if (!enableProfilerTimer || !enableComponentPerformanceTrack) {
return;

View File

@@ -17,6 +17,7 @@ import {
NoEventPriority,
type EventPriority,
} from 'react-reconciler/src/ReactEventPriorities';
import {enableProfilerTimer} from 'shared/ReactFeatureFlags';
export {default as rendererVersion} from 'shared/ReactVersion'; // TODO: Consider exporting the react-native version.
export const rendererPackageName = 'react-test-renderer';
@@ -446,9 +447,13 @@ export function startGestureTransition(
mutationCallback: () => void,
animateCallback: () => void,
errorCallback: mixed => void,
finishedAnimation: () => void, // Profiling-only
): null | RunningViewTransition {
mutationCallback();
animateCallback();
if (enableProfilerTimer) {
finishedAnimation();
}
return null;
}

View File

@@ -3227,6 +3227,11 @@
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24"
integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==
"@jridgewell/sourcemap-codec@1.5.5":
version "1.5.5"
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba"
integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==
"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.4.15":
version "1.4.15"
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32"
@@ -8269,7 +8274,7 @@ eslint-utils@^2.0.0, eslint-utils@^2.1.0:
dependencies:
eslint-visitor-keys "^1.1.0"
"eslint-v7@npm:eslint@^7.7.0", eslint@^7.7.0:
"eslint-v7@npm:eslint@^7.7.0":
version "7.32.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.32.0.tgz#c6d328a14be3fb08c8d1d21e12c02fdb7a2a812d"
integrity sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==
@@ -8468,6 +8473,52 @@ eslint@8.57.0:
strip-ansi "^6.0.1"
text-table "^0.2.0"
eslint@^7.7.0:
version "7.32.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.32.0.tgz#c6d328a14be3fb08c8d1d21e12c02fdb7a2a812d"
integrity sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==
dependencies:
"@babel/code-frame" "7.12.11"
"@eslint/eslintrc" "^0.4.3"
"@humanwhocodes/config-array" "^0.5.0"
ajv "^6.10.0"
chalk "^4.0.0"
cross-spawn "^7.0.2"
debug "^4.0.1"
doctrine "^3.0.0"
enquirer "^2.3.5"
escape-string-regexp "^4.0.0"
eslint-scope "^5.1.1"
eslint-utils "^2.1.0"
eslint-visitor-keys "^2.0.0"
espree "^7.3.1"
esquery "^1.4.0"
esutils "^2.0.2"
fast-deep-equal "^3.1.3"
file-entry-cache "^6.0.1"
functional-red-black-tree "^1.0.1"
glob-parent "^5.1.2"
globals "^13.6.0"
ignore "^4.0.6"
import-fresh "^3.0.0"
imurmurhash "^0.1.4"
is-glob "^4.0.0"
js-yaml "^3.13.1"
json-stable-stringify-without-jsonify "^1.0.1"
levn "^0.4.1"
lodash.merge "^4.6.2"
minimatch "^3.0.4"
natural-compare "^1.4.0"
optionator "^0.9.1"
progress "^2.0.0"
regexpp "^3.1.0"
semver "^7.2.1"
strip-ansi "^6.0.0"
strip-json-comments "^3.1.0"
table "^6.0.9"
text-table "^0.2.0"
v8-compile-cache "^2.0.3"
espree@10.0.1, espree@^10.0.1:
version "10.0.1"
resolved "https://registry.yarnpkg.com/espree/-/espree-10.0.1.tgz#600e60404157412751ba4a6f3a2ee1a42433139f"
@@ -14269,7 +14320,7 @@ prepend-http@^2.0.0:
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897"
integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=
"prettier-2@npm:prettier@^2", prettier@^2.5.1:
"prettier-2@npm:prettier@^2":
version "2.8.8"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da"
integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==
@@ -14284,6 +14335,11 @@ prettier@^1.19.1:
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb"
integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==
prettier@^2.5.1:
version "2.8.8"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da"
integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==
pretty-format@^29.4.1:
version "29.4.1"
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.4.1.tgz#0da99b532559097b8254298da7c75a0785b1751c"
@@ -16158,7 +16214,7 @@ string-natural-compare@^3.0.1:
resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4"
integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
"string-width-cjs@npm:string-width@^4.2.0":
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -16193,6 +16249,15 @@ string-width@^4.0.0:
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.0"
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^5.0.1, string-width@^5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794"
@@ -16253,7 +16318,7 @@ string_decoder@~1.1.1:
dependencies:
safe-buffer "~5.1.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -16281,6 +16346,13 @@ strip-ansi@^5.1.0:
dependencies:
ansi-regex "^4.1.0"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^7.0.1:
version "7.1.0"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
@@ -17889,7 +17961,7 @@ workerize-loader@^2.0.2:
dependencies:
loader-utils "^2.0.0"
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@@ -17907,6 +17979,15 @@ wrap-ansi@^6.2.0:
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"