Compare commits

..

1 Commits

Author SHA1 Message Date
Jordan Brown
28a86ffe47 [lint] Allow useEffectEvent in useLayoutEffect and useInsertionEffect 2025-09-15 09:11:08 -04:00
84 changed files with 667 additions and 3444 deletions

View File

@@ -577,7 +577,6 @@ module.exports = {
$AsyncIterator: 'readonly',
Iterator: 'readonly',
AsyncIterator: 'readonly',
IntervalID: 'readonly',
IteratorResult: 'readonly',
JSONValue: 'readonly',
JSResourceReference: 'readonly',

View File

@@ -6,51 +6,52 @@
*/
import MonacoEditor, {loader, type Monaco} from '@monaco-editor/react';
import {parseConfigPragmaAsString} from 'babel-plugin-react-compiler';
import type {editor} from 'monaco-editor';
import * as monaco from 'monaco-editor';
import {useState} from 'react';
import parserBabel from 'prettier/plugins/babel';
import * as prettierPluginEstree from 'prettier/plugins/estree';
import * as prettier from 'prettier/standalone';
import {useState, useEffect} from 'react';
import {Resizable} from 're-resizable';
import {useStore, useStoreDispatch} from '../StoreContext';
import {useStore} from '../StoreContext';
import {monacoOptions} from './monacoOptions';
import {
generateOverridePragmaFromConfig,
updateSourceWithOverridePragma,
} from '../../lib/configUtils';
loader.config({monaco});
export default function ConfigEditor(): JSX.Element {
const [, setMonaco] = useState<Monaco | null>(null);
const store = useStore();
const dispatchStore = useStoreDispatch();
const handleChange: (value: string | undefined) => void = async value => {
if (value === undefined) return;
// Parse string-based override config from pragma comment and format it
const [configJavaScript, setConfigJavaScript] = useState('');
try {
const newPragma = await generateOverridePragmaFromConfig(value);
const updatedSource = updateSourceWithOverridePragma(
store.source,
newPragma,
);
useEffect(() => {
const pragma = store.source.substring(0, store.source.indexOf('\n'));
const configString = `(${parseConfigPragmaAsString(pragma)})`;
// Update the store with both the new config and updated source
dispatchStore({
type: 'updateFile',
payload: {
source: updatedSource,
config: value,
},
prettier
.format(configString, {
semi: true,
parser: 'babel-ts',
plugins: [parserBabel, prettierPluginEstree],
})
.then(formatted => {
setConfigJavaScript(formatted);
})
.catch(error => {
console.error('Error formatting config:', error);
setConfigJavaScript('({})'); // Return empty object if not valid for now
//TODO: Add validation and error handling for config
});
} catch (_) {
dispatchStore({
type: 'updateFile',
payload: {
source: store.source,
config: value,
},
});
}
console.log('Config:', configString);
}, [store.source]);
const handleChange: (value: string | undefined) => void = value => {
if (!value) return;
// TODO: Implement sync logic to update pragma comments in the source
console.log('Config changed:', value);
};
const handleMount: (
@@ -80,11 +81,12 @@ export default function ConfigEditor(): JSX.Element {
<MonacoEditor
path={'config.js'}
language={'javascript'}
value={store.config}
value={configJavaScript}
onMount={handleMount}
onChange={handleChange}
options={{
...monacoOptions,
readOnly: true,
lineNumbers: 'off',
folding: false,
renderLineHighlight: 'none',

View File

@@ -48,7 +48,6 @@ import {
import {transformFromAstSync} from '@babel/core';
import {LoggerEvent} from 'babel-plugin-react-compiler/dist/Entrypoint';
import {useSearchParams} from 'next/navigation';
import {parseAndFormatConfig} from '../../lib/configUtils';
function parseInput(
input: string,
@@ -316,17 +315,9 @@ export default function Editor(): JSX.Element {
});
mountStore = defaultStore;
}
parseAndFormatConfig(mountStore.source).then(config => {
dispatchStore({
type: 'setStore',
payload: {
store: {
...mountStore,
config,
},
},
});
dispatchStore({
type: 'setStore',
payload: {store: mountStore},
});
});

View File

@@ -17,7 +17,6 @@ import {useStore, useStoreDispatch} from '../StoreContext';
import {monacoOptions} from './monacoOptions';
// @ts-expect-error TODO: Make TS recognize .d.ts files, in addition to loading them with webpack.
import React$Types from '../../node_modules/@types/react/index.d.ts';
import {parseAndFormatConfig} from '../../lib/configUtils.ts';
loader.config({monaco});
@@ -80,17 +79,13 @@ export default function Input({errors, language}: Props): JSX.Element {
});
}, [monaco, language]);
const handleChange: (value: string | undefined) => void = async value => {
const handleChange: (value: string | undefined) => void = value => {
if (!value) return;
// Parse and format the config
const config = await parseAndFormatConfig(value);
dispatchStore({
type: 'updateFile',
payload: {
source: value,
config,
},
});
};

View File

@@ -56,7 +56,6 @@ type ReducerAction =
type: 'updateFile';
payload: {
source: string;
config?: string;
};
};
@@ -67,11 +66,10 @@ function storeReducer(store: Store, action: ReducerAction): Store {
return newStore;
}
case 'updateFile': {
const {source, config} = action.payload;
const {source} = action.payload;
const newStore = {
...store,
source,
config,
};
return newStore;
}

View File

@@ -1,87 +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 parserBabel from 'prettier/plugins/babel';
import prettierPluginEstree from 'prettier/plugins/estree';
import * as prettier from 'prettier/standalone';
import {parseConfigPragmaAsString} from '../../../packages/babel-plugin-react-compiler/src/Utils/TestUtils';
/**
* Parse config from pragma and format it with prettier
*/
export async function parseAndFormatConfig(source: string): Promise<string> {
const pragma = source.substring(0, source.indexOf('\n'));
let configString = parseConfigPragmaAsString(pragma);
if (configString !== '') {
configString = `(${configString})`;
}
try {
const formatted = await prettier.format(configString, {
semi: true,
parser: 'babel-ts',
plugins: [parserBabel, prettierPluginEstree],
});
return formatted;
} catch (error) {
console.error('Error formatting config:', error);
return ''; // Return empty string if not valid for now
}
}
function extractCurlyBracesContent(input: string): string {
const startIndex = input.indexOf('{');
const endIndex = input.lastIndexOf('}');
if (startIndex === -1 || endIndex === -1 || endIndex <= startIndex) {
throw new Error('No outer curly braces found in input');
}
return input.slice(startIndex, endIndex + 1);
}
function cleanContent(content: string): string {
return content
.replace(/[\r\n]+/g, ' ')
.replace(/\s+/g, ' ')
.trim();
}
/**
* Generate a the override pragma comment from a formatted config object string
*/
export async function generateOverridePragmaFromConfig(
formattedConfigString: string,
): Promise<string> {
const content = extractCurlyBracesContent(formattedConfigString);
const cleanConfig = cleanContent(content);
// Format the config to ensure it's valid
await prettier.format(`(${cleanConfig})`, {
semi: false,
parser: 'babel-ts',
plugins: [parserBabel, prettierPluginEstree],
});
return `// @OVERRIDE:${cleanConfig}`;
}
/**
* Update the override pragma comment in source code.
*/
export function updateSourceWithOverridePragma(
source: string,
newPragma: string,
): string {
const firstLineEnd = source.indexOf('\n');
const firstLine = source.substring(0, firstLineEnd);
const pragmaRegex = /^\/\/\s*@/;
if (firstLineEnd !== -1 && pragmaRegex.test(firstLine.trim())) {
return newPragma + source.substring(firstLineEnd);
} else {
return newPragma + '\n' + source;
}
}

View File

@@ -15,10 +15,8 @@ export default function MyApp() {
export const defaultStore: Store = {
source: index,
config: '',
};
export const emptyStore: Store = {
source: '',
config: '',
};

View File

@@ -17,7 +17,6 @@ import {defaultStore} from '../defaultStore';
*/
export interface Store {
source: string;
config?: string;
}
export function encodeStore(store: Store): string {
return compressToEncodedURIComponent(JSON.stringify(store));
@@ -66,14 +65,5 @@ export function initStoreFromUrlOrLocalStorage(): Store {
const raw = decodeStore(encodedSource);
invariant(isValidStore(raw), 'Invalid Store');
// Add config property if missing for backwards compatibility
if (!('config' in raw)) {
return {
...raw,
config: '',
};
}
return raw;
}

View File

@@ -1,6 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference path="./.next/types/routes.d.ts" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@@ -34,30 +34,26 @@
"invariant": "^2.2.4",
"lz-string": "^1.5.0",
"monaco-editor": "^0.52.0",
"next": "15.5.2",
"next": "^15.2.0-canary.64",
"notistack": "^3.0.0-alpha.7",
"prettier": "^3.3.3",
"pretty-format": "^29.3.1",
"re-resizable": "^6.9.16",
"react": "19.1.1",
"react-dom": "19.1.1"
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@types/node": "18.11.9",
"@types/react": "19.1.12",
"@types/react-dom": "19.1.9",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"autoprefixer": "^10.4.13",
"clsx": "^1.2.1",
"concurrently": "^7.4.0",
"eslint": "^8.28.0",
"eslint-config-next": "15.5.2",
"eslint-config-next": "^15.0.1",
"monaco-editor-webpack-plugin": "^7.1.0",
"postcss": "^8.4.31",
"tailwindcss": "^3.2.4",
"wait-on": "^7.2.0"
},
"resolutions": {
"@types/react": "19.1.12",
"@types/react-dom": "19.1.9"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -575,9 +575,7 @@ export enum ErrorCategory {
// Checks for no setState in effect bodies
EffectSetState = 'EffectSetState',
EffectDerivationDeriveInRender = 'EffectDerivationDeriveInRender',
EffectDerivationShadowingParentState = 'EffectDerivationShadowingParentState',
EffectDerivationsOfState = 'EffectDerivationsOfState',
// Validates against try/catch in place of error boundaries
ErrorBoundaries = 'ErrorBoundaries',
@@ -694,21 +692,12 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
recommended: false,
};
}
case ErrorCategory.EffectDerivationDeriveInRender: {
case ErrorCategory.EffectDerivationsOfState: {
return {
category,
name: 'no-deriving-state-in-effects',
description:
'Validates if a useEffect is deriving state from props and/or local state that could be calculated in render.',
recommended: false,
};
}
case ErrorCategory.EffectDerivationShadowingParentState: {
return {
category,
name: 'no-deriving-state-in-effects',
description:
'Validates if a useEffect is deriving state from parent state and if the component is updating the shadowed state locally.',
'Validates against deriving values from state in an effect',
recommended: false,
};
}

View File

@@ -2089,7 +2089,7 @@ function computeSignatureForInstruction(
effects.push({
kind: 'Freeze',
value: operand,
reason: ValueReason.HookCaptured,
reason: ValueReason.Other,
});
}
}

View File

@@ -175,41 +175,6 @@ export function alignReactiveScopesToBlockScopesHIR(fn: HIRFunction): void {
if (node != null) {
valueBlockNodes.set(fallthrough, node);
}
} else if (terminal.kind === 'goto') {
/**
* If we encounter a goto that is not to the natural fallthrough of the current
* block (not the topmost fallthrough on the stack), then this is a goto to a
* label. Any scopes that extend beyond the goto must be extended to include
* the labeled range, so that the break statement doesn't accidentally jump
* out of the scope. We do this by extending the start and end of the scope's
* range to the label and its fallthrough respectively.
*/
const start = activeBlockFallthroughRanges.find(
range => range.fallthrough === terminal.block,
);
if (start != null && start !== activeBlockFallthroughRanges.at(-1)) {
const fallthroughBlock = fn.body.blocks.get(start.fallthrough)!;
const firstId =
fallthroughBlock.instructions[0]?.id ?? fallthroughBlock.terminal.id;
for (const scope of activeScopes) {
/**
* activeScopes is only filtered at block start points, so some of the
* scopes may not actually be active anymore, ie we've past their end
* instruction. Only extend ranges for scopes that are actually active.
*
* TODO: consider pruning activeScopes per instruction
*/
if (scope.range.end <= terminal.id) {
continue;
}
scope.range.start = makeInstructionId(
Math.min(start.range.start, scope.range.start),
);
scope.range.end = makeInstructionId(
Math.max(firstId, scope.range.end),
);
}
}
}
/*

View File

@@ -411,9 +411,7 @@ class CollectDependenciesVisitor extends ReactiveFunctionVisitor<
this.state = state;
this.options = {
memoizeJsxElements: !this.env.config.enableForest,
forceMemoizePrimitives:
this.env.config.enableForest ||
this.env.config.enablePreserveExistingMemoizationGuarantees,
forceMemoizePrimitives: this.env.config.enableForest,
};
}
@@ -536,23 +534,9 @@ class CollectDependenciesVisitor extends ReactiveFunctionVisitor<
case 'JSXText':
case 'BinaryExpression':
case 'UnaryExpression': {
if (options.forceMemoizePrimitives) {
/**
* Because these instructions produce primitives we usually don't consider
* them as escape points: they are known to copy, not return references.
* However if we're forcing memoization of primitives then we mark these
* instructions as needing memoization and walk their rvalues to ensure
* any scopes transitively reachable from the rvalues are considered for
* memoization. Note: we may still prune primitive-producing scopes if
* they don't ultimately escape at all.
*/
const level = MemoizationLevel.Conditional;
return {
lvalues: lvalue !== null ? [{place: lvalue, level}] : [],
rvalues: [...eachReactiveValueOperand(value)],
};
}
const level = MemoizationLevel.Never;
const level = options.forceMemoizePrimitives
? MemoizationLevel.Memoized
: MemoizationLevel.Never;
return {
// All of these instructions return a primitive value and never need to be memoized
lvalues: lvalue !== null ? [{place: lvalue, level}] : [],
@@ -701,7 +685,9 @@ class CollectDependenciesVisitor extends ReactiveFunctionVisitor<
}
case 'ComputedLoad':
case 'PropertyLoad': {
const level = MemoizationLevel.Conditional;
const level = options.forceMemoizePrimitives
? MemoizationLevel.Memoized
: MemoizationLevel.Conditional;
return {
// Indirection for the inner value, memoized if the value is
lvalues: lvalue !== null ? [{place: lvalue, level}] : [],

View File

@@ -255,16 +255,11 @@ function parseConfigStringAsJS(
console.log('OVERRIDE:', parsedConfig);
const environment = parseConfigPragmaEnvironmentForTest(
'',
defaults.environment ?? {},
);
const options: Record<keyof PluginOptions, unknown> = {
...defaultOptions,
panicThreshold: 'all_errors',
compilationMode: defaults.compilationMode,
environment,
environment: defaults.environment ?? defaultOptions.environment,
};
// Apply parsed config, merging environment if it exists
@@ -274,9 +269,22 @@ function parseConfigStringAsJS(
...parsedConfig.environment,
};
// Apply complex defaults for environment flags that are set to true
const environmentConfig: Partial<Record<keyof EnvironmentConfig, unknown>> =
{};
for (const [key, value] of Object.entries(mergedEnvironment)) {
if (hasOwnProperty(EnvironmentConfigSchema.shape, key)) {
if (value === true && key in testComplexConfigDefaults) {
environmentConfig[key] = testComplexConfigDefaults[key];
} else {
environmentConfig[key] = value;
}
}
}
// Validate environment config
const validatedEnvironment =
EnvironmentConfigSchema.safeParse(mergedEnvironment);
EnvironmentConfigSchema.safeParse(environmentConfig);
if (!validatedEnvironment.success) {
CompilerError.invariant(false, {
reason: 'Invalid environment configuration in config pragma',
@@ -286,6 +294,10 @@ function parseConfigStringAsJS(
});
}
if (validatedEnvironment.data.enableResetCacheOnSourceFileChanges == null) {
validatedEnvironment.data.enableResetCacheOnSourceFileChanges = false;
}
options.environment = validatedEnvironment.data;
}
@@ -296,7 +308,9 @@ function parseConfigStringAsJS(
}
if (hasOwnProperty(defaultOptions, key)) {
if (key === 'target' && value === 'donotuse_meta_internal') {
if (value === true && key in testComplexPluginOptionDefaults) {
options[key] = testComplexPluginOptionDefaults[key];
} else if (key === 'target' && value === 'donotuse_meta_internal') {
options[key] = {
kind: value,
runtimeModule: 'react',

View File

@@ -5,53 +5,21 @@
* LICENSE file in the root directory of this source tree.
*/
import {
CompilerDiagnostic,
CompilerError,
Effect,
ErrorSeverity,
SourceLocation,
} from '..';
import {CompilerError, ErrorSeverity, SourceLocation} from '..';
import {ErrorCategory} from '../CompilerError';
import {
ArrayExpression,
BasicBlock,
BlockId,
FunctionExpression,
HIRFunction,
IdentifierId,
Instruction,
Place,
isSetStateType,
isUseEffectHookType,
isUseStateType,
GeneratedSource,
} from '../HIR';
import {eachInstructionOperand, eachInstructionLValue} from '../HIR/visitors';
import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables';
import {assertExhaustive} from '../Utils/utils';
type SetStateCall = {
loc: SourceLocation;
derivedDep: DerivationMetadata;
setStateId: IdentifierId;
};
type TypeOfValue = 'ignored' | 'fromProps' | 'fromState' | 'fromPropsOrState';
type DerivationMetadata = {
typeOfValue: TypeOfValue;
place: Place;
sources: Array<Place>;
};
type ErrorMetadata = {
type: TypeOfValue;
description: string | undefined;
loc: SourceLocation;
setStateName: string | undefined | null;
derivedDepsNames: Array<string>;
};
import {
eachInstructionValueOperand,
eachTerminalOperand,
} from '../HIR/visitors';
/**
* Validates that useEffect is not used for derived computations which could/should
@@ -80,46 +48,12 @@ export function validateNoDerivedComputationsInEffects(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 derivationCache: Map<IdentifierId, DerivationMetadata> = new Map();
const shadowingUseState: Map<string, Array<SourceLocation>> = new Map();
const effectSetStates: Map<
string | undefined | null,
Array<Place>
> = new Map();
const setStateCalls: Map<string | undefined | null, Array<Place>> = new Map();
const errors: Array<ErrorMetadata> = [];
if (fn.fnType === 'Hook') {
for (const param of fn.params) {
if (param.kind === 'Identifier') {
derivationCache.set(param.identifier.id, {
place: param,
sources: [param],
typeOfValue: 'fromProps',
});
}
}
} else if (fn.fnType === 'Component') {
const props = fn.params[0];
if (props != null && props.kind === 'Identifier') {
derivationCache.set(props.identifier.id, {
place: props,
sources: [props],
typeOfValue: 'fromProps',
});
}
}
const errors = new CompilerError();
for (const block of fn.body.blocks.values()) {
parseBlockPhi(block, derivationCache);
for (const instr of block.instructions) {
const {lvalue, value} = instr;
parseInstr(instr, derivationCache, setStateCalls, shadowingUseState);
if (value.kind === 'LoadLocal') {
locals.set(lvalue.identifier.id, value.place.identifier.id);
} else if (value.kind === 'ArrayExpression') {
@@ -132,7 +66,6 @@ export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
) {
const callee =
value.kind === 'CallExpression' ? value.callee : value.property;
if (
isUseEffectHookType(callee.identifier) &&
value.args.length === 2 &&
@@ -157,8 +90,6 @@ export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
validateEffect(
effectFunction.loweredFunc.func,
dependencies,
derivationCache,
effectSetStates,
errors,
);
}
@@ -166,330 +97,43 @@ export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
}
}
}
const compilerError = generateCompilerError(
setStateCalls,
effectSetStates,
shadowingUseState,
errors,
);
if (compilerError.hasErrors()) {
throw compilerError;
}
}
function generateCompilerError(
setStateCalls: Map<string | undefined | null, Array<Place>>,
effectSetStates: Map<string | undefined | null, Array<Place>>,
shadowingUseState: Map<string, Array<SourceLocation>>,
errors: Array<ErrorMetadata>,
): CompilerError {
const throwableErrors = new CompilerError();
for (const error of errors) {
let compilerDiagnostic: CompilerDiagnostic | undefined = undefined;
/*
* If we use a setState from an invalid useEffect elsewhere then we probably have to
* hoist state up, else we should calculate in render
*/
if (
setStateCalls.get(error.setStateName)?.length !=
effectSetStates.get(error.setStateName)?.length &&
error.type !== 'fromState'
) {
compilerDiagnostic = CompilerDiagnostic.create({
description: `The setState within a useEffect is deriving from ${error.description}. Instead of shadowing the prop with local state, hoist the state to the parent component and update it there. If you are purposefully initializing state with a prop, and want to update it when a prop changes, do so conditionally in render`,
category: ErrorCategory.EffectDerivationShadowingParentState,
severity: ErrorSeverity.InvalidReact,
reason:
'You might not need an effect. Local state shadows parent state.',
}).withDetail({
kind: 'error',
loc: error.loc,
message: `this derives values from props ${error.type === 'fromPropsOrState' ? 'and local state ' : ''}to synchronize state`,
});
for (const derivedDep of error.derivedDepsNames) {
if (shadowingUseState.has(derivedDep)) {
for (const loc of shadowingUseState.get(derivedDep)!) {
compilerDiagnostic.withDetail({
kind: 'error',
loc: loc,
message: `this useState shadows ${derivedDep}`,
});
}
}
}
for (const [key, setStateCallArray] of effectSetStates) {
if (setStateCallArray.length === 0) {
continue;
}
const nonUseEffectSetStateCalls = setStateCalls.get(key);
if (nonUseEffectSetStateCalls) {
for (const place of nonUseEffectSetStateCalls) {
if (!setStateCallArray.includes(place)) {
compilerDiagnostic.withDetail({
kind: 'error',
loc: place.loc,
message:
'this setState updates the shadowed state, but should call an onChange event from the parent',
});
}
}
}
}
} else {
compilerDiagnostic = CompilerDiagnostic.create({
description: `${error.description ? error.description.charAt(0).toUpperCase() + error.description.slice(1) : ''}. Derived values should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.`,
category: ErrorCategory.EffectDerivationDeriveInRender,
severity: ErrorSeverity.InvalidReact,
reason:
'You might not need an effect. Derive values in render, not effects.',
}).withDetail({
kind: 'error',
loc: error.loc,
message: 'This should be computed during render, not in an effect',
});
}
if (compilerDiagnostic) {
throwableErrors.pushDiagnostic(compilerDiagnostic);
}
}
return throwableErrors;
}
function joinValue(
lvalueType: TypeOfValue,
valueType: TypeOfValue,
): TypeOfValue {
if (lvalueType === 'ignored') return valueType;
if (valueType === 'ignored') return lvalueType;
if (lvalueType === valueType) return lvalueType;
return 'fromPropsOrState';
}
function updateDerivationMetadata(
target: Place,
sources: Array<DerivationMetadata> | undefined,
typeOfValue: TypeOfValue | undefined,
derivationCache: Map<IdentifierId, DerivationMetadata>,
): void {
let newValue: DerivationMetadata = {
place: target,
sources: [],
typeOfValue: typeOfValue ?? 'ignored',
};
if (sources !== undefined) {
for (const source of sources) {
/*
* If the identifier of the source is a promoted identifier, then
* we should set the target as the source.
*/
for (const place of source.sources) {
if (
place.identifier.name === null ||
place.identifier.name?.kind === 'promoted'
) {
newValue.sources.push(target);
} else {
newValue.sources.push(place);
}
}
}
}
derivationCache.set(target.identifier.id, newValue);
}
function parseInstr(
instr: Instruction,
derivationCache: Map<IdentifierId, DerivationMetadata>,
setStateCalls: Map<string | undefined | null, Array<Place>>,
shadowingUseState: Map<string, Array<SourceLocation>>,
): void {
// Recursively parse function expressions
let typeOfValue: TypeOfValue = 'ignored';
let sources: Array<DerivationMetadata> = [];
if (instr.value.kind === 'FunctionExpression') {
for (const [, block] of instr.value.loweredFunc.func.body.blocks) {
for (const instr of block.instructions) {
parseInstr(instr, derivationCache, setStateCalls, shadowingUseState);
}
}
} else if (
instr.value.kind === 'CallExpression' &&
isSetStateType(instr.value.callee.identifier) &&
instr.value.args.length === 1 &&
instr.value.args[0].kind === 'Identifier' &&
instr.value.callee.loc !== GeneratedSource
) {
if (setStateCalls.has(instr.value.callee.loc.identifierName)) {
setStateCalls
.get(instr.value.callee.loc.identifierName)!
.push(instr.value.callee);
} else {
setStateCalls.set(instr.value.callee.loc.identifierName, [
instr.value.callee,
]);
}
} else if (
(instr.value.kind === 'CallExpression' ||
instr.value.kind === 'MethodCall') &&
isUseStateType(instr.lvalue.identifier) &&
instr.value.args.length > 0
) {
const stateValueSource = instr.value.args[0];
if (stateValueSource.kind === 'Identifier') {
sources.push({
place: stateValueSource,
typeOfValue: typeOfValue,
sources: [stateValueSource],
});
}
typeOfValue = joinValue(typeOfValue, 'fromState');
}
for (const operand of eachInstructionOperand(instr)) {
const opSource = derivationCache.get(operand.identifier.id);
if (opSource === undefined) {
continue;
}
typeOfValue = joinValue(typeOfValue, opSource.typeOfValue);
sources.push(opSource);
if (
(instr.value.kind === 'CallExpression' ||
instr.value.kind === 'MethodCall') &&
opSource.typeOfValue === 'fromProps' &&
isUseStateType(instr.lvalue.identifier)
) {
opSource.sources.forEach(source => {
if (source.identifier.name !== null) {
if (shadowingUseState.has(source.identifier.name.value)) {
shadowingUseState
.get(source.identifier.name.value)
?.push(instr.lvalue.loc);
} else {
shadowingUseState.set(source.identifier.name.value, [
instr.lvalue.loc,
]);
}
}
});
}
}
if (typeOfValue !== 'ignored') {
for (const lvalue of eachInstructionLValue(instr)) {
updateDerivationMetadata(lvalue, sources, typeOfValue, derivationCache);
}
for (const operand of eachInstructionOperand(instr)) {
switch (operand.effect) {
case Effect.Capture:
case Effect.Store:
case Effect.ConditionallyMutate:
case Effect.ConditionallyMutateIterator:
case Effect.Mutate: {
if (isMutable(instr, operand)) {
updateDerivationMetadata(
operand,
sources,
typeOfValue,
derivationCache,
);
}
break;
}
case Effect.Freeze:
case Effect.Read: {
// no-op
break;
}
case Effect.Unknown: {
CompilerError.invariant(false, {
reason: 'Unexpected unknown effect',
description: null,
loc: operand.loc,
suggestions: null,
});
}
default: {
assertExhaustive(
operand.effect,
`Unexpected effect kind \`${operand.effect}\``,
);
}
}
}
}
}
function parseBlockPhi(
block: BasicBlock,
derivationCache: Map<IdentifierId, DerivationMetadata>,
): void {
for (const phi of block.phis) {
let typeOfValue: TypeOfValue = 'ignored';
let sources: Array<DerivationMetadata> = [];
for (const operand of phi.operands.values()) {
const opSource = derivationCache.get(operand.identifier.id);
if (opSource === undefined) {
continue;
}
typeOfValue = joinValue(typeOfValue, opSource?.typeOfValue ?? 'ignored');
sources.push(opSource);
}
if (typeOfValue !== 'ignored') {
updateDerivationMetadata(
phi.place,
sources,
typeOfValue,
derivationCache,
);
}
if (errors.hasErrors()) {
throw errors;
}
}
function validateEffect(
effectFunction: HIRFunction,
effectDeps: Array<IdentifierId>,
derivationCache: Map<IdentifierId, DerivationMetadata>,
effectSetStates: Map<string | undefined | null, Array<Place>>,
errors: Array<ErrorMetadata>,
errors: CompilerError,
): void {
let isUsingDerivedDeps = false;
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) {
const depMetadata = derivationCache.get(dep);
if (
effectFunction.context.find(operand => operand.identifier.id === dep) !=
null ||
(depMetadata !== undefined && depMetadata.typeOfValue !== 'ignored')
effectFunction.context.find(operand => operand.identifier.id === dep) ==
null
) {
isUsingDerivedDeps = true;
// effect dep wasn't actually used in the function
return;
}
}
if (!isUsingDerivedDeps) {
// no prop/state derived deps were used in the body of the effect
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 seenBlocks: Set<BlockId> = new Set();
const derivedSetStateCall: Array<SetStateCall> = [];
const setStateLocations: Array<SourceLocation> = [];
for (const block of effectFunction.body.blocks.values()) {
for (const pred of block.preds) {
if (!seenBlocks.has(pred)) {
@@ -497,29 +141,21 @@ function validateEffect(
return;
}
}
parseBlockPhi(block, derivationCache);
for (const instr of block.instructions) {
if (
instr.value.kind === 'CallExpression' &&
isSetStateType(instr.value.callee.identifier) &&
instr.value.args.length === 1 &&
instr.value.args[0].kind === 'Identifier' &&
instr.value.callee.loc !== GeneratedSource &&
instr.value.callee.loc.identifierName !== undefined &&
instr.value.callee.loc.identifierName !== null
) {
if (effectSetStates.has(instr.value.callee.loc.identifierName)) {
effectSetStates
.get(instr.value.callee.loc.identifierName)!
.push(instr.value.callee);
} else {
effectSetStates.set(instr.value.callee.loc.identifierName, [
instr.value.callee,
]);
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':
@@ -527,6 +163,10 @@ function validateEffect(
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':
@@ -535,61 +175,58 @@ function validateEffect(
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 derivedDep = derivationCache.get(
instr.value.args[0].identifier.id,
);
if (derivedDep !== undefined) {
derivedSetStateCall.push({
loc: instr.value.callee.loc,
setStateId: instr.value.callee.identifier.id,
derivedDep: derivedDep,
});
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 call of derivedSetStateCall) {
const derivedDepsStr = Array.from(call.derivedDep.sources)
.map(place => {
return place.identifier.name?.value;
})
.filter(Boolean)
.join(', ');
let errorDescription = '';
if (call.derivedDep.typeOfValue === 'fromProps') {
errorDescription = `props [${derivedDepsStr}]`;
} else if (call.derivedDep.typeOfValue === 'fromState') {
errorDescription = `local state [${derivedDepsStr}]`;
} else {
errorDescription = `both props and local state [${derivedDepsStr}]`;
}
for (const loc of setStateLocations) {
errors.push({
type: call.derivedDep.typeOfValue,
description: `${errorDescription}`,
loc: call.loc,
setStateName:
call.loc !== GeneratedSource ? call.loc.identifierName : undefined,
derivedDepsNames: Array.from(call.derivedDep.sources)
.map(place => {
return place.identifier.name?.value ?? '';
})
.filter(Boolean),
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,
severity: ErrorSeverity.InvalidReact,
loc,
suggestions: null,
});
}
}

View File

@@ -46,16 +46,14 @@ function useFoo(t0) {
t1 = $[0];
}
let items = t1;
if ($[1] !== cond) {
bb0: {
if (cond) {
items = [];
} else {
break bb0;
}
items.push(2);
bb0: if ($[1] !== cond) {
if (cond) {
items = [];
} else {
break bb0;
}
items.push(2);
$[1] = cond;
$[2] = items;
} else {

View File

@@ -1,77 +0,0 @@
## Input
```javascript
// @compilationMode:"infer" @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees
import {useMemo} from 'react';
import {makeObject_Primitives, ValidateMemoization} from 'shared-runtime';
function Component(props) {
const result = useMemo(
() => makeObject(props.value).value + 1,
[props.value]
);
console.log(result);
return 'ok';
}
function makeObject(value) {
console.log(value);
return {value};
}
export const TODO_FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 42}],
sequentialRenders: [
{value: 42},
{value: 42},
{value: 3.14},
{value: 3.14},
{value: 42},
{value: 3.14},
{value: 42},
{value: 3.14},
],
};
```
## Code
```javascript
// @compilationMode:"infer" @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees
import { useMemo } from "react";
import { makeObject_Primitives, ValidateMemoization } from "shared-runtime";
function Component(props) {
const result = makeObject(props.value).value + 1;
console.log(result);
return "ok";
}
function makeObject(value) {
console.log(value);
return { value };
}
export const TODO_FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ value: 42 }],
sequentialRenders: [
{ value: 42 },
{ value: 42 },
{ value: 3.14 },
{ value: 3.14 },
{ value: 42 },
{ value: 3.14 },
{ value: 42 },
{ value: 3.14 },
],
};
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -1,32 +0,0 @@
// @compilationMode:"infer" @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees
import {useMemo} from 'react';
import {makeObject_Primitives, ValidateMemoization} from 'shared-runtime';
function Component(props) {
const result = useMemo(
() => makeObject(props.value).value + 1,
[props.value]
);
console.log(result);
return 'ok';
}
function makeObject(value) {
console.log(value);
return {value};
}
export const TODO_FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 42}],
sequentialRenders: [
{value: 42},
{value: 42},
{value: 3.14},
{value: 3.14},
{value: 42},
{value: 3.14},
{value: 42},
{value: 3.14},
],
};

View File

@@ -1,81 +0,0 @@
## Input
```javascript
// @compilationMode:"infer" @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees
import {useMemo} from 'react';
import {makeObject_Primitives, ValidateMemoization} from 'shared-runtime';
function Component(props) {
const result = makeObject(props.value).value + 1;
console.log(result);
return 'ok';
}
function makeObject(value) {
console.log(value);
return {value};
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 42}],
sequentialRenders: [
{value: 42},
{value: 42},
{value: 3.14},
{value: 3.14},
{value: 42},
{value: 3.14},
{value: 42},
{value: 3.14},
],
};
```
## Code
```javascript
// @compilationMode:"infer" @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees
import { useMemo } from "react";
import { makeObject_Primitives, ValidateMemoization } from "shared-runtime";
function Component(props) {
const result = makeObject(props.value).value + 1;
console.log(result);
return "ok";
}
function makeObject(value) {
console.log(value);
return { value };
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ value: 42 }],
sequentialRenders: [
{ value: 42 },
{ value: 42 },
{ value: 3.14 },
{ value: 3.14 },
{ value: 42 },
{ value: 3.14 },
{ value: 42 },
{ value: 3.14 },
],
};
```
### Eval output
(kind: ok) "ok"
"ok"
"ok"
"ok"
"ok"
"ok"
"ok"
"ok"
logs: [42,43,42,43,3.14,4.140000000000001,3.14,4.140000000000001,42,43,3.14,4.140000000000001,42,43,3.14,4.140000000000001]

View File

@@ -1,29 +0,0 @@
// @compilationMode:"infer" @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees
import {useMemo} from 'react';
import {makeObject_Primitives, ValidateMemoization} from 'shared-runtime';
function Component(props) {
const result = makeObject(props.value).value + 1;
console.log(result);
return 'ok';
}
function makeObject(value) {
console.log(value);
return {value};
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 42}],
sequentialRenders: [
{value: 42},
{value: 42},
{value: 3.14},
{value: 3.14},
{value: 42},
{value: 3.14},
{value: 42},
{value: 3.14},
],
};

View File

@@ -24,15 +24,13 @@ function BadExample() {
```
Found 1 error:
Error: You might not need an effect. Derive values in render, not effects.
Local state [firstName, lastName]. Derived values should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
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));
| ^^^^^^^^^^^ This should be computed during render, not in an effect
| ^^^^^^^^^^^ 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>;

View File

@@ -1,107 +0,0 @@
## Input
```javascript
// @compilationMode:"infer" @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees
import {useMemo} from 'react';
import {makeObject_Primitives, ValidateMemoization} from 'shared-runtime';
function Component(props) {
const result = useMemo(() => {
return makeObject(props.value).value + 1;
}, [props.value]);
return <ValidateMemoization inputs={[props.value]} output={result} />;
}
function makeObject(value) {
console.log(value);
return {value};
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 42}],
sequentialRenders: [
{value: 42},
{value: 42},
{value: 3.14},
{value: 3.14},
{value: 42},
{value: 3.14},
{value: 42},
{value: 3.14},
],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @compilationMode:"infer" @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees
import { useMemo } from "react";
import { makeObject_Primitives, ValidateMemoization } from "shared-runtime";
function Component(props) {
const $ = _c(7);
let t0;
if ($[0] !== props.value) {
t0 = makeObject(props.value);
$[0] = props.value;
$[1] = t0;
} else {
t0 = $[1];
}
const result = t0.value + 1;
let t1;
if ($[2] !== props.value) {
t1 = [props.value];
$[2] = props.value;
$[3] = t1;
} else {
t1 = $[3];
}
let t2;
if ($[4] !== result || $[5] !== t1) {
t2 = <ValidateMemoization inputs={t1} output={result} />;
$[4] = result;
$[5] = t1;
$[6] = t2;
} else {
t2 = $[6];
}
return t2;
}
function makeObject(value) {
console.log(value);
return { value };
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ value: 42 }],
sequentialRenders: [
{ value: 42 },
{ value: 42 },
{ value: 3.14 },
{ value: 3.14 },
{ value: 42 },
{ value: 3.14 },
{ value: 42 },
{ value: 3.14 },
],
};
```
### Eval output
(kind: ok) <div>{"inputs":[42],"output":43}</div>
<div>{"inputs":[42],"output":43}</div>
<div>{"inputs":[3.14],"output":4.140000000000001}</div>
<div>{"inputs":[3.14],"output":4.140000000000001}</div>
<div>{"inputs":[42],"output":43}</div>
<div>{"inputs":[3.14],"output":4.140000000000001}</div>
<div>{"inputs":[42],"output":43}</div>
<div>{"inputs":[3.14],"output":4.140000000000001}</div>
logs: [42,3.14,42,3.14,42,3.14]

View File

@@ -1,30 +0,0 @@
// @compilationMode:"infer" @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees
import {useMemo} from 'react';
import {makeObject_Primitives, ValidateMemoization} from 'shared-runtime';
function Component(props) {
const result = useMemo(() => {
return makeObject(props.value).value + 1;
}, [props.value]);
return <ValidateMemoization inputs={[props.value]} output={result} />;
}
function makeObject(value) {
console.log(value);
return {value};
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 42}],
sequentialRenders: [
{value: 42},
{value: 42},
{value: 3.14},
{value: 3.14},
{value: 42},
{value: 3.14},
{value: 42},
{value: 3.14},
],
};

View File

@@ -2,7 +2,6 @@
## Input
```javascript
// @compilationMode:"infer"
import {makeArray} from 'shared-runtime';
function Component() {
@@ -31,7 +30,7 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @compilationMode:"infer"
import { c as _c } from "react/compiler-runtime";
import { makeArray } from "shared-runtime";
function Component() {

View File

@@ -1,4 +1,3 @@
// @compilationMode:"infer"
import {makeArray} from 'shared-runtime';
function Component() {

View File

@@ -49,12 +49,12 @@ import {
} from "shared-runtime";
function useFoo(t0) {
const $ = _c(4);
const $ = _c(3);
const { data } = t0;
let obj;
let myDiv = null;
if ($[0] !== data.cond || $[1] !== data.cond1) {
bb0: if (data.cond) {
bb0: if (data.cond) {
if ($[0] !== data.cond1) {
obj = makeObject_Primitives();
if (data.cond1) {
myDiv = <Stringify value={mutateAndReturn(obj)} />;
@@ -62,14 +62,13 @@ function useFoo(t0) {
}
mutate(obj);
$[0] = data.cond1;
$[1] = obj;
$[2] = myDiv;
} else {
obj = $[1];
myDiv = $[2];
}
$[0] = data.cond;
$[1] = data.cond1;
$[2] = obj;
$[3] = myDiv;
} else {
obj = $[2];
myDiv = $[3];
}
return myDiv;
}

View File

@@ -1,86 +0,0 @@
## Input
```javascript
// @enablePreserveExistingMemoizationGuarantees
import {fbt} from 'fbt';
function Component() {
const buttonLabel = () => {
if (!someCondition) {
return <fbt desc="My label">{'Purchase as a gift'}</fbt>;
} else if (
!iconOnly &&
showPrice &&
item?.current_gift_offer?.price?.formatted != null
) {
return (
<fbt desc="Gift button's label">
{'Gift | '}
<fbt:param name="price">
{item?.current_gift_offer?.price?.formatted}
</fbt:param>
</fbt>
);
} else if (!iconOnly && !showPrice) {
return <fbt desc="Gift button's label">{'Gift'}</fbt>;
}
};
return (
<View>
<Button text={buttonLabel()} />
</View>
);
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enablePreserveExistingMemoizationGuarantees
import { fbt } from "fbt";
function Component() {
const $ = _c(1);
const buttonLabel = _temp;
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = (
<View>
<Button text={buttonLabel()} />
</View>
);
$[0] = t0;
} else {
t0 = $[0];
}
return t0;
}
function _temp() {
if (!someCondition) {
return fbt._("Purchase as a gift", null, { hk: "1gHj4g" });
} else {
if (
!iconOnly &&
showPrice &&
item?.current_gift_offer?.price?.formatted != null
) {
return fbt._(
"Gift | {price}",
[fbt._param("price", item?.current_gift_offer?.price?.formatted)],
{ hk: "3GTnGE" },
);
} else {
if (!iconOnly && !showPrice) {
return fbt._("Gift", null, { hk: "3fqfrk" });
}
}
}
}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -1,31 +0,0 @@
// @enablePreserveExistingMemoizationGuarantees
import {fbt} from 'fbt';
function Component() {
const buttonLabel = () => {
if (!someCondition) {
return <fbt desc="My label">{'Purchase as a gift'}</fbt>;
} else if (
!iconOnly &&
showPrice &&
item?.current_gift_offer?.price?.formatted != null
) {
return (
<fbt desc="Gift button's label">
{'Gift | '}
<fbt:param name="price">
{item?.current_gift_offer?.price?.formatted}
</fbt:param>
</fbt>
);
} else if (!iconOnly && !showPrice) {
return <fbt desc="Gift button's label">{'Gift'}</fbt>;
}
};
return (
<View>
<Button text={buttonLabel()} />
</View>
);
}

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @enablePreserveExistingMemoizationGuarantees
// @enableForest
function Component({base, start, increment, test}) {
let value = base;
for (let i = start; i < test; i += increment) {
@@ -27,23 +27,25 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enablePreserveExistingMemoizationGuarantees
import { c as _c } from "react/compiler-runtime"; // @enableForest
function Component(t0) {
const $ = _c(2);
const $ = _c(5);
const { base, start, increment, test } = t0;
let value = base;
for (let i = start; i < test; i = i + increment, i) {
value = value + i;
}
let t1;
if ($[0] !== value) {
t1 = <div>{value}</div>;
$[0] = value;
$[1] = t1;
let value;
if ($[0] !== base || $[1] !== increment || $[2] !== start || $[3] !== test) {
value = base;
for (let i = start; i < test; i = i + increment, i) {
value = value + i;
}
$[0] = base;
$[1] = increment;
$[2] = start;
$[3] = test;
$[4] = value;
} else {
t1 = $[1];
value = $[4];
}
return t1;
return <div>{value}</div>;
}
export const FIXTURE_ENTRYPOINT = {

View File

@@ -1,4 +1,4 @@
// @enablePreserveExistingMemoizationGuarantees
// @enableForest
function Component({base, start, increment, test}) {
let value = base;
for (let i = start; i < test; i += increment) {

View File

@@ -34,16 +34,17 @@ import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR
import { useMemo } from "react";
function Component(props) {
const $ = _c(5);
const $ = _c(6);
let t0;
if (
$[0] !== props.a ||
$[1] !== props.b ||
$[2] !== props.cond ||
$[3] !== props.cond2
) {
bb0: {
const y = [];
bb0: {
let y;
if (
$[0] !== props.a ||
$[1] !== props.b ||
$[2] !== props.cond ||
$[3] !== props.cond2
) {
y = [];
if (props.cond) {
y.push(props.a);
}
@@ -53,15 +54,17 @@ function Component(props) {
}
y.push(props.b);
t0 = y;
$[0] = props.a;
$[1] = props.b;
$[2] = props.cond;
$[3] = props.cond2;
$[4] = y;
$[5] = t0;
} else {
y = $[4];
t0 = $[5];
}
$[0] = props.a;
$[1] = props.b;
$[2] = props.cond;
$[3] = props.cond2;
$[4] = t0;
} else {
t0 = $[4];
t0 = y;
}
const x = t0;
return x;

View File

@@ -1,118 +0,0 @@
## Input
```javascript
import {useMemo} from 'react';
import {
makeObject_Primitives,
mutate,
Stringify,
ValidateMemoization,
} from 'shared-runtime';
function Component({cond}) {
const memoized = useMemo(() => {
const value = makeObject_Primitives();
if (cond) {
return value;
} else {
mutate(value);
return value;
}
}, [cond]);
return <ValidateMemoization inputs={[cond]} output={memoized} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{cond: false}],
sequentialRenders: [
{cond: false},
{cond: false},
{cond: true},
{cond: true},
{cond: false},
{cond: true},
{cond: false},
{cond: true},
],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { useMemo } from "react";
import {
makeObject_Primitives,
mutate,
Stringify,
ValidateMemoization,
} from "shared-runtime";
function Component(t0) {
const $ = _c(7);
const { cond } = t0;
let t1;
if ($[0] !== cond) {
const value = makeObject_Primitives();
if (cond) {
t1 = value;
} else {
mutate(value);
t1 = value;
}
$[0] = cond;
$[1] = t1;
} else {
t1 = $[1];
}
const memoized = t1;
let t2;
if ($[2] !== cond) {
t2 = [cond];
$[2] = cond;
$[3] = t2;
} else {
t2 = $[3];
}
let t3;
if ($[4] !== memoized || $[5] !== t2) {
t3 = <ValidateMemoization inputs={t2} output={memoized} />;
$[4] = memoized;
$[5] = t2;
$[6] = t3;
} else {
t3 = $[6];
}
return t3;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ cond: false }],
sequentialRenders: [
{ cond: false },
{ cond: false },
{ cond: true },
{ cond: true },
{ cond: false },
{ cond: true },
{ cond: false },
{ cond: true },
],
};
```
### Eval output
(kind: ok) <div>{"inputs":[false],"output":{"a":0,"b":"value1","c":true,"wat0":"joe"}}</div>
<div>{"inputs":[false],"output":{"a":0,"b":"value1","c":true,"wat0":"joe"}}</div>
<div>{"inputs":[true],"output":{"a":0,"b":"value1","c":true}}</div>
<div>{"inputs":[true],"output":{"a":0,"b":"value1","c":true}}</div>
<div>{"inputs":[false],"output":{"a":0,"b":"value1","c":true,"wat0":"joe"}}</div>
<div>{"inputs":[true],"output":{"a":0,"b":"value1","c":true}}</div>
<div>{"inputs":[false],"output":{"a":0,"b":"value1","c":true,"wat0":"joe"}}</div>
<div>{"inputs":[true],"output":{"a":0,"b":"value1","c":true}}</div>

View File

@@ -1,35 +0,0 @@
import {useMemo} from 'react';
import {
makeObject_Primitives,
mutate,
Stringify,
ValidateMemoization,
} from 'shared-runtime';
function Component({cond}) {
const memoized = useMemo(() => {
const value = makeObject_Primitives();
if (cond) {
return value;
} else {
mutate(value);
return value;
}
}, [cond]);
return <ValidateMemoization inputs={[cond]} output={memoized} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{cond: false}],
sequentialRenders: [
{cond: false},
{cond: false},
{cond: true},
{cond: true},
{cond: false},
{cond: true},
{cond: false},
{cond: true},
],
};

View File

@@ -1,87 +0,0 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function Component({initialName}) {
const [name, setName] = useState('');
useEffect(() => {
setName(initialName);
}, []);
return (
<div>
<input value={name} onChange={e => setName(e.target.value)} />
</div>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{initialName: 'John'}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects
import { useEffect, useState } from "react";
function Component(t0) {
const $ = _c(6);
const { initialName } = t0;
const [name, setName] = useState("");
let t1;
if ($[0] !== initialName) {
t1 = () => {
setName(initialName);
};
$[0] = initialName;
$[1] = t1;
} else {
t1 = $[1];
}
let t2;
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
t2 = [];
$[2] = t2;
} else {
t2 = $[2];
}
useEffect(t1, t2);
let t3;
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
t3 = (e) => setName(e.target.value);
$[3] = t3;
} else {
t3 = $[3];
}
let t4;
if ($[4] !== name) {
t4 = (
<div>
<input value={name} onChange={t3} />
</div>
);
$[4] = name;
$[5] = t4;
} else {
t4 = $[5];
}
return t4;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ initialName: "John" }],
};
```
### Eval output
(kind: ok) <div><input value="John"></div>

View File

@@ -1,21 +0,0 @@
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function Component({initialName}) {
const [name, setName] = useState('');
useEffect(() => {
setName(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,51 +0,0 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function Component({prefix}) {
const [name, setName] = useState('');
const [displayName, setDisplayName] = useState('');
useEffect(() => {
setDisplayName(prefix + name);
}, [prefix, name]);
return (
<div>
<input value={name} onChange={e => setName(e.target.value)} />
<div>{displayName}</div>
</div>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{prefix: 'Hello, '}],
};
```
## Error
```
Found 1 error:
Error: You might not need an effect. Derive values in render, not effects.
Both props and local state [prefix, name]. Derived values should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
error.bug-derived-state-from-mixed-deps.ts:9:4
7 |
8 | useEffect(() => {
> 9 | setDisplayName(prefix + name);
| ^^^^^^^^^^^^^^ This should be computed during render, not in an effect
10 | }, [prefix, name]);
11 |
12 | return (
```

View File

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

View File

@@ -1,86 +0,0 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects
import {useState, useEffect} from 'react';
function Component({props, number}) {
const nothing = 0;
const missDirection = number;
const [displayValue, setDisplayValue] = useState(
props.prefix + missDirection + nothing
);
useEffect(() => {
setDisplayValue(props.prefix + missDirection + nothing);
}, [props.prefix, missDirection, nothing]);
return (
<div
onClick={() => {
setDisplayValue('clicked');
}}>
{displayValue}
</div>
);
}
```
## Error
```
Found 1 error:
Error: You might not need an effect. Local state shadows parent state.
The setState within a useEffect is deriving from props [props, number]. Instead of shadowing the prop with local state, hoist the state to the parent component and update it there. If you are purposefully initializing state with a prop, and want to update it when a prop changes, do so conditionally in render
error.derived-state-from-shadowed-props.ts:12:4
10 |
11 | useEffect(() => {
> 12 | setDisplayValue(props.prefix + missDirection + nothing);
| ^^^^^^^^^^^^^^^ this derives values from props to synchronize state
13 | }, [props.prefix, missDirection, nothing]);
14 |
15 | return (
error.derived-state-from-shadowed-props.ts:7:42
5 | const nothing = 0;
6 | const missDirection = number;
> 7 | const [displayValue, setDisplayValue] = useState(
| ^^^^^^^^^
> 8 | props.prefix + missDirection + nothing
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 9 | );
| ^^^^ this useState shadows props
10 |
11 | useEffect(() => {
12 | setDisplayValue(props.prefix + missDirection + nothing);
error.derived-state-from-shadowed-props.ts:7:42
5 | const nothing = 0;
6 | const missDirection = number;
> 7 | const [displayValue, setDisplayValue] = useState(
| ^^^^^^^^^
> 8 | props.prefix + missDirection + nothing
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 9 | );
| ^^^^ this useState shadows number
10 |
11 | useEffect(() => {
12 | setDisplayValue(props.prefix + missDirection + nothing);
error.derived-state-from-shadowed-props.ts:18:8
16 | <div
17 | onClick={() => {
> 18 | setDisplayValue('clicked');
| ^^^^^^^^^^^^^^^ this setState updates the shadowed state, but should call an onChange event from the parent
19 | }}>
20 | {displayValue}
21 | </div>
```

View File

@@ -1,23 +0,0 @@
// @validateNoDerivedComputationsInEffects
import {useState, useEffect} from 'react';
function Component({props, number}) {
const nothing = 0;
const missDirection = number;
const [displayValue, setDisplayValue] = useState(
props.prefix + missDirection + nothing
);
useEffect(() => {
setDisplayValue(props.prefix + missDirection + nothing);
}, [props.prefix, missDirection, nothing]);
return (
<div
onClick={() => {
setDisplayValue('clicked');
}}>
{displayValue}
</div>
);
}

View File

@@ -1,49 +0,0 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects
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}],
};
```
## Error
```
Found 1 error:
Error: You might not need an effect. Derive values in render, not effects.
Props [value]. Derived values should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
error.derived-state-with-conditional.ts:9:6
7 | useEffect(() => {
8 | if (enabled) {
> 9 | setLocalValue(value);
| ^^^^^^^^^^^^^ This should be computed during render, not in an effect
10 | } else {
11 | setLocalValue('disabled');
12 | }
```

View File

@@ -1,21 +0,0 @@
// @validateNoDerivedComputationsInEffects
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,47 +0,0 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function Component({value}) {
const [localValue, setLocalValue] = useState('');
useEffect(() => {
console.log('Value changed:', value);
setLocalValue(value);
document.title = `Value: ${value}`;
}, [value]);
return <div>{localValue}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 'test'}],
};
```
## Error
```
Found 1 error:
Error: You might not need an effect. Derive values in render, not effects.
Props [value]. Derived values should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
error.derived-state-with-side-effects.ts:9:4
7 | useEffect(() => {
8 | console.log('Value changed:', value);
> 9 | setLocalValue(value);
| ^^^^^^^^^^^^^ This should be computed during render, not in an effect
10 | document.title = `Value: ${value}`;
11 | }, [value]);
12 |
```

View File

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

View File

@@ -1,46 +0,0 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
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: ']'}],
};
```
## Error
```
Found 1 error:
Error: You might not need an effect. Derive values in render, not effects.
Props [props, props, props]. Derived values should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
error.invalid-derived-state-from-props-computed.ts:9:4
7 | useEffect(() => {
8 | const computed = props.prefix + props.value + props.suffix;
> 9 | setDisplayValue(computed);
| ^^^^^^^^^^^^^^^ This should be computed during render, not in an effect
10 | }, [props.prefix, props.value, props.suffix]);
11 |
12 | return <div>{displayValue}</div>;
```

View File

@@ -1,18 +0,0 @@
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
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,47 +0,0 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
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: [{firstName: 'John', lastName: 'Doe'}],
};
```
## Error
```
Found 1 error:
Error: You might not need an effect. Derive values in render, not effects.
Props [props, props]. Derived values should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
error.invalid-derived-state-from-props-destructured.ts:10:4
8 |
9 | useEffect(() => {
> 10 | setFullName(props.firstName + ' ' + props.lastName);
| ^^^^^^^^^^^ This should be computed during render, not in an effect
11 | }, [props.firstName, props.lastName]);
12 |
13 | return <div>{fullName}</div>;
```

View File

@@ -1,19 +0,0 @@
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
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: [{firstName: 'John', lastName: 'Doe'}],
};

View File

@@ -1,45 +0,0 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function Component({firstName, lastName}) {
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
return <div>{fullName}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{firstName: 'John', lastName: 'Doe'}],
};
```
## Error
```
Found 1 error:
Error: You might not need an effect. Derive values in render, not effects.
Props [firstName, lastName]. Derived values should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
error.invalid-derived-state-from-props-in-effect.ts:8:4
6 |
7 | useEffect(() => {
> 8 | setFullName(firstName + ' ' + lastName);
| ^^^^^^^^^^^ This should be computed during render, not in an effect
9 | }, [firstName, lastName]);
10 |
11 | return <div>{fullName}</div>;
```

View File

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

View File

@@ -1,39 +0,0 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects
export default function InProductLobbyGeminiCard(input = 'empty') {
const [currInput, setCurrInput] = useState(input);
useEffect(() => {
setCurrInput(input);
}, [input]);
return <div>{currInput}</div>;
}
```
## Error
```
Found 1 error:
Error: You might not need an effect. Derive values in render, not effects.
Props [input]. Derived values should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
error.invalid-derived-state-from-props-with-default-value.ts:7:4
5 |
6 | useEffect(() => {
> 7 | setCurrInput(input);
| ^^^^^^^^^^^^ This should be computed during render, not in an effect
8 | }, [input]);
9 |
10 | return <div>{currInput}</div>;
```

View File

@@ -1,11 +0,0 @@
// @validateNoDerivedComputationsInEffects
export default function InProductLobbyGeminiCard(input = 'empty') {
const [currInput, setCurrInput] = useState(input);
useEffect(() => {
setCurrInput(input);
}, [input]);
return <div>{currInput}</div>;
}

View File

@@ -1,53 +0,0 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function Component() {
const [firstName, setFirstName] = useState('John');
const [lastName, setLastName] = useState('Doe');
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
return (
<div>
<input value={firstName} onChange={e => setFirstName(e.target.value)} />
<input value={lastName} onChange={e => setLastName(e.target.value)} />
<div>{fullName}</div>
</div>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [],
};
```
## Error
```
Found 1 error:
Error: You might not need an effect. Derive values in render, not effects.
Local state [firstName, lastName]. Derived values should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
error.invalid-derived-state-from-state-in-effect.ts:10:4
8 |
9 | useEffect(() => {
> 10 | setFullName(firstName + ' ' + lastName);
| ^^^^^^^^^^^ This should be computed during render, not in an effect
11 | }, [firstName, lastName]);
12 |
13 | return (
```

View File

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

View File

@@ -1,63 +0,0 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects
function EndDate({startDate, endDate, onStartDateChange}) {
const [localStartDate, setLocalStartDate] = useState(startDate);
useEffect(() => {
setLocalStartDate(startDate);
}, [startDate]);
const onChange = date => {
setLocalStartDate(date);
onStartDateChange(date);
};
return (
<DateInput value={localStartDate} second={endDate} onChange={onChange} />
);
}
```
## Error
```
Found 1 error:
Error: You might not need an effect. Local state shadows parent state.
The setState within a useEffect is deriving from props [startDate]. Instead of shadowing the prop with local state, hoist the state to the parent component and update it there. If you are purposefully initializing state with a prop, and want to update it when a prop changes, do so conditionally in render
error.shadowed-props-with-onchange.ts:7:4
5 |
6 | useEffect(() => {
> 7 | setLocalStartDate(startDate);
| ^^^^^^^^^^^^^^^^^ this derives values from props to synchronize state
8 | }, [startDate]);
9 |
10 | const onChange = date => {
error.shadowed-props-with-onchange.ts:4:46
2 |
3 | function EndDate({startDate, endDate, onStartDateChange}) {
> 4 | const [localStartDate, setLocalStartDate] = useState(startDate);
| ^^^^^^^^^^^^^^^^^^^ this useState shadows startDate
5 |
6 | useEffect(() => {
7 | setLocalStartDate(startDate);
error.shadowed-props-with-onchange.ts:11:4
9 |
10 | const onChange = date => {
> 11 | setLocalStartDate(date);
| ^^^^^^^^^^^^^^^^^ this setState updates the shadowed state, but should call an onChange event from the parent
12 | onStartDateChange(date);
13 | };
14 | return (
```

View File

@@ -1,17 +0,0 @@
// @validateNoDerivedComputationsInEffects
function EndDate({startDate, endDate, onStartDateChange}) {
const [localStartDate, setLocalStartDate] = useState(startDate);
useEffect(() => {
setLocalStartDate(startDate);
}, [startDate]);
const onChange = date => {
setLocalStartDate(date);
onStartDateChange(date);
};
return (
<DateInput value={localStartDate} second={endDate} onChange={onChange} />
);
}

View File

@@ -33,16 +33,17 @@ import { c as _c } from "react/compiler-runtime";
import { useMemo } from "react";
function Component(props) {
const $ = _c(5);
const $ = _c(6);
let t0;
if (
$[0] !== props.a ||
$[1] !== props.b ||
$[2] !== props.cond ||
$[3] !== props.cond2
) {
bb0: {
const y = [];
bb0: {
let y;
if (
$[0] !== props.a ||
$[1] !== props.b ||
$[2] !== props.cond ||
$[3] !== props.cond2
) {
y = [];
if (props.cond) {
y.push(props.a);
}
@@ -52,15 +53,17 @@ function Component(props) {
}
y.push(props.b);
t0 = y;
$[0] = props.a;
$[1] = props.b;
$[2] = props.cond;
$[3] = props.cond2;
$[4] = y;
$[5] = t0;
} else {
y = $[4];
t0 = $[5];
}
$[0] = props.a;
$[1] = props.b;
$[2] = props.cond;
$[3] = props.cond2;
$[4] = t0;
} else {
t0 = $[4];
t0 = y;
}
const x = t0;
return x;

View File

@@ -48,7 +48,10 @@ export {
printReactiveFunction,
printReactiveFunctionWithOutlined,
} from './ReactiveScopes';
export {parseConfigPragmaForTests} from './Utils/TestUtils';
export {
parseConfigPragmaForTests,
parseConfigPragmaAsString,
} from './Utils/TestUtils';
declare global {
let __DEV__: boolean | null | undefined;
}

View File

@@ -10494,7 +10494,16 @@ string-length@^4.0.1:
char-regex "^1.0.2"
strip-ansi "^6.0.0"
"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.npmjs.org/string-width/-/string-width-4.2.3.tgz"
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@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -10567,7 +10576,14 @@ 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.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -11344,7 +11360,7 @@ workerpool@^6.5.1:
resolved "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz"
integrity sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==
"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.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@@ -11362,6 +11378,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.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz"
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.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz"

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

@@ -6,7 +6,7 @@ const contentScriptsToInject = [
js: ['build/proxy.js'],
matches: ['<all_urls>'],
persistAcrossSessions: true,
runAt: 'document_start',
runAt: 'document_end',
world: chrome.scripting.ExecutionWorld.ISOLATED,
},
{

View File

@@ -1,20 +1,6 @@
/**
* 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.
*
* @flow
*/
/* global chrome */
export function executeScriptInIsolatedWorld({
target,
files,
}: {
files: any,
target: any,
}): Promise<void> {
export function executeScriptInIsolatedWorld({target, files}) {
return chrome.scripting.executeScript({
target,
files,
@@ -22,20 +8,10 @@ export function executeScriptInIsolatedWorld({
});
}
export function executeScriptInMainWorld({
target,
files,
injectImmediately,
}: {
files: any,
target: any,
// It's nice to have this required to make active choices.
injectImmediately: boolean,
}): Promise<void> {
export function executeScriptInMainWorld({target, files}) {
return chrome.scripting.executeScript({
target,
files,
injectImmediately,
world: chrome.scripting.ExecutionWorld.MAIN,
});
}

View File

@@ -1,6 +1,5 @@
/* global chrome */
import {__DEBUG__} from 'react-devtools-shared/src/constants';
import setExtensionIconAndPopup from './setExtensionIconAndPopup';
import {executeScriptInMainWorld} from './executeScript';
@@ -26,7 +25,6 @@ export function handleBackendManagerMessage(message, sender) {
payload.versions.forEach(version => {
if (EXTENSION_CONTAINED_VERSIONS.includes(version)) {
executeScriptInMainWorld({
injectImmediately: true,
target: {tabId: sender.tab.id},
files: [`/build/react_devtools_backend_${version}.js`],
});
@@ -81,19 +79,9 @@ export function handleDevToolsPageMessage(message) {
}
executeScriptInMainWorld({
injectImmediately: true,
target: {tabId},
files: ['/build/backendManager.js'],
}).then(
() => {
if (__DEBUG__) {
console.log('Successfully injected backend manager');
}
},
reason => {
console.error('Failed to inject backend manager:', reason);
},
);
});
break;
}

View File

@@ -1,16 +1,8 @@
/**
* 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.
*
* @flow
*/
/* global chrome */
'use strict';
function injectProxy({target}: {target: any}) {
window.addEventListener('pageshow', function ({target}) {
// Firefox's behaviour for injecting this content script can be unpredictable
// While navigating the history, some content scripts might not be re-injected and still be alive
if (!window.__REACT_DEVTOOLS_PROXY_INJECTED__) {
@@ -22,7 +14,7 @@ function injectProxy({target}: {target: any}) {
// The backend waits to install the global hook until notified by the content script.
// In the event of a page reload, the content script might be loaded before the backend manager is injected.
// Because of this we need to poll the backend manager until it has been initialized.
const intervalID: IntervalID = setInterval(() => {
const intervalID = setInterval(() => {
if (backendInitialized) {
clearInterval(intervalID);
} else {
@@ -30,11 +22,7 @@ function injectProxy({target}: {target: any}) {
}
}, 500);
}
}
window.addEventListener('pagereveal', injectProxy);
// For backwards compat with browsers not implementing `pagereveal` which is a fairly new event.
window.addEventListener('pageshow', injectProxy);
});
window.addEventListener('pagehide', function ({target}) {
if (target !== window.document) {
@@ -57,7 +45,7 @@ function sayHelloToBackendManager() {
);
}
function handleMessageFromDevtools(message: any) {
function handleMessageFromDevtools(message) {
window.postMessage(
{
source: 'react-devtools-content-script',
@@ -67,7 +55,7 @@ function handleMessageFromDevtools(message: any) {
);
}
function handleMessageFromPage(event: any) {
function handleMessageFromPage(event) {
if (event.source !== window || !event.data) {
return;
}
@@ -77,7 +65,6 @@ function handleMessageFromPage(event: any) {
case 'react-devtools-bridge': {
backendInitialized = true;
// $FlowFixMe[incompatible-use]
port.postMessage(event.data.payload);
break;
}
@@ -112,8 +99,6 @@ function connectPort() {
window.addEventListener('message', handleMessageFromPage);
// $FlowFixMe[incompatible-use]
port.onMessage.addListener(handleMessageFromDevtools);
// $FlowFixMe[incompatible-use]
port.onDisconnect.addListener(handleDisconnect);
}

View File

@@ -987,34 +987,6 @@ describe('Store', () => {
<Suspense name="two" rects={[{x:1,y:2,width:5,height:1}]}>
<Suspense name="three" rects={[{x:1,y:2,width:5,height:1}]}>
`);
await actAsync(() => {
agent.overrideSuspenseMilestone({
rendererID,
rootID,
suspendedSet: [],
});
});
expect(store).toMatchInlineSnapshot(`
[root]
▾ <App>
<Component key="Outside">
▾ <Suspense name="parent">
<Component key="Unrelated at Start">
▾ <Suspense name="one">
<Component key="Suspense 1 Content">
▾ <Suspense name="two">
<Component key="Suspense 2 Content">
▾ <Suspense name="three">
<Component key="Suspense 3 Content">
<Component key="Unrelated at End">
[shell]
<Suspense name="parent" rects={[{x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}]}>
<Suspense name="one" rects={[{x:1,y:2,width:5,height:1}]}>
<Suspense name="two" rects={[{x:1,y:2,width:5,height:1}]}>
<Suspense name="three" rects={[{x:1,y:2,width:5,height:1}]}>
`);
});
it('should display a partially rendered SuspenseList', async () => {

View File

@@ -7519,9 +7519,6 @@ export function attach(
}
// TODO: Allow overriding the timeline for the specified root.
forceFallbackForFibers.forEach(fiber => {
scheduleUpdate(fiber);
});
forceFallbackForFibers.clear();
for (let i = 0; i < suspendedSet.length; ++i) {

View File

@@ -23,7 +23,7 @@ type Props = {
symbolicatedSourcePromise: Promise<SourceMappedLocation | null>,
};
function OpenSymbolicatedSourceInEditorButton({
function OpenInEditorButton({
editorURL,
source,
symbolicatedSourcePromise,
@@ -45,17 +45,4 @@ function OpenSymbolicatedSourceInEditorButton({
);
}
function OpenInEditorButton(props: Props): React.Node {
return (
<React.Suspense
fallback={
<Button disabled={true} title="retrieving original source…">
<ButtonIcon type="editor" />
</Button>
}>
<OpenSymbolicatedSourceInEditorButton {...props} />
</React.Suspense>
);
}
export default OpenInEditorButton;

View File

@@ -45,9 +45,10 @@ function getSuspendableDocumentOrderSuspense(
if (current === undefined) {
continue;
}
// Include the root even if we won't suspend it.
// You should be able to see what suspended the shell.
suspenseTreeList.push(current);
// Don't include the root. It's currently not supported to suspend the shell.
if (current !== suspense) {
suspenseTreeList.push(current);
}
// Add children in reverse order to maintain document order
for (let j = current.children.length - 1; j >= 0; j--) {
const childSuspense = store.getSuspenseByID(current.children[j]);
@@ -125,27 +126,25 @@ function SuspenseTimelineInput({rootID}: {rootID: Element['id'] | void}) {
}
function handleChange(event: SyntheticEvent) {
if (rootID === undefined) {
return;
}
const rendererID = store.getRendererIDForElement(rootID);
if (rendererID === null) {
console.error(
`No renderer ID found for root element ${rootID} in suspense timeline.`,
);
return;
}
const pendingValue = +event.currentTarget.value;
const suspendedSet = timeline
.slice(pendingValue)
.map(suspense => suspense.id);
bridge.send('overrideSuspenseMilestone', {
rendererID,
rootID,
suspendedSet,
});
for (let i = 0; i < timeline.length; i++) {
const forceFallback = i > pendingValue;
const suspense = timeline[i];
const elementID = suspense.id;
const rendererID = store.getRendererIDForElement(elementID);
if (rendererID === null) {
// TODO: Handle disconnected elements.
console.warn(
`No renderer ID found for element ${elementID} in suspense timeline.`,
);
} else {
bridge.send('overrideSuspense', {
id: elementID,
rendererID,
forceFallback,
});
}
}
const suspense = timeline[pendingValue];
const elementID = suspense.id;

View File

@@ -126,7 +126,6 @@ function wwwOnCaughtError(
defaultOnCaughtError(error, errorInfo);
}
const noopOnDefaultTransitionIndicator = noop;
export function createRoot(
container: Element | Document | DocumentFragment,
@@ -138,7 +137,6 @@ export function createRoot(
({
onUncaughtError: wwwOnUncaughtError,
onCaughtError: wwwOnCaughtError,
onDefaultTransitionIndicator: noopOnDefaultTransitionIndicator,
}: any),
options,
),
@@ -157,7 +155,6 @@ export function hydrateRoot(
({
onUncaughtError: wwwOnUncaughtError,
onCaughtError: wwwOnCaughtError,
onDefaultTransitionIndicator: noopOnDefaultTransitionIndicator,
}: any),
options,
),
@@ -214,6 +211,7 @@ function getReactRootElementInContainer(container: any) {
// This isn't reachable because onRecoverableError isn't called in the
// legacy API.
const noopOnRecoverableError = noop;
const noopOnDefaultTransitionIndicator = noop;
function legacyCreateRootFromDOMContainer(
container: Container,

View File

@@ -46,10 +46,7 @@ import {
createPublicRootInstance,
type PublicRootInstance,
} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface';
import {
disableLegacyMode,
enableDefaultTransitionIndicator,
} from 'shared/ReactFeatureFlags';
import {disableLegacyMode} from 'shared/ReactFeatureFlags';
if (typeof ReactFiberErrorDialog.showErrorDialog !== 'function') {
throw new Error(
@@ -135,12 +132,6 @@ function render(
if (options && options.onRecoverableError !== undefined) {
onRecoverableError = options.onRecoverableError;
}
let onDefaultTransitionIndicator = nativeOnDefaultTransitionIndicator;
if (enableDefaultTransitionIndicator) {
if (options && options.onDefaultTransitionIndicator !== undefined) {
onDefaultTransitionIndicator = options.onDefaultTransitionIndicator;
}
}
const publicRootInstance = createPublicRootInstance(containerTag);
const rootInstance = {
@@ -160,7 +151,7 @@ function render(
onUncaughtError,
onCaughtError,
onRecoverableError,
onDefaultTransitionIndicator,
nativeOnDefaultTransitionIndicator,
null,
);

View File

@@ -134,7 +134,6 @@ export type RenderRootOptions = {
error: mixed,
errorInfo: {+componentStack?: ?string},
) => void,
onDefaultTransitionIndicator?: () => void | (() => void),
};
/**

View File

@@ -73,7 +73,6 @@ import {
includesSomeLane,
isGestureRender,
GestureLane,
UpdateLanes,
} from './ReactFiberLane';
import {
ContinuousEventPriority,
@@ -2984,20 +2983,6 @@ function rerenderDeferredValue<T>(value: T, initialValue?: T): T {
}
}
function isRenderingDeferredWork(): boolean {
if (!includesSomeLane(renderLanes, DeferredLane)) {
// None of the render lanes are deferred lanes.
return false;
}
// At least one of the render lanes are deferred lanes. However, if the
// current render is also batched together with an update, then we can't
// say that the render is wholly the result of deferred work. We can check
// this by checking if the root render lanes contain any "update" lanes, i.e.
// lanes that are only assigned to updates, like setState.
const rootRenderLanes = getWorkInProgressRootRenderLanes();
return !includesSomeLane(rootRenderLanes, UpdateLanes);
}
function mountDeferredValueImpl<T>(hook: Hook, value: T, initialValue?: T): T {
if (
// When `initialValue` is provided, we defer the initial render even if the
@@ -3006,7 +2991,7 @@ function mountDeferredValueImpl<T>(hook: Hook, value: T, initialValue?: T): T {
// However, to avoid waterfalls, we do not defer if this render
// was itself spawned by an earlier useDeferredValue. Check if DeferredLane
// is part of the render lanes.
!isRenderingDeferredWork()
!includesSomeLane(renderLanes, DeferredLane)
) {
// Render with the initial value
hook.memoizedState = initialValue;
@@ -3053,7 +3038,8 @@ function updateDeferredValueImpl<T>(
}
const shouldDeferValue =
!includesOnlyNonUrgentLanes(renderLanes) && !isRenderingDeferredWork();
!includesOnlyNonUrgentLanes(renderLanes) &&
!includesSomeLane(renderLanes, DeferredLane);
if (shouldDeferValue) {
// This is an urgent update. Since the value has changed, keep using the
// previous value and spawn a deferred render to update it later.

View File

@@ -73,20 +73,6 @@ const TransitionLane12: Lane = /* */ 0b0000000000010000000
const TransitionLane13: Lane = /* */ 0b0000000000100000000000000000000;
const TransitionLane14: Lane = /* */ 0b0000000001000000000000000000000;
const TransitionUpdateLanes =
TransitionLane1 |
TransitionLane2 |
TransitionLane3 |
TransitionLane4 |
TransitionLane5 |
TransitionLane6 |
TransitionLane7 |
TransitionLane8 |
TransitionLane9 |
TransitionLane10;
const TransitionDeferredLanes =
TransitionLane11 | TransitionLane12 | TransitionLane13 | TransitionLane14;
const RetryLanes: Lanes = /* */ 0b0000011110000000000000000000000;
const RetryLane1: Lane = /* */ 0b0000000010000000000000000000000;
const RetryLane2: Lane = /* */ 0b0000000100000000000000000000000;
@@ -108,7 +94,7 @@ export const DeferredLane: Lane = /* */ 0b1000000000000000000
// Any lane that might schedule an update. This is used to detect infinite
// update loops, so it doesn't include hydration lanes or retries.
export const UpdateLanes: Lanes =
SyncLane | InputContinuousLane | DefaultLane | TransitionUpdateLanes;
SyncLane | InputContinuousLane | DefaultLane | TransitionLanes;
export const HydrationLanes =
SyncHydrationLane |
@@ -169,8 +155,7 @@ export function getLabelForLane(lane: Lane): string | void {
export const NoTimestamp = -1;
let nextTransitionUpdateLane: Lane = TransitionLane1;
let nextTransitionDeferredLane: Lane = TransitionLane11;
let nextTransitionLane: Lane = TransitionLane1;
let nextRetryLane: Lane = RetryLane1;
function getHighestPriorityLanes(lanes: Lanes | Lane): Lanes {
@@ -205,12 +190,11 @@ function getHighestPriorityLanes(lanes: Lanes | Lane): Lanes {
case TransitionLane8:
case TransitionLane9:
case TransitionLane10:
return lanes & TransitionUpdateLanes;
case TransitionLane11:
case TransitionLane12:
case TransitionLane13:
case TransitionLane14:
return lanes & TransitionDeferredLanes;
return lanes & TransitionLanes;
case RetryLane1:
case RetryLane2:
case RetryLane3:
@@ -695,23 +679,14 @@ export function isGestureRender(lanes: Lanes): boolean {
return lanes === GestureLane;
}
export function claimNextTransitionUpdateLane(): Lane {
export function claimNextTransitionLane(): Lane {
// Cycle through the lanes, assigning each new transition to the next lane.
// In most cases, this means every transition gets its own lane, until we
// run out of lanes and cycle back to the beginning.
const lane = nextTransitionUpdateLane;
nextTransitionUpdateLane <<= 1;
if ((nextTransitionUpdateLane & TransitionUpdateLanes) === NoLanes) {
nextTransitionUpdateLane = TransitionLane1;
}
return lane;
}
export function claimNextTransitionDeferredLane(): Lane {
const lane = nextTransitionDeferredLane;
nextTransitionDeferredLane <<= 1;
if ((nextTransitionDeferredLane & TransitionDeferredLanes) === NoLanes) {
nextTransitionDeferredLane = TransitionLane11;
const lane = nextTransitionLane;
nextTransitionLane <<= 1;
if ((nextTransitionLane & TransitionLanes) === NoLanes) {
nextTransitionLane = TransitionLane1;
}
return lane;
}
@@ -977,14 +952,6 @@ function markSpawnedDeferredLane(
// Entangle the spawned lane with the DeferredLane bit so that we know it
// was the result of another render. This lets us avoid a useDeferredValue
// waterfall — only the first level will defer.
// TODO: Now that there is a reserved set of transition lanes that are used
// exclusively for deferred work, we should get rid of this special
// DeferredLane bit; the same information can be inferred by checking whether
// the lane is one of the TransitionDeferredLanes. The only reason this still
// exists is because we need to also do the same for OffscreenLane. That
// requires additional changes because there are more places around the
// codebase that treat OffscreenLane as a magic value; would need to check
// for a new OffscreenDeferredLane, too. Will leave this for a follow-up.
const spawnedLaneIndex = laneToIndex(spawnedLane);
root.entangledLanes |= spawnedLane;
root.entanglements[spawnedLaneIndex] |=

View File

@@ -228,20 +228,9 @@ export function logComponentRender(
? 'tertiary-dark'
: 'primary-dark'
: 'error';
if (!__DEV__) {
console.timeStamp(
name,
startTime,
endTime,
COMPONENTS_TRACK,
undefined,
color,
);
} else {
const debugTask = fiber._debugTask;
if (__DEV__ && debugTask) {
const props = fiber.memoizedProps;
const debugTask = fiber._debugTask;
if (
props !== null &&
alternate !== null &&
@@ -279,45 +268,38 @@ export function logComponentRender(
reusableComponentDevToolDetails.properties = properties;
reusableComponentOptions.start = startTime;
reusableComponentOptions.end = endTime;
if (debugTask != null) {
debugTask.run(
// $FlowFixMe[method-unbinding]
performance.measure.bind(
performance,
'\u200b' + name,
reusableComponentOptions,
),
);
} else {
performance.measure('\u200b' + name, reusableComponentOptions);
}
}
} else {
if (debugTask != null) {
debugTask.run(
// $FlowFixMe[method-unbinding]
console.timeStamp.bind(
console,
name,
startTime,
endTime,
COMPONENTS_TRACK,
undefined,
color,
performance.measure.bind(
performance,
'\u200b' + name,
reusableComponentOptions,
),
);
} else {
console.timeStamp(
name,
startTime,
endTime,
COMPONENTS_TRACK,
undefined,
color,
);
return;
}
}
debugTask.run(
// $FlowFixMe[method-unbinding]
console.timeStamp.bind(
console,
name,
startTime,
endTime,
COMPONENTS_TRACK,
undefined,
color,
),
);
} else {
console.timeStamp(
name,
startTime,
endTime,
COMPONENTS_TRACK,
undefined,
color,
);
}
}
}

View File

@@ -31,7 +31,7 @@ import {
getNextLanes,
includesSyncLane,
markStarvedLanesAsExpired,
claimNextTransitionUpdateLane,
claimNextTransitionLane,
getNextLanesToFlushSync,
checkIfRootIsPrerendering,
isGestureRender,
@@ -716,7 +716,7 @@ export function requestTransitionLane(
: // We may or may not be inside an async action scope. If we are, this
// is the first update in that scope. Either way, we need to get a
// fresh transition lane.
claimNextTransitionUpdateLane();
claimNextTransitionLane();
}
return currentEventTransitionLane;
}

View File

@@ -192,7 +192,7 @@ import {
OffscreenLane,
SyncUpdateLanes,
UpdateLanes,
claimNextTransitionDeferredLane,
claimNextTransitionLane,
checkIfRootIsPrerendering,
includesOnlyViewTransitionEligibleLanes,
isGestureRender,
@@ -827,7 +827,7 @@ export function requestDeferredLane(): Lane {
workInProgressDeferredLane = OffscreenLane;
} else {
// Everything else is spawned as a transition.
workInProgressDeferredLane = claimNextTransitionDeferredLane();
workInProgressDeferredLane = claimNextTransitionLane();
}
}

View File

@@ -608,48 +608,6 @@ describe('ReactDeferredValue', () => {
},
);
it(
"regression: useDeferredValue's initial value argument works even if an unrelated " +
'transition is suspended',
async () => {
// Simulates a previous bug where a new useDeferredValue hook is mounted
// while some unrelated transition is suspended. In the regression case,
// the initial values was skipped/ignored.
function Content({text}) {
return (
<AsyncText text={useDeferredValue(text, `Preview ${text}...`)} />
);
}
function App({text}) {
// Use a key to force a new Content instance to be mounted each time
// the text changes.
return <Content key={text} text={text} />;
}
const root = ReactNoop.createRoot();
// Render a previous UI using useDeferredValue. Suspend on the
// final value.
resolveText('Preview A...');
await act(() => startTransition(() => root.render(<App text="A" />)));
assertLog(['Preview A...', 'Suspend! [A]']);
// While it's still suspended, update the UI to show a different screen
// with a different preview value. We should be able to show the new
// preview even though the previous transition never finished.
resolveText('Preview B...');
await act(() => startTransition(() => root.render(<App text="B" />)));
assertLog(['Preview B...', 'Suspend! [B]']);
// Now finish loading the final value.
await act(() => resolveText('B'));
assertLog(['B']);
expect(root).toMatchRenderedOutput('B');
},
);
it('avoids a useDeferredValue waterfall when separated by a Suspense boundary', async () => {
// Same as the previous test but with a Suspense boundary separating the
// two useDeferredValue hooks.

View File

@@ -79,7 +79,7 @@ export const enableSuspenseyImages: boolean = false;
export const enableFizzBlockingRender: boolean = true;
export const enableSrcObject: boolean = false;
export const enableHydrationChangeEvent: boolean = true;
export const enableDefaultTransitionIndicator: boolean = true;
export const enableDefaultTransitionIndicator: boolean = false;
export const ownerStackLimit = 1e4;
export const enableComponentPerformanceTrack: boolean =
__PROFILE__ && dynamicFlags.enableComponentPerformanceTrack;

View File

@@ -66,7 +66,7 @@ export const enableSuspenseyImages = false;
export const enableFizzBlockingRender = true;
export const enableSrcObject = false;
export const enableHydrationChangeEvent = false;
export const enableDefaultTransitionIndicator = true;
export const enableDefaultTransitionIndicator = false;
export const enableFragmentRefs = false;
export const enableFragmentRefsScrollIntoView = false;
export const ownerStackLimit = 1e4;

View File

@@ -79,7 +79,7 @@ export const enableSuspenseyImages: boolean = false;
export const enableFizzBlockingRender: boolean = true;
export const enableSrcObject: boolean = false;
export const enableHydrationChangeEvent: boolean = false;
export const enableDefaultTransitionIndicator: boolean = true;
export const enableDefaultTransitionIndicator: boolean = false;
export const enableFragmentRefs: boolean = false;
export const enableFragmentRefsScrollIntoView: boolean = false;

View File

@@ -109,7 +109,7 @@ export const enableSuspenseyImages: boolean = false;
export const enableFizzBlockingRender: boolean = true;
export const enableSrcObject: boolean = false;
export const enableHydrationChangeEvent: boolean = false;
export const enableDefaultTransitionIndicator: boolean = true;
export const enableDefaultTransitionIndicator: boolean = false;
export const ownerStackLimit = 1e4;