Compare commits

...

13 Commits

Author SHA1 Message Date
Jorge Cabiedes Acosta
1d9ccd1d1b [Compiler] ValidateNoDerivedComputationsInEffects test cases
Summary:
This creates the test cases we expect this first iteration of calculate in render to catch

The goal is to have tests that will be in a good state once we have the first iteration of the calculate in render validation working, which should be pretty limited in what its capturing.

Test Plan:
Test cases
2025-10-20 17:04:25 -07:00
Sebastian "Sebbie" Silbermann
720bb13069 [compiler] Export PluginOptions as a type that can be used in input positions (#34550) 2025-09-22 18:28:19 +02:00
Eugene Choi
1eca9a2747 [playground] Add compiler playground tests (#34528)
<!--
  Thanks for submitting a pull request!
We appreciate you spending the time to work on these changes. Please
provide enough information so that others can review your pull request.
The three fields below are mandatory.

Before submitting a pull request, please make sure the following is
done:

1. Fork [the repository](https://github.com/facebook/react) and create
your branch from `main`.
  2. Run `yarn` in the repository root.
3. If you've fixed a bug or added code that should be tested, add tests!
4. Ensure the test suite passes (`yarn test`). Tip: `yarn test --watch
TestName` is helpful in development.
5. Run `yarn test --prod` to test in the production environment. It
supports the same options as `yarn test`.
6. If you need a debugger, run `yarn test --debug --watch TestName`,
open `chrome://inspect`, and press "Inspect".
7. Format your code with
[prettier](https://github.com/prettier/prettier) (`yarn prettier`).
8. Make sure your code lints (`yarn lint`). Tip: `yarn linc` to only
check changed files.
  9. Run the [Flow](https://flowtype.org/) type checks (`yarn flow`).
  10. If you haven't already, complete the CLA.

Learn more about contributing:
https://reactjs.org/docs/how-to-contribute.html
-->

## Summary

<!--
Explain the **motivation** for making this change. What existing problem
does the pull request solve?
-->

Added more tests for the compiler playground with the addition of the
new config editor and "Show Internals" button. Added testing to check
for incomplete store params in the URL, toggle functionality, and
correct errors showing for syntax/validation errors in the config
overrides.
2025-09-22 12:11:45 -04:00
Sebastian "Sebbie" Silbermann
cd85bb5616 Include Fizz runtime diff in CI (#34525) 2025-09-22 17:09:50 +02:00
Sebastian "Sebbie" Silbermann
07e4974bad [compiler] Don't leak global __DEV__ type (#34551) 2025-09-22 16:51:57 +02:00
Sebastian Markbåge
d91d28c8ba Use the JSX of the ViewTransition as the Stack Trace of "Animating" Traces (#34539)
Stacked on #34538.

Track the Task of the first ViewTransition that we detected as
animating. Use this as the Task as "Starting Animation", "Animating"
etc. That way you can see which ViewTransition spawned the Animation.
Although it's likely to be multiple.

<img width="757" height="393" alt="Screenshot 2025-09-19 at 10 19 18 PM"
src="https://github.com/user-attachments/assets/a6cdcb89-bd02-40ec-b3c3-11121c29e892"
/>
2025-09-20 11:11:27 -04:00
Sebastian Markbåge
b4fe1e6c7e Log the time until the Animation finishes as "Animating" (#34538)
Stacked on #34522.

<img width="1025" height="200" alt="Screenshot 2025-09-19 at 6 37 28 PM"
src="https://github.com/user-attachments/assets/f25900f6-6503-48b1-876d-bd6697a29c6f"
/>

We already cover the time between "Starting Animation" and "Remaining
Effects" as "Animating". However, if the effects are forced then we can
still be animating after that. This fills in that gap.

This also fills in the gap if another render starts before the animation
finishes on the same track. It'll mark the blank space between the
previous render finishing and the next render starting as "Animating".

This should correspond roughly to the native "Animations" track.
2025-09-20 11:10:42 -04:00
Sebastian Markbåge
b204edda3a Log Custom Reason for the Suspended Commit Track (#34522)
Stacked on #34511.

We currently log all Suspended Commit as "Suspended on Images or CSS"
but it can really be other reasons too now. Like waiting on the previous
View Transition. This allows the host config configure this reason.

Now when one animation starts before another one finishes we log that as
"Waiting for the previous Animation".

<img width="592" height="257" alt="Screenshot 2025-09-17 at 11 53 45 PM"
src="https://github.com/user-attachments/assets/817af8b5-37ae-46d8-bfd1-cd3fc637f3f3"
/>
2025-09-20 11:01:52 -04:00
Hendrik Liebau
115e3ec15f [ci] Document that full git shas are required for manual prereleases (#34537)
Triggering the "(Runtime) Publish Prereleases Manual" workflow with a
short git sha doesn't work. It needs the full sha. We might be able to
make it work with the short sha as well, but for now we can at least
document the restriction.
2025-09-20 08:09:44 +02:00
Sebastian Markbåge
565eb7888e Unwrap a reference to a Lazy value (#34535)
If we are referencing a lazy value that isn't explicitly lazy ($L...)
it's because we added it around an element that was blocked to be able
to defer things inside.

However, once that is unblocked we can start unwrap it and just use the
inner element instead for any future reference. The race condition is
still there since it's a race condition whether we added the wrapper in
the first place.

This just makes it consistent with unwrapping of the rest of the path.
2025-09-19 18:23:18 -04:00
Hendrik Liebau
d415fd3ed7 [Flight] Handle Lazy in renderDebugModel (#34536)
If we don't handle Lazy types specifically in `renderDebugModel`, all of
their properties will be emitted using `renderDebugModel` as well. This
also includes its `_debugInfo` property, if the Lazy comes from the
Flight client. That array might contain objects that are deduped, and
resolving those references in the client can cause runtime errors, e.g.:

```
TypeError: Cannot read properties of undefined (reading '$$typeof')
```

This happened specifically when an "RSC stream" debug info entry, coming
from the Flight client through IO tracking, was emitted and its
`debugTask` property was deduped, which couldn't be resolved in the
client.

To avoid actually initializing a lazy causing a side-effect, we make
some assumptions about the structure of its payload, and only emit
resolved or rejected values, otherwise we emit a halted chunk.
2025-09-19 23:38:11 +02:00
Jack Pope
5e3cd53f20 Update MAINTAINERS (#34534) 2025-09-19 15:49:08 -04:00
Janka Uryga
01cad9eaca [Flight] Support Async Modules in Turbopack Server References (#34531)
Seems like this was missed in
https://github.com/facebook/react/pull/31313
2025-09-19 12:12:37 -07:00
69 changed files with 2010 additions and 153 deletions

View File

@@ -194,7 +194,7 @@ jobs:
if: steps.node_modules.outputs.cache-hit != 'true'
- run: |
yarn generate-inline-fizz-runtime
git diff --quiet || (echo "There was a change to the Fizz runtime. Run `yarn generate-inline-fizz-runtime` and check in the result." && false)
git diff --exit-code || (echo "There was a change to the Fizz runtime. Run \`yarn generate-inline-fizz-runtime\` and check in the result." && false)
# ----- FEATURE FLAGS -----
flags:
@@ -567,7 +567,7 @@ jobs:
- name: Search build artifacts for unminified errors
run: |
yarn extract-errors
git diff --quiet || (echo "Found unminified errors. Either update the error codes map or disable error minification for the affected build, if appropriate." && false)
git diff --exit-code || (echo "Found unminified errors. Either update the error codes map or disable error minification for the affected build, if appropriate." && false)
check_release_dependencies:
name: Check release dependencies

View File

@@ -1,5 +1,6 @@
acdlite
eps1lon
EugeneChoi4
gaearon
gnoff
unstubbable

View File

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

View File

@@ -0,0 +1,14 @@
import { c as _c } from "react/compiler-runtime";
export default function TestComponent(t0) {
const $ = _c(2);
const { x } = t0;
let t1;
if ($[0] !== x || true) {
t1 = <Button>{x}</Button>;
$[0] = x;
$[1] = t1;
} else {
t1 = $[1];
}
return t1;
}

View File

@@ -5,8 +5,9 @@
* LICENSE file in the root directory of this source tree.
*/
import {expect, test} from '@playwright/test';
import {expect, test, type Page} from '@playwright/test';
import {encodeStore, type Store} from '../../lib/stores';
import {defaultConfig} from '../../lib/defaultStore';
import {format} from 'prettier';
function isMonacoLoaded(): boolean {
@@ -20,6 +21,15 @@ function formatPrint(data: Array<string>): Promise<string> {
return format(data.join(''), {parser: 'babel'});
}
async function expandConfigs(page: Page): Promise<void> {
const expandButton = page.locator('[title="Expand config editor"]');
expandButton.click();
}
const TEST_SOURCE = `export default function TestComponent({ x }) {
return <Button>{x}</Button>;
}`;
const TEST_CASE_INPUTS = [
{
name: 'module-scope-use-memo',
@@ -121,10 +131,9 @@ test('editor should open successfully', async ({page}) => {
test('editor should compile from hash successfully', async ({page}) => {
const store: Store = {
source: `export default function TestComponent({ x }) {
return <Button>{x}</Button>;
}
`,
source: TEST_SOURCE,
config: defaultConfig,
showInternals: false,
};
const hash = encodeStore(store);
await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
@@ -136,7 +145,7 @@ test('editor should compile from hash successfully', async ({page}) => {
path: 'test-results/01-compiles-from-hash.png',
});
const text =
(await page.locator('.monaco-editor').nth(3).allInnerTexts()) ?? [];
(await page.locator('.monaco-editor-output').allInnerTexts()) ?? [];
const output = await formatPrint(text);
expect(output).not.toEqual('');
@@ -145,10 +154,9 @@ test('editor should compile from hash successfully', async ({page}) => {
test('reset button works', async ({page}) => {
const store: Store = {
source: `export default function TestComponent({ x }) {
return <Button>{x}</Button>;
}
`,
source: TEST_SOURCE,
config: defaultConfig,
showInternals: false,
};
const hash = encodeStore(store);
await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
@@ -157,33 +165,171 @@ test('reset button works', async ({page}) => {
// Reset button works
page.on('dialog', dialog => dialog.accept());
await page.getByRole('button', {name: 'Reset'}).click();
await expandConfigs(page);
await page.screenshot({
fullPage: true,
path: 'test-results/02-reset-button-works.png',
});
const text =
(await page.locator('.monaco-editor').nth(3).allInnerTexts()) ?? [];
(await page.locator('.monaco-editor-output').allInnerTexts()) ?? [];
const output = await formatPrint(text);
const configText =
(await page.locator('.monaco-editor-config').allInnerTexts()) ?? [];
const configOutput = configText.join('');
expect(output).not.toEqual('');
expect(output).toMatchSnapshot('02-default-output.txt');
expect(configOutput).not.toEqual('');
expect(configOutput).toMatchSnapshot('default-config.txt');
});
test('defaults load when only source is in Store', async ({page}) => {
// Test for backwards compatibility
const partial = {
source: TEST_SOURCE,
};
const hash = encodeStore(partial as Store);
await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
await page.waitForFunction(isMonacoLoaded);
await expandConfigs(page);
await page.screenshot({
fullPage: true,
path: 'test-results/03-missing-defaults.png',
});
// Config editor has default config
const configText =
(await page.locator('.monaco-editor-config').allInnerTexts()) ?? [];
const configOutput = configText.join('');
expect(configOutput).not.toEqual('');
expect(configOutput).toMatchSnapshot('default-config.txt');
const checkbox = page.locator('label.show-internals');
await expect(checkbox).not.toBeChecked();
const ssaTab = page.locator('text=SSA');
await expect(ssaTab).not.toBeVisible();
});
test('show internals button toggles correctly', async ({page}) => {
await page.goto(`/`, {waitUntil: 'networkidle'});
await page.waitForFunction(isMonacoLoaded);
// show internals should be off
const checkbox = page.locator('label.show-internals');
await checkbox.click();
await page.screenshot({
fullPage: true,
path: 'test-results/04-show-internals-on.png',
});
await expect(checkbox).toBeChecked();
const ssaTab = page.locator('text=SSA');
await expect(ssaTab).toBeVisible();
});
test('error is displayed when config has syntax error', async ({page}) => {
const store: Store = {
source: TEST_SOURCE,
config: `compilationMode: `,
showInternals: false,
};
const hash = encodeStore(store);
await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
await page.waitForFunction(isMonacoLoaded);
await expandConfigs(page);
await page.screenshot({
fullPage: true,
path: 'test-results/05-config-syntax-error.png',
});
const text =
(await page.locator('.monaco-editor-output').allInnerTexts()) ?? [];
const output = text.join('');
// Remove hidden chars
expect(output.replace(/\s+/g, ' ')).toContain('Invalid override format');
});
test('error is displayed when config has validation error', async ({page}) => {
const store: Store = {
source: TEST_SOURCE,
config: `import type { PluginOptions } from 'babel-plugin-react-compiler/dist';
({
compilationMode: "123"
} satisfies Partial<PluginOptions>);`,
showInternals: false,
};
const hash = encodeStore(store);
await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
await page.waitForFunction(isMonacoLoaded);
await expandConfigs(page);
await page.screenshot({
fullPage: true,
path: 'test-results/06-config-validation-error.png',
});
const text =
(await page.locator('.monaco-editor-output').allInnerTexts()) ?? [];
const output = text.join('');
expect(output.replace(/\s+/g, ' ')).toContain('Unexpected compilationMode');
});
test('disableMemoizationForDebugging flag works as expected', async ({
page,
}) => {
const store: Store = {
source: TEST_SOURCE,
config: `import type { PluginOptions } from 'babel-plugin-react-compiler/dist';
({
environment: {
disableMemoizationForDebugging: true
}
} satisfies Partial<PluginOptions>);`,
showInternals: false,
};
const hash = encodeStore(store);
await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
await page.waitForFunction(isMonacoLoaded);
await expandConfigs(page);
await page.screenshot({
fullPage: true,
path: 'test-results/07-config-disableMemoizationForDebugging-flag.png',
});
const text =
(await page.locator('.monaco-editor-output').allInnerTexts()) ?? [];
const output = await formatPrint(text);
expect(output).not.toEqual('');
expect(output).toMatchSnapshot('disableMemoizationForDebugging-output.txt');
});
TEST_CASE_INPUTS.forEach((t, idx) =>
test(`playground compiles: ${t.name}`, async ({page}) => {
const store: Store = {
source: t.input,
config: defaultConfig,
showInternals: false,
};
const hash = encodeStore(store);
await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
await page.waitForFunction(isMonacoLoaded);
await page.screenshot({
fullPage: true,
path: `test-results/03-0${idx}-${t.name}.png`,
path: `test-results/08-0${idx}-${t.name}.png`,
});
const text =
(await page.locator('.monaco-editor').nth(3).allInnerTexts()) ?? [];
(await page.locator('.monaco-editor-output').allInnerTexts()) ?? [];
let output: string;
if (t.noFormat) {
output = text.join('');

View File

@@ -9,7 +9,7 @@ import MonacoEditor, {loader, type Monaco} from '@monaco-editor/react';
import {PluginOptions} from 'babel-plugin-react-compiler';
import type {editor} from 'monaco-editor';
import * as monaco from 'monaco-editor';
import React, {useState, useRef, useEffect} from 'react';
import React, {useState, useRef} from 'react';
import {Resizable} from 're-resizable';
import {useStore, useStoreDispatch} from '../StoreContext';
import {monacoOptions} from './monacoOptions';
@@ -145,6 +145,7 @@ function ExpandedEditor({
onMount={handleMount}
onChange={handleChange}
loading={''}
className="monaco-editor-config"
options={{
...monacoOptions,
lineNumbers: 'off',
@@ -170,6 +171,7 @@ function ExpandedEditor({
language={'javascript'}
value={formattedAppliedOptions}
loading={''}
className="monaco-editor-applied-config"
options={{
...monacoOptions,
lineNumbers: 'off',

View File

@@ -145,6 +145,7 @@ export default function Input({errors, language}: Props): JSX.Element {
value={store.source}
onMount={handleMount}
onChange={handleChange}
className="monaco-editor-input"
options={monacoOptions}
loading={''}
/>

View File

@@ -340,6 +340,7 @@ function TextTabContent({
language={language ?? 'javascript'}
value={output}
loading={''}
className="monaco-editor-output"
options={{
...monacoOptions,
readOnly: true,

View File

@@ -58,7 +58,7 @@ export default function Header(): JSX.Element {
</div>
<div className="flex items-center text-[15px] gap-4">
<div className="flex items-center gap-2">
<label className="relative inline-block w-[34px] h-5">
<label className="show-internals relative inline-block w-[34px] h-5">
<input
type="checkbox"
checked={store.showInternals}

View File

@@ -18,7 +18,7 @@ import type { PluginOptions } from 'babel-plugin-react-compiler/dist';
({
//compilationMode: "all"
} satisfies Partial<PluginOptions>);`;
} satisfies PluginOptions);`;
export const defaultStore: Store = {
source: index,

View File

@@ -17,7 +17,7 @@ export function runBabelPluginReactCompiler(
text: string,
file: string,
language: 'flow' | 'typescript',
options: Partial<PluginOptions> | null,
options: PluginOptions | null,
includeAst: boolean = false,
): BabelCore.BabelFileResult {
const ast = BabelParser.parse(text, {

View File

@@ -18,7 +18,7 @@ import {
import {getOrInsertWith} from '../Utils/utils';
import {ExternalFunction, isHookName} from '../HIR/Environment';
import {Err, Ok, Result} from '../Utils/Result';
import {LoggerEvent, PluginOptions} from './Options';
import {LoggerEvent, ParsedPluginOptions} from './Options';
import {BabelFn, getReactCompilerRuntimeModule} from './Program';
import {SuppressionRange} from './Suppression';
@@ -56,7 +56,7 @@ export function validateRestrictedImports(
type ProgramContextOptions = {
program: NodePath<t.Program>;
suppressions: Array<SuppressionRange>;
opts: PluginOptions;
opts: ParsedPluginOptions;
filename: string | null;
code: string | null;
hasModuleScopeOptOut: boolean;
@@ -66,7 +66,7 @@ export class ProgramContext {
* Program and environment context
*/
scope: BabelScope;
opts: PluginOptions;
opts: ParsedPluginOptions;
filename: string | null;
code: string | null;
reactRuntimeModule: string;

View File

@@ -51,8 +51,8 @@ const CustomOptOutDirectiveSchema = z
.default(null);
type CustomOptOutDirective = z.infer<typeof CustomOptOutDirectiveSchema>;
export type PluginOptions = {
environment: EnvironmentConfig;
export type PluginOptions = Partial<{
environment: Partial<EnvironmentConfig>;
logger: Logger | null;
@@ -166,7 +166,11 @@ export type PluginOptions = {
* a userspace approximation of runtime APIs.
*/
target: CompilerReactTarget;
};
}>;
export type ParsedPluginOptions = Required<
Omit<PluginOptions, 'environment'>
> & {environment: EnvironmentConfig};
const CompilerReactTargetSchema = z.union([
z.literal('17'),
@@ -282,7 +286,7 @@ export type Logger = {
debugLogIRs?: (value: CompilerPipelineValue) => void;
};
export const defaultOptions: PluginOptions = {
export const defaultOptions: ParsedPluginOptions = {
compilationMode: 'infer',
panicThreshold: 'none',
environment: parseEnvironmentConfig({}).unwrap(),
@@ -299,9 +303,9 @@ export const defaultOptions: PluginOptions = {
enableReanimatedCheck: true,
customOptOutDirectives: null,
target: '19',
} as const;
};
export function parsePluginOptions(obj: unknown): PluginOptions {
export function parsePluginOptions(obj: unknown): ParsedPluginOptions {
if (obj == null || typeof obj !== 'object') {
return defaultOptions;
}

View File

@@ -103,6 +103,7 @@ 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 =
@@ -275,6 +276,10 @@ function runWithEnvironment(
validateNoDerivedComputationsInEffects(hir);
}
if (env.config.validateNoDerivedComputationsInEffects_exp) {
validateNoDerivedComputationsInEffects_exp(hir);
}
if (env.config.validateNoSetStateInEffects) {
env.logErrors(validateNoSetStateInEffects(hir, env));
}

View File

@@ -23,7 +23,11 @@ import {
ProgramContext,
validateRestrictedImports,
} from './Imports';
import {CompilerReactTarget, PluginOptions} from './Options';
import {
CompilerReactTarget,
ParsedPluginOptions,
PluginOptions,
} from './Options';
import {compileFn} from './Pipeline';
import {
filterSuppressionsThatAffectFunction,
@@ -34,7 +38,7 @@ import {GeneratedSource} from '../HIR';
import {Err, Ok, Result} from '../Utils/Result';
export type CompilerPass = {
opts: PluginOptions;
opts: ParsedPluginOptions;
filename: string | null;
comments: Array<t.CommentBlock | t.CommentLine>;
code: string | null;
@@ -45,7 +49,7 @@ const DYNAMIC_GATING_DIRECTIVE = new RegExp('^use memo if\\(([^\\)]*)\\)$');
export function tryFindDirectiveEnablingMemoization(
directives: Array<t.Directive>,
opts: PluginOptions,
opts: ParsedPluginOptions,
): Result<t.Directive | null, CompilerError> {
const optIn = directives.find(directive =>
OPT_IN_DIRECTIVES.has(directive.value.value),
@@ -81,7 +85,7 @@ export function findDirectiveDisablingMemoization(
}
function findDirectivesDynamicGating(
directives: Array<t.Directive>,
opts: PluginOptions,
opts: ParsedPluginOptions,
): Result<
{
gating: ExternalFunction;

View File

@@ -7,7 +7,7 @@
import type * as BabelCore from '@babel/core';
import {hasOwnProperty} from '../Utils/utils';
import {PluginOptions} from './Options';
import {ParsedPluginOptions} from './Options';
function hasModule(name: string): boolean {
if (typeof require === 'undefined') {
@@ -52,7 +52,9 @@ export function pipelineUsesReanimatedPlugin(
return hasModule('react-native-reanimated');
}
export function injectReanimatedFlag(options: PluginOptions): PluginOptions {
export function injectReanimatedFlag(
options: ParsedPluginOptions,
): ParsedPluginOptions {
return {
...options,
environment: {

View File

@@ -334,6 +334,12 @@ 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

@@ -175,7 +175,7 @@ function parseConfigPragmaEnvironmentForTest(
});
}
const testComplexPluginOptionDefaults: Partial<PluginOptions> = {
const testComplexPluginOptionDefaults: PluginOptions = {
gating: {
source: 'ReactForgetFeatureFlag',
importSpecifierName: 'isForgetEnabled_Fixtures',

View File

@@ -0,0 +1,240 @@
/**
* 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

@@ -0,0 +1,79 @@
## 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

@@ -0,0 +1,21 @@
// @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

@@ -0,0 +1,71 @@
## 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

@@ -0,0 +1,18 @@
// @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

@@ -0,0 +1,70 @@
## 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

@@ -0,0 +1,15 @@
// @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

@@ -0,0 +1,108 @@
## 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

@@ -0,0 +1,25 @@
// @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

@@ -0,0 +1,71 @@
## 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

@@ -0,0 +1,18 @@
// @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

@@ -0,0 +1,86 @@
## 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

@@ -0,0 +1,22 @@
// @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

@@ -0,0 +1,47 @@
## 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

@@ -0,0 +1,21 @@
// @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

@@ -0,0 +1,46 @@
## 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

@@ -0,0 +1,20 @@
// @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

@@ -0,0 +1,43 @@
## 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

@@ -0,0 +1,17 @@
// @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

@@ -0,0 +1,73 @@
## 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

@@ -0,0 +1,20 @@
// @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

@@ -0,0 +1,72 @@
## 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

@@ -0,0 +1,18 @@
// @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

@@ -0,0 +1,74 @@
## 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

@@ -0,0 +1,19 @@
// @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,6 +3,8 @@
```javascript
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function BadExample() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
@@ -10,7 +12,7 @@ function BadExample() {
// 🔴 Avoid: redundant state and unnecessary Effect
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(capitalize(firstName + ' ' + lastName));
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
return <div>{fullName}</div>;
@@ -26,14 +28,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:9:4
7 | const [fullName, setFullName] = useState('');
8 | useEffect(() => {
> 9 | setFullName(capitalize(firstName + ' ' + lastName));
error.invalid-derived-computation-in-effect.ts:11:4
9 | const [fullName, setFullName] = useState('');
10 | useEffect(() => {
> 11 | setFullName(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)
10 | }, [firstName, lastName]);
11 |
12 | return <div>{fullName}</div>;
12 | }, [firstName, lastName]);
13 |
14 | return <div>{fullName}</div>;
```

View File

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

View File

@@ -51,6 +51,7 @@ export {
} from './ReactiveScopes';
export {parseConfigPragmaForTests} from './Utils/TestUtils';
declare global {
// @internal
let __DEV__: boolean | null | undefined;
}

View File

@@ -10,6 +10,7 @@
"importsNotUsedAsValues": "remove",
"noUncheckedIndexedAccess": false,
"noUnusedParameters": false,
"stripInternal": true,
"useUnknownInCatchVariables": true,
"target": "ES2015",
// ideally turn off only during dev, or on a per-file basis

View File

@@ -24,7 +24,7 @@ import * as HermesParser from 'hermes-parser';
import {isDeepStrictEqual} from 'util';
import type {ParseResult} from '@babel/parser';
const COMPILER_OPTIONS: Partial<PluginOptions> = {
const COMPILER_OPTIONS: PluginOptions = {
noEmit: true,
panicThreshold: 'none',
// Don't emit errors on Flow suppressions--Flow already gave a signal

View File

@@ -46,7 +46,7 @@ const logger = {
},
};
const COMPILER_OPTIONS: Partial<PluginOptions> = {
const COMPILER_OPTIONS: PluginOptions = {
noEmit: true,
compilationMode: 'infer',
panicThreshold: 'critical_errors',
@@ -72,7 +72,7 @@ function runBabelPluginReactCompiler(
text: string,
file: string,
language: 'flow' | 'typescript',
options: Partial<PluginOptions> | null,
options: PluginOptions | null,
): BabelCore.BabelFileResult {
const ast = BabelParser.parse(text, {
sourceFilename: file,

View File

@@ -27,7 +27,7 @@ export type PrintedCompilerPipelineValue =
type CompileOptions = {
text: string;
file: string;
options: Partial<PluginOptions> | null;
options: PluginOptions | null;
};
export async function compile({
text,

View File

@@ -145,7 +145,7 @@ server.tool(
}
};
const errors: Array<{message: string; loc: SourceLocation | null}> = [];
const compilerOptions: Partial<PluginOptions> = {
const compilerOptions: PluginOptions = {
panicThreshold: 'none',
logger: {
debugLogIRs: logIR,

View File

@@ -25,12 +25,12 @@ import * as HermesParser from 'hermes-parser';
import {isDeepStrictEqual} from 'util';
import type {ParseResult} from '@babel/parser';
const COMPILER_OPTIONS: Partial<PluginOptions> = {
const COMPILER_OPTIONS: PluginOptions = {
noEmit: true,
panicThreshold: 'none',
// Don't emit errors on Flow suppressions--Flow already gave a signal
flowSuppressions: false,
environment: validateEnvironmentConfig({
environment: {
validateRefAccessDuringRender: true,
validateNoSetStateInRender: true,
validateNoSetStateInEffects: true,
@@ -43,7 +43,7 @@ const COMPILER_OPTIONS: Partial<PluginOptions> = {
validateNoCapitalizedCalls: [],
validateHooksUsage: true,
validateNoDerivedComputationsInEffects: true,
}),
},
};
export type UnusedOptOutDirective = {
@@ -113,7 +113,7 @@ function runReactCompilerImpl({
userOpts,
}: RunParams): RunCacheEntry {
// Compat with older versions of eslint
const options: PluginOptions = parsePluginOptions({
const options = parsePluginOptions({
...COMPILER_OPTIONS,
...userOpts,
environment: {

View File

@@ -621,6 +621,10 @@ export function waitForCommitToBeReady(timeoutOffset) {
return null;
}
export function getSuspendedCommitReason(state, rootContainer) {
return null;
}
export const NotPendingTransition = null;
export const HostTransitionContext: ReactContext<TransitionStatus> = {
$$typeof: REACT_CONTEXT_TYPE,

View File

@@ -1337,7 +1337,11 @@ function fulfillReference(
const {response, handler, parentObject, key, map, path} = reference;
for (let i = 1; i < path.length; i++) {
while (value.$$typeof === REACT_LAZY_TYPE) {
while (
typeof value === 'object' &&
value !== null &&
value.$$typeof === REACT_LAZY_TYPE
) {
// We never expect to see a Lazy node on this path because we encode those as
// separate models. This must mean that we have inserted an extra lazy node
// e.g. to replace a blocked element. We must instead look for it inside.
@@ -1408,6 +1412,39 @@ function fulfillReference(
}
value = value[path[i]];
}
while (
typeof value === 'object' &&
value !== null &&
value.$$typeof === REACT_LAZY_TYPE
) {
// If what we're referencing is a Lazy it must be because we inserted one as a virtual node
// while it was blocked by other data. If it's no longer blocked, we can unwrap it.
const referencedChunk: SomeChunk<any> = value._payload;
if (referencedChunk === handler.chunk) {
// This is a reference to the thing we're currently blocking. We can peak
// inside of it to get the value.
value = handler.value;
continue;
} else {
switch (referencedChunk.status) {
case RESOLVED_MODEL:
initializeModelChunk(referencedChunk);
break;
case RESOLVED_MODULE:
initializeModuleChunk(referencedChunk);
break;
}
switch (referencedChunk.status) {
case INITIALIZED: {
value = referencedChunk.value;
continue;
}
}
}
break;
}
const mappedValue = map(response, value, parentObject, key);
parentObject[key] = mappedValue;
@@ -1855,7 +1892,11 @@ function getOutlinedModel<T>(
case INITIALIZED:
let value = chunk.value;
for (let i = 1; i < path.length; i++) {
while (value.$$typeof === REACT_LAZY_TYPE) {
while (
typeof value === 'object' &&
value !== null &&
value.$$typeof === REACT_LAZY_TYPE
) {
const referencedChunk: SomeChunk<any> = value._payload;
switch (referencedChunk.status) {
case RESOLVED_MODEL:
@@ -1924,6 +1965,32 @@ function getOutlinedModel<T>(
}
value = value[path[i]];
}
while (
typeof value === 'object' &&
value !== null &&
value.$$typeof === REACT_LAZY_TYPE
) {
// If what we're referencing is a Lazy it must be because we inserted one as a virtual node
// while it was blocked by other data. If it's no longer blocked, we can unwrap it.
const referencedChunk: SomeChunk<any> = value._payload;
switch (referencedChunk.status) {
case RESOLVED_MODEL:
initializeModelChunk(referencedChunk);
break;
case RESOLVED_MODULE:
initializeModuleChunk(referencedChunk);
break;
}
switch (referencedChunk.status) {
case INITIALIZED: {
value = referencedChunk.value;
continue;
}
}
break;
}
const chunkValue = map(response, value, parentObject, key);
if (
parentObject[0] === REACT_ELEMENT_TYPE &&

View File

@@ -2100,6 +2100,7 @@ export function startViewTransition(
passiveCallback: () => mixed,
errorCallback: mixed => void,
blockedCallback: string => void, // Profiling-only
finishedAnimation: () => void, // Profiling-only
): null | RunningViewTransition {
const ownerDocument: Document =
rootContainer.nodeType === DOCUMENT_NODE
@@ -2302,6 +2303,9 @@ export function startViewTransition(
// $FlowFixMe[prop-missing]
ownerDocument.__reactViewTransition = null;
}
if (enableProfilerTimer) {
finishedAnimation();
}
passiveCallback();
});
return transition;
@@ -5965,6 +5969,7 @@ export opaque type SuspendedState = {
imgBytes: number, // number of bytes we estimate needing to download
suspenseyImages: Array<HTMLImageElement>, // instances of suspensey images (whether loaded or not)
waitingForImages: boolean, // false when we're no longer blocking on images
waitingForViewTransition: boolean,
unsuspend: null | (() => void),
};
@@ -5976,6 +5981,7 @@ export function startSuspendingCommit(): SuspendedState {
imgBytes: 0,
suspenseyImages: [],
waitingForImages: true,
waitingForViewTransition: false,
// We use a noop function when we begin suspending because if possible we want the
// waitfor step to finish synchronously. If it doesn't we'll return a function to
// provide the actual unsuspend function and that will get completed when the count
@@ -6123,6 +6129,7 @@ export function suspendOnActiveViewTransition(
return;
}
state.count++;
state.waitingForViewTransition = true;
const ping = onUnsuspend.bind(state);
activeViewTransition.finished.then(ping, ping);
}
@@ -6206,6 +6213,28 @@ export function waitForCommitToBeReady(
return null;
}
export function getSuspendedCommitReason(
state: SuspendedState,
rootContainer: Container,
): null | string {
if (state.waitingForViewTransition) {
return 'Waiting for the previous Animation';
}
if (state.count > 0) {
if (state.imgCount > 0) {
return 'Suspended on CSS and Images';
}
return 'Suspended on CSS';
}
if (state.imgCount === 1) {
return 'Suspended on an Image';
}
if (state.imgCount > 0) {
return 'Suspended on Images';
}
return null;
}
function checkIfFullyUnsuspended(state: SuspendedState) {
if (state.count === 0 && (state.imgCount === 0 || !state.waitingForImages)) {
if (state.stylesheets) {

View File

@@ -627,6 +627,13 @@ export function waitForCommitToBeReady(
return null;
}
export function getSuspendedCommitReason(
state: SuspendedState,
rootContainer: Container,
): null | string {
return null;
}
export type FragmentInstanceType = {
_fragmentFiber: Fiber,
_observers: null | Set<IntersectionObserver>,

View File

@@ -674,6 +674,7 @@ export function startViewTransition(
passiveCallback: () => mixed,
errorCallback: mixed => void,
blockedCallback: string => void, // Profiling-only
finishedAnimation: () => void, // Profiling-only
): null | RunningViewTransition {
mutationCallback();
layoutCallback();
@@ -806,6 +807,13 @@ export function waitForCommitToBeReady(
return null;
}
export function getSuspendedCommitReason(
state: SuspendedState,
rootContainer: Container,
): null | string {
return null;
}
export const NotPendingTransition: TransitionStatus = null;
export const HostTransitionContext: ReactContext<TransitionStatus> = {
$$typeof: REACT_CONTEXT_TYPE,

View File

@@ -702,6 +702,13 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
waitForCommitToBeReady,
getSuspendedCommitReason(
state: SuspendedState,
rootContainer: Container,
): null | string {
return null;
},
NotPendingTransition: (null: TransitionStatus),
resetFormInstance(form: Instance) {},
@@ -853,6 +860,8 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
spawnedWorkCallback: () => void,
passiveCallback: () => mixed,
errorCallback: mixed => void,
blockedCallback: string => void, // Profiling-only
finishedAnimation: () => void, // Profiling-only
): null | RunningViewTransition {
mutationCallback();
layoutCallback();

View File

@@ -39,6 +39,11 @@ import {
getViewTransitionName,
getViewTransitionClassName,
} from './ReactFiberViewTransitionComponent';
import {trackAnimatingTask} from './ReactProfilerTimer';
import {
enableComponentPerformanceTrack,
enableProfilerTimer,
} from 'shared/ReactFeatureFlags';
export let shouldStartViewTransition: boolean = false;
@@ -101,21 +106,27 @@ export function popViewTransitionCancelableScope(
let viewTransitionHostInstanceIdx = 0;
export function applyViewTransitionToHostInstances(
child: null | Fiber,
function applyViewTransitionToHostInstances(
fiber: Fiber,
name: string,
className: ?string,
collectMeasurements: null | Array<InstanceMeasurement>,
stopAtNestedViewTransitions: boolean,
): boolean {
viewTransitionHostInstanceIdx = 0;
return applyViewTransitionToHostInstancesRecursive(
child,
const inViewport = applyViewTransitionToHostInstancesRecursive(
fiber.child,
name,
className,
collectMeasurements,
stopAtNestedViewTransitions,
);
if (enableProfilerTimer && enableComponentPerformanceTrack && inViewport) {
if (fiber._debugTask != null) {
trackAnimatingTask(fiber._debugTask);
}
}
return inViewport;
}
function applyViewTransitionToHostInstancesRecursive(
@@ -247,7 +258,7 @@ function commitAppearingPairViewTransitions(placement: Fiber): void {
// We found a new appearing view transition with the same name as this deletion.
// We'll transition between them.
const inViewport = applyViewTransitionToHostInstances(
child.child,
child,
name,
className,
null,
@@ -284,7 +295,7 @@ export function commitEnterViewTransitions(
);
if (className !== 'none') {
const inViewport = applyViewTransitionToHostInstances(
placement.child,
placement,
name,
className,
null,
@@ -355,7 +366,7 @@ function commitDeletedPairViewTransitions(deletion: Fiber): void {
if (className !== 'none') {
// We found a new appearing view transition with the same name as this deletion.
const inViewport = applyViewTransitionToHostInstances(
child.child,
child,
name,
className,
null,
@@ -406,7 +417,7 @@ export function commitExitViewTransitions(deletion: Fiber): void {
);
if (className !== 'none') {
const inViewport = applyViewTransitionToHostInstances(
deletion.child,
deletion,
name,
className,
null,
@@ -490,7 +501,7 @@ export function commitBeforeUpdateViewTransition(
return;
}
applyViewTransitionToHostInstances(
current.child,
current,
oldName,
className,
(current.memoizedState = []),
@@ -518,7 +529,7 @@ export function commitNestedViewTransitions(changedParent: Fiber): void {
child.flags &= ~Update;
if (className !== 'none') {
applyViewTransitionToHostInstances(
child.child,
child,
name,
className,
(child.memoizedState = []),

View File

@@ -73,6 +73,8 @@ const TransitionLane12: Lane = /* */ 0b0000000000010000000
const TransitionLane13: Lane = /* */ 0b0000000000100000000000000000000;
const TransitionLane14: Lane = /* */ 0b0000000001000000000000000000000;
export const SomeTransitionLane: Lane = TransitionLane1;
const TransitionUpdateLanes =
TransitionLane1 |
TransitionLane2 |
@@ -633,6 +635,22 @@ export function includesTransitionLane(lanes: Lanes): boolean {
return (lanes & TransitionLanes) !== NoLanes;
}
export function includesRetryLane(lanes: Lanes): boolean {
return (lanes & RetryLanes) !== NoLanes;
}
export function includesIdleGroupLanes(lanes: Lanes): boolean {
return (
(lanes &
(SelectiveHydrationLane |
IdleHydrationLane |
IdleLane |
OffscreenLane |
DeferredLane)) !==
NoLanes
);
}
export function includesOnlyHydrationLanes(lanes: Lanes): boolean {
return (lanes & HydrationLanes) === lanes;
}

View File

@@ -1180,45 +1180,10 @@ export function logInconsistentRender(
}
}
export function logSuspenseThrottlePhase(
startTime: number,
endTime: number,
debugTask: null | ConsoleTask,
): void {
// This was inside a throttled Suspense boundary commit.
if (supportsUserTiming) {
if (endTime <= startTime) {
return;
}
if (__DEV__ && debugTask) {
debugTask.run(
// $FlowFixMe[method-unbinding]
console.timeStamp.bind(
console,
'Throttled',
startTime,
endTime,
currentTrack,
LANES_TRACK_GROUP,
'secondary-light',
),
);
} else {
console.timeStamp(
'Throttled',
startTime,
endTime,
currentTrack,
LANES_TRACK_GROUP,
'secondary-light',
);
}
}
}
export function logSuspendedCommitPhase(
startTime: number,
endTime: number,
reason: string,
debugTask: null | ConsoleTask,
): void {
// This means the commit was suspended on CSS or images.
@@ -1233,7 +1198,7 @@ export function logSuspendedCommitPhase(
// $FlowFixMe[method-unbinding]
console.timeStamp.bind(
console,
'Suspended on CSS or Images',
reason,
startTime,
endTime,
currentTrack,
@@ -1243,7 +1208,7 @@ export function logSuspendedCommitPhase(
);
} else {
console.timeStamp(
'Suspended on CSS or Images',
reason,
startTime,
endTime,
currentTrack,
@@ -1493,7 +1458,7 @@ export function logAnimatingPhase(
endTime,
currentTrack,
LANES_TRACK_GROUP,
'secondary',
'secondary-dark',
),
);
} else {
@@ -1503,7 +1468,7 @@ export function logAnimatingPhase(
endTime,
currentTrack,
LANES_TRACK_GROUP,
'secondary',
'secondary-dark',
);
}
}

View File

@@ -79,7 +79,6 @@ import {
logErroredRenderPhase,
logInconsistentRender,
logSuspendedWithDelayPhase,
logSuspenseThrottlePhase,
logSuspendedCommitPhase,
logSuspendedViewTransitionPhase,
logCommitPhase,
@@ -103,6 +102,7 @@ import {
startSuspendingCommit,
suspendOnActiveViewTransition,
waitForCommitToBeReady,
getSuspendedCommitReason,
preloadInstance,
preloadResource,
supportsHydration,
@@ -179,6 +179,8 @@ import {
includesOnlyTransitions,
includesBlockingLane,
includesTransitionLane,
includesRetryLane,
includesIdleGroupLanes,
includesExpiredLane,
getNextLanes,
getEntangledLanes,
@@ -201,6 +203,9 @@ import {
includesOnlyViewTransitionEligibleLanes,
isGestureRender,
GestureLane,
SomeTransitionLane,
SomeRetryLane,
IdleLane,
} from './ReactFiberLane';
import {
DiscreteEventPriority,
@@ -292,6 +297,8 @@ import {
clearTransitionTimers,
clampBlockingTimers,
clampTransitionTimers,
clampRetryTimers,
clampIdleTimers,
markNestedUpdateScheduled,
renderStartTime,
commitStartTime,
@@ -312,6 +319,12 @@ import {
resetCommitErrors,
PINGED_UPDATE,
SPAWNED_UPDATE,
startAnimating,
stopAnimating,
animatingLanes,
retryClampTime,
idleClampTime,
animatingTask,
} from './ReactProfilerTimer';
// DEV stuff
@@ -672,12 +685,10 @@ export function getRenderTargetTime(): number {
let legacyErrorBoundariesThatAlreadyFailed: Set<mixed> | null = null;
type SuspendedCommitReason = 0 | 1 | 2;
const IMMEDIATE_COMMIT = 0;
const SUSPENDED_COMMIT = 1;
const THROTTLED_COMMIT = 2;
type SuspendedCommitReason = null | string;
type DelayedCommitReason = 0 | 1 | 2 | 3;
const IMMEDIATE_COMMIT = 0;
const ABORTED_VIEW_TRANSITION_COMMIT = 1;
const DELAYED_PASSIVE_COMMIT = 2;
const ANIMATION_STARTED_COMMIT = 3;
@@ -703,7 +714,7 @@ let pendingViewTransitionEvents: Array<(types: Array<string>) => void> | null =
null;
let pendingTransitionTypes: null | TransitionTypes = null;
let pendingDidIncludeRenderPhaseUpdate: boolean = false;
let pendingSuspendedCommitReason: SuspendedCommitReason = IMMEDIATE_COMMIT; // Profiling-only
let pendingSuspendedCommitReason: SuspendedCommitReason = null; // Profiling-only
let pendingDelayedCommitReason: DelayedCommitReason = IMMEDIATE_COMMIT; // Profiling-only
let pendingSuspendedViewTransitionReason: null | string = null; // Profiling-only
@@ -1391,7 +1402,7 @@ function finishConcurrentRender(
workInProgressSuspendedRetryLanes,
exitStatus,
null,
IMMEDIATE_COMMIT,
null,
renderStartTime,
renderEndTime,
);
@@ -1428,6 +1439,7 @@ function finishConcurrentRender(
// immediately, wait for more data to arrive.
// TODO: Combine retry throttling with Suspensey commits. Right now they
// run one after the other.
pendingEffectsLanes = lanes;
root.timeoutHandle = scheduleTimeout(
commitRootWhenReady.bind(
null,
@@ -1442,7 +1454,7 @@ function finishConcurrentRender(
workInProgressSuspendedRetryLanes,
workInProgressRootDidSkipSuspendedSiblings,
exitStatus,
THROTTLED_COMMIT,
'Throttled',
renderStartTime,
renderEndTime,
),
@@ -1463,7 +1475,7 @@ function finishConcurrentRender(
workInProgressSuspendedRetryLanes,
workInProgressRootDidSkipSuspendedSiblings,
exitStatus,
IMMEDIATE_COMMIT,
null,
renderStartTime,
renderEndTime,
);
@@ -1541,6 +1553,7 @@ function commitRootWhenReady(
// Not yet ready to commit. Delay the commit until the renderer notifies
// us that it's ready. This will be canceled if we start work on the
// root again.
pendingEffectsLanes = lanes;
root.cancelPendingCommit = schedulePendingCommit(
commitRoot.bind(
null,
@@ -1555,7 +1568,9 @@ function commitRootWhenReady(
suspendedRetryLanes,
exitStatus,
suspendedState,
SUSPENDED_COMMIT,
enableProfilerTimer
? getSuspendedCommitReason(suspendedState, root.containerInfo)
: null,
completedRenderStartTime,
completedRenderEndTime,
),
@@ -1889,6 +1904,12 @@ function finalizeRender(lanes: Lanes, finalizationTime: number): void {
if (includesTransitionLane(lanes)) {
clampTransitionTimers(finalizationTime);
}
if (includesRetryLane(lanes)) {
clampRetryTimers(finalizationTime);
}
if (includesIdleGroupLanes(lanes)) {
clampIdleTimers(finalizationTime);
}
}
}
@@ -1939,6 +1960,7 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
}
finalizeRender(workInProgressRootRenderLanes, renderStartTime);
}
const previousUpdateTask = workInProgressUpdateTask;
workInProgressUpdateTask = null;
if (includesSyncLane(lanes) || includesBlockingLane(lanes)) {
@@ -1951,18 +1973,30 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
blockingEventTime >= 0 && blockingEventTime < blockingClampTime
? blockingClampTime
: blockingEventTime;
const clampedRenderStartTime = // Clamp the suspended time to the first event/update.
clampedEventTime >= 0
? clampedEventTime
: clampedUpdateTime >= 0
? clampedUpdateTime
: renderStartTime;
if (blockingSuspendedTime >= 0) {
setCurrentTrackFromLanes(lanes);
setCurrentTrackFromLanes(SyncLane);
logSuspendedWithDelayPhase(
blockingSuspendedTime,
// Clamp the suspended time to the first event/update.
clampedEventTime >= 0
? clampedEventTime
: clampedUpdateTime >= 0
? clampedUpdateTime
: renderStartTime,
clampedRenderStartTime,
lanes,
workInProgressUpdateTask,
previousUpdateTask,
);
} else if (
includesSyncLane(animatingLanes) ||
includesBlockingLane(animatingLanes)
) {
// If this lane is still animating, log the time from previous render finishing to now as animating.
setCurrentTrackFromLanes(SyncLane);
logAnimatingPhase(
blockingClampTime,
clampedRenderStartTime,
animatingTask,
);
}
logBlockingStart(
@@ -1994,19 +2028,29 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
transitionEventTime >= 0 && transitionEventTime < transitionClampTime
? transitionClampTime
: transitionEventTime;
const clampedRenderStartTime =
// Clamp the suspended time to the first event/update.
clampedEventTime >= 0
? clampedEventTime
: clampedUpdateTime >= 0
? clampedUpdateTime
: renderStartTime;
if (transitionSuspendedTime >= 0) {
setCurrentTrackFromLanes(lanes);
setCurrentTrackFromLanes(SomeTransitionLane);
logSuspendedWithDelayPhase(
transitionSuspendedTime,
// Clamp the suspended time to the first event/update.
clampedEventTime >= 0
? clampedEventTime
: clampedUpdateTime >= 0
? clampedUpdateTime
: renderStartTime,
clampedRenderStartTime,
lanes,
workInProgressUpdateTask,
);
} else if (includesTransitionLane(animatingLanes)) {
// If this lane is still animating, log the time from previous render finishing to now as animating.
setCurrentTrackFromLanes(SomeTransitionLane);
logAnimatingPhase(
transitionClampTime,
clampedRenderStartTime,
animatingTask,
);
}
logTransitionStart(
clampedStartTime,
@@ -2022,6 +2066,20 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
);
clearTransitionTimers();
}
if (includesRetryLane(lanes)) {
if (includesRetryLane(animatingLanes)) {
// If this lane is still animating, log the time from previous render finishing to now as animating.
setCurrentTrackFromLanes(SomeRetryLane);
logAnimatingPhase(retryClampTime, renderStartTime, animatingTask);
}
}
if (includesIdleGroupLanes(lanes)) {
if (includesIdleGroupLanes(animatingLanes)) {
// If this lane is still animating, log the time from previous render finishing to now as animating.
setCurrentTrackFromLanes(IdleLane);
logAnimatingPhase(idleClampTime, renderStartTime, animatingTask);
}
}
}
const timeoutHandle = root.timeoutHandle;
@@ -2038,6 +2096,8 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
cancelPendingCommit();
}
pendingEffectsLanes = NoLanes;
resetWorkInProgressStack();
workInProgressRoot = root;
const rootWorkInProgress = createWorkInProgress(root.current, null);
@@ -3458,7 +3518,7 @@ function commitRoot(
recoverableErrors,
suspendedState,
enableProfilerTimer
? suspendedCommitReason === IMMEDIATE_COMMIT
? suspendedCommitReason === null
? completedRenderEndTime
: commitStartTime
: 0,
@@ -3530,16 +3590,11 @@ function commitRoot(
resetCommitErrors();
recordCommitTime();
if (enableComponentPerformanceTrack) {
if (suspendedCommitReason === SUSPENDED_COMMIT) {
if (suspendedCommitReason !== null) {
logSuspendedCommitPhase(
completedRenderEndTime,
commitStartTime,
workInProgressUpdateTask,
);
} else if (suspendedCommitReason === THROTTLED_COMMIT) {
logSuspenseThrottlePhase(
completedRenderEndTime,
commitStartTime,
suspendedCommitReason,
workInProgressUpdateTask,
);
}
@@ -3597,6 +3652,9 @@ function commitRoot(
pendingEffectsStatus = PENDING_MUTATION_PHASE;
if (enableViewTransition && willStartViewTransition) {
if (enableProfilerTimer && enableComponentPerformanceTrack) {
startAnimating(lanes);
}
pendingViewTransition = startViewTransition(
suspendedState,
root.containerInfo,
@@ -3608,6 +3666,10 @@ function commitRoot(
flushPassiveEffects,
reportViewTransitionError,
enableProfilerTimer ? suspendedViewTransition : (null: any),
enableProfilerTimer
? // This callback fires after "pendingEffects" so we need to snapshot the arguments.
finishedViewTransition.bind(null, lanes)
: (null: any),
);
} else {
// Flush synchronously.
@@ -3633,16 +3695,63 @@ function suspendedViewTransition(reason: string): void {
// We'll split the commit into two phases, because we're suspended in the middle.
recordCommitEndTime();
logCommitPhase(
pendingSuspendedCommitReason === IMMEDIATE_COMMIT
pendingSuspendedCommitReason === null
? pendingEffectsRenderEndTime
: commitStartTime,
commitEndTime,
commitErrors,
pendingDelayedCommitReason === ABORTED_VIEW_TRANSITION_COMMIT,
workInProgressUpdateTask,
workInProgressUpdateTask, // TODO: Use a ViewTransition Task and this is not safe to read in this phase.
);
pendingSuspendedViewTransitionReason = reason;
pendingSuspendedCommitReason = SUSPENDED_COMMIT;
pendingSuspendedCommitReason = reason;
}
}
function finishedViewTransition(lanes: Lanes): void {
if (enableProfilerTimer && enableComponentPerformanceTrack) {
if ((animatingLanes & lanes) === NoLanes) {
// Was already stopped by some other action or maybe other root.
return;
}
const task = animatingTask;
stopAnimating(lanes);
// 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)
) {
setCurrentTrackFromLanes(SyncLane);
logAnimatingPhase(blockingClampTime, now(), task);
}
if (
includesTransitionLane(lanes) &&
!includesTransitionLane(workInProgressRootRenderLanes) &&
!includesTransitionLane(pendingEffectsLanes)
) {
setCurrentTrackFromLanes(SomeTransitionLane);
logAnimatingPhase(transitionClampTime, now(), task);
}
if (
includesRetryLane(lanes) &&
!includesRetryLane(workInProgressRootRenderLanes) &&
!includesRetryLane(pendingEffectsLanes)
) {
setCurrentTrackFromLanes(SomeRetryLane);
logAnimatingPhase(retryClampTime, now(), task);
}
if (
includesIdleGroupLanes(lanes) &&
!includesIdleGroupLanes(workInProgressRootRenderLanes) &&
!includesIdleGroupLanes(pendingEffectsLanes)
) {
setCurrentTrackFromLanes(IdleLane);
logAnimatingPhase(idleClampTime, now(), task);
}
}
}
@@ -3720,7 +3829,7 @@ function flushLayoutEffects(): void {
commitEndTime, // The start is the end of the first commit part.
commitStartTime, // The end is the start of the second commit part.
suspendedViewTransitionReason,
workInProgressUpdateTask,
animatingTask,
);
}
}
@@ -3792,9 +3901,7 @@ function flushLayoutEffects(): void {
if (enableProfilerTimer && enableComponentPerformanceTrack) {
recordCommitEndTime();
logCommitPhase(
suspendedCommitReason === IMMEDIATE_COMMIT
? completedRenderEndTime
: commitStartTime,
suspendedCommitReason === null ? completedRenderEndTime : commitStartTime,
commitEndTime,
commitErrors,
pendingDelayedCommitReason === ABORTED_VIEW_TRANSITION_COMMIT,
@@ -3825,7 +3932,7 @@ function flushSpawnedWork(): void {
startViewTransitionStartTime,
commitEndTime,
pendingDelayedCommitReason === ABORTED_VIEW_TRANSITION_COMMIT,
workInProgressUpdateTask, // TODO: Use a ViewTransition Task.
animatingTask,
);
if (pendingDelayedCommitReason !== ABORTED_VIEW_TRANSITION_COMMIT) {
pendingDelayedCommitReason = ANIMATION_STARTED_COMMIT;
@@ -4327,11 +4434,7 @@ function flushPassiveEffectsImpl() {
passiveEffectStartTime = now();
if (pendingDelayedCommitReason === ANIMATION_STARTED_COMMIT) {
// The animation was started, so we've been animating since that happened.
logAnimatingPhase(
commitEndTime,
passiveEffectStartTime,
workInProgressUpdateTask, // TODO: Use a ViewTransition Task
);
logAnimatingPhase(commitEndTime, passiveEffectStartTime, animatingTask);
} else {
logPaintYieldPhase(
commitEndTime,

View File

@@ -22,6 +22,7 @@ import {
includesTransitionLane,
includesBlockingLane,
includesSyncLane,
NoLanes,
} from './ReactFiberLane';
import {resolveEventType, resolveEventTimeStamp} from './ReactFiberConfig';
@@ -88,6 +89,12 @@ export let transitionEventType: null | string = null; // Event type of the first
export let transitionEventIsRepeat: boolean = false;
export let transitionSuspendedTime: number = -1.1;
export let retryClampTime: number = -0;
export let idleClampTime: number = -0;
export let animatingLanes: Lanes = NoLanes;
export let animatingTask: null | ConsoleTask = null; // First ViewTransition applying an Animation.
export let yieldReason: SuspendedReason = (0: any);
export let yieldStartTime: number = -1.1; // The time when we yielded to the event loop
@@ -306,6 +313,20 @@ export function clampTransitionTimers(finalTime: number): void {
transitionClampTime = finalTime;
}
export function clampRetryTimers(finalTime: number): void {
if (!enableProfilerTimer || !enableComponentPerformanceTrack) {
return;
}
retryClampTime = finalTime;
}
export function clampIdleTimers(finalTime: number): void {
if (!enableProfilerTimer || !enableComponentPerformanceTrack) {
return;
}
idleClampTime = finalTime;
}
export function pushNestedEffectDurations(): number {
if (!enableProfilerTimer || !enableProfilerCommitHooks) {
return 0;
@@ -578,3 +599,19 @@ export function transferActualDuration(fiber: Fiber): void {
child = child.sibling;
}
}
export function startAnimating(lanes: Lanes): void {
animatingLanes |= lanes;
animatingTask = null;
}
export function stopAnimating(lanes: Lanes): void {
animatingLanes &= ~lanes;
animatingTask = null;
}
export function trackAnimatingTask(task: ConsoleTask): void {
if (animatingTask === null) {
animatingTask = task;
}
}

View File

@@ -114,6 +114,9 @@ describe('ReactFiberHostContext', () => {
waitForCommitToBeReady(state, timeoutOffset) {
return null;
},
getSuspendedCommitReason(state, rootContainer) {
return null;
},
supportsMutation: true,
});

View File

@@ -99,6 +99,7 @@ export const suspendInstance = $$$config.suspendInstance;
export const suspendOnActiveViewTransition =
$$$config.suspendOnActiveViewTransition;
export const waitForCommitToBeReady = $$$config.waitForCommitToBeReady;
export const getSuspendedCommitReason = $$$config.getSuspendedCommitReason;
export const NotPendingTransition = $$$config.NotPendingTransition;
export const HostTransitionContext = $$$config.HostTransitionContext;
export const resetFormInstance = $$$config.resetFormInstance;

View File

@@ -132,7 +132,19 @@ export function resolveServerReference<T>(
);
}
}
// TODO: This needs to return async: true if it's an async module.
if (resolvedModuleData.async) {
// If the module is marked as async in a Client Reference, we don't actually care.
// What matters is whether the consumer wants to unwrap it or not.
// For Server References, it is different because the consumer is completely internal
// to the bundler. So instead of passing it to each reference we can mark it in the
// manifest.
return [
resolvedModuleData.id,
resolvedModuleData.chunks,
name,
1 /* async */,
];
}
return [resolvedModuleData.id, resolvedModuleData.chunks, name];
}

View File

@@ -4702,6 +4702,70 @@ function renderDebugModel(
element._store.validated,
];
}
case REACT_LAZY_TYPE: {
// To avoid actually initializing a lazy causing a side-effect, we make
// some assumptions about the structure of the payload even though
// that's not really part of the contract. In practice, this is really
// just coming from React.lazy helper or Flight.
const lazy: LazyComponent<any, any> = (value: any);
const payload = lazy._payload;
if (payload !== null && typeof payload === 'object') {
// React.lazy constructor
switch (payload._status) {
case -1 /* Uninitialized */:
case 0 /* Pending */:
break;
case 1 /* Resolved */: {
const id = outlineDebugModel(request, counter, payload._result);
return serializeLazyID(id);
}
case 2 /* Rejected */: {
// We don't log these errors since they didn't actually throw into
// Flight.
const digest = '';
const id = request.nextChunkId++;
emitErrorChunk(request, id, digest, payload._result, true, null);
return serializeLazyID(id);
}
}
// React Flight
switch (payload.status) {
case 'pending':
case 'blocked':
case 'resolved_model':
// The value is an uninitialized model from the Flight client.
// It's not very useful to emit that.
break;
case 'resolved_module':
// The value is client reference metadata from the Flight client.
// It's likely for SSR, so we choose not to emit it.
break;
case 'fulfilled': {
const id = outlineDebugModel(request, counter, payload.value);
return serializeLazyID(id);
}
case 'rejected': {
// We don't log these errors since they didn't actually throw into
// Flight.
const digest = '';
const id = request.nextChunkId++;
emitErrorChunk(request, id, digest, payload.reason, true, null);
return serializeLazyID(id);
}
}
}
// We couldn't emit a resolved or rejected value synchronously. For now,
// we emit this as a halted chunk. TODO: We could maybe also handle
// pending lazy debug models like we do in serializeDebugThenable,
// if/when we determine that it's worth the added complexity.
request.pendingDebugChunks++;
const id = request.nextChunkId++;
emitDebugHaltChunk(request, id);
return serializeLazyID(id);
}
}
// $FlowFixMe[method-unbinding]

View File

@@ -424,6 +424,7 @@ export function startViewTransition(
passiveCallback: () => mixed,
errorCallback: mixed => void,
blockedCallback: string => void, // Profiling-only
finishedAnimation: () => void, // Profiling-only
): null | RunningViewTransition {
mutationCallback();
layoutCallback();
@@ -589,6 +590,13 @@ export function waitForCommitToBeReady(
return null;
}
export function getSuspendedCommitReason(
state: SuspendedState,
rootContainer: Container,
): null | string {
return null;
}
export const NotPendingTransition: TransitionStatus = null;
export const HostTransitionContext: ReactContext<TransitionStatus> = {
$$typeof: REACT_CONTEXT_TYPE,

View File

@@ -37,7 +37,7 @@ The high level process of creating releases is [documented below](#process). Ind
If your code lands in the main branch, it will be automatically published to the prerelease channels within the next weekday. However, if you want to immediately publish a prerelease, you can trigger the job to run immediately via the GitHub UI:
1. Wait for the commit you want to release to finish its [(Runtime) Build and Test workflow](https://github.com/facebook/react/actions/workflows/runtime_build_and_test.yml), as the prerelease script needs to download the build from that workflow.
2. Copy the git sha of whichever commit you are trying to release
2. Copy the full git sha of whichever commit you are trying to release
3. Go to https://github.com/facebook/react/actions/workflows/runtime_prereleases_manual.yml
4. Paste the git sha into the "Run workflow" dropdown
5. Let the job finish and it will be released on npm