Compare commits
44 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
107d1983fa | ||
|
|
19b769fa5f | ||
|
|
dbf2538355 | ||
|
|
21f282425c | ||
|
|
257b033fc7 | ||
|
|
de97ef9ad5 | ||
|
|
93fc57400b | ||
|
|
093b3246e1 | ||
|
|
3a495ae722 | ||
|
|
bbe3f4d322 | ||
|
|
1ea46df8ba | ||
|
|
8c15edd57c | ||
|
|
5e94655cbb | ||
|
|
db8273c12f | ||
|
|
04ee54cd12 | ||
|
|
100fc4a8cf | ||
|
|
92ac4e8b80 | ||
|
|
f76c3617e0 | ||
|
|
7296120396 | ||
|
|
6347c6d373 | ||
|
|
01fb328632 | ||
|
|
ce4054ebdd | ||
|
|
21c1d51acb | ||
|
|
be48396dbd | ||
|
|
5268492536 | ||
|
|
c83be7da9f | ||
|
|
6362b5c711 | ||
|
|
5a9921b839 | ||
|
|
717e70843e | ||
|
|
a10ff9c857 | ||
|
|
fa50caf5f8 | ||
|
|
1e986f514f | ||
|
|
38bdda1ca6 | ||
|
|
a44e750e87 | ||
|
|
37b089a59c | ||
|
|
1a31a814f1 | ||
|
|
5a2205ba28 | ||
|
|
fa767dade6 | ||
|
|
0ba2f01f74 | ||
|
|
dd048c3b2d | ||
|
|
c308cb5905 | ||
|
|
986323f8c6 | ||
|
|
8f8b336734 | ||
|
|
d000261eef |
@@ -11,7 +11,7 @@ body:
|
||||
options:
|
||||
- label: React Compiler core (the JS output is incorrect, or your app works incorrectly after optimization)
|
||||
- label: babel-plugin-react-compiler (build issue installing or using the Babel plugin)
|
||||
- label: eslint-plugin-react-compiler (build issue installing or using the eslint plugin)
|
||||
- label: eslint-plugin-react-hooks (build issue installing or using the eslint plugin)
|
||||
- label: react-compiler-healthcheck (build issue installing or using the healthcheck script)
|
||||
- type: input
|
||||
attributes:
|
||||
|
||||
@@ -105,6 +105,7 @@ import {inferMutationAliasingRanges} from '../Inference/InferMutationAliasingRan
|
||||
import {validateNoDerivedComputationsInEffects} from '../Validation/ValidateNoDerivedComputationsInEffects';
|
||||
import {validateNoDerivedComputationsInEffects_exp} from '../Validation/ValidateNoDerivedComputationsInEffects_exp';
|
||||
import {nameAnonymousFunctions} from '../Transform/NameAnonymousFunctions';
|
||||
import {validateSourceLocations} from '../Validation/ValidateSourceLocations';
|
||||
|
||||
export type CompilerPipelineValue =
|
||||
| {kind: 'ast'; name: string; value: CodegenFunction}
|
||||
@@ -272,12 +273,10 @@ function runWithEnvironment(
|
||||
validateNoSetStateInRender(hir).unwrap();
|
||||
}
|
||||
|
||||
if (env.config.validateNoDerivedComputationsInEffects) {
|
||||
validateNoDerivedComputationsInEffects(hir);
|
||||
}
|
||||
|
||||
if (env.config.validateNoDerivedComputationsInEffects_exp) {
|
||||
validateNoDerivedComputationsInEffects_exp(hir);
|
||||
env.logErrors(validateNoDerivedComputationsInEffects_exp(hir));
|
||||
} else if (env.config.validateNoDerivedComputationsInEffects) {
|
||||
validateNoDerivedComputationsInEffects(hir);
|
||||
}
|
||||
|
||||
if (env.config.validateNoSetStateInEffects) {
|
||||
@@ -559,6 +558,10 @@ function runWithEnvironment(
|
||||
log({kind: 'ast', name: 'Codegen (outlined)', value: outlined.fn});
|
||||
}
|
||||
|
||||
if (env.config.validateSourceLocations) {
|
||||
validateSourceLocations(func, ast).unwrap();
|
||||
}
|
||||
|
||||
/**
|
||||
* This flag should be only set for unit / fixture tests to check
|
||||
* that Forget correctly handles unexpected errors (e.g. exceptions
|
||||
|
||||
@@ -364,6 +364,13 @@ export const EnvironmentConfigSchema = z.object({
|
||||
validateNoCapitalizedCalls: z.nullable(z.array(z.string())).default(null),
|
||||
validateBlocklistedImports: z.nullable(z.array(z.string())).default(null),
|
||||
|
||||
/**
|
||||
* Validates that AST nodes generated during codegen have proper source locations.
|
||||
* This is useful for debugging issues with source maps and Istanbul coverage.
|
||||
* When enabled, the compiler will error if important source locations are missing in the generated AST.
|
||||
*/
|
||||
validateSourceLocations: z.boolean().default(false),
|
||||
|
||||
/**
|
||||
* Validate against impure functions called during render
|
||||
*/
|
||||
@@ -670,6 +677,15 @@ export const EnvironmentConfigSchema = z.object({
|
||||
* from refs need to be stored in state during mount.
|
||||
*/
|
||||
enableAllowSetStateFromRefsInEffects: z.boolean().default(true),
|
||||
|
||||
/**
|
||||
* Enables inference of event handler types for JSX props on built-in DOM elements.
|
||||
* When enabled, functions passed to event handler props (props starting with "on")
|
||||
* on primitive JSX tags are inferred to have the BuiltinEventHandlerId type, which
|
||||
* allows ref access within those functions since DOM event handlers are guaranteed
|
||||
* by React to only execute in response to events, not during render.
|
||||
*/
|
||||
enableInferEventHandlers: z.boolean().default(false),
|
||||
});
|
||||
|
||||
export type EnvironmentConfig = z.infer<typeof EnvironmentConfigSchema>;
|
||||
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
BuiltInUseTransitionId,
|
||||
BuiltInWeakMapId,
|
||||
BuiltInWeakSetId,
|
||||
BuiltinEffectEventId,
|
||||
BuiltInEffectEventId,
|
||||
ReanimatedSharedValueId,
|
||||
ShapeRegistry,
|
||||
addFunction,
|
||||
@@ -863,7 +863,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
|
||||
returnType: {
|
||||
kind: 'Function',
|
||||
return: {kind: 'Poly'},
|
||||
shapeId: BuiltinEffectEventId,
|
||||
shapeId: BuiltInEffectEventId,
|
||||
isConstructor: false,
|
||||
},
|
||||
calleeEffect: Effect.Read,
|
||||
|
||||
@@ -403,8 +403,9 @@ export const BuiltInStartTransitionId = 'BuiltInStartTransition';
|
||||
export const BuiltInFireId = 'BuiltInFire';
|
||||
export const BuiltInFireFunctionId = 'BuiltInFireFunction';
|
||||
export const BuiltInUseEffectEventId = 'BuiltInUseEffectEvent';
|
||||
export const BuiltinEffectEventId = 'BuiltInEffectEventFunction';
|
||||
export const BuiltInEffectEventId = 'BuiltInEffectEventFunction';
|
||||
export const BuiltInAutodepsId = 'BuiltInAutoDepsId';
|
||||
export const BuiltInEventHandlerId = 'BuiltInEventHandlerId';
|
||||
|
||||
// See getReanimatedModuleType() in Globals.ts — this is part of supporting Reanimated's ref-like types
|
||||
export const ReanimatedSharedValueId = 'ReanimatedSharedValueId';
|
||||
@@ -1243,7 +1244,20 @@ addFunction(
|
||||
calleeEffect: Effect.ConditionallyMutate,
|
||||
returnValueKind: ValueKind.Mutable,
|
||||
},
|
||||
BuiltinEffectEventId,
|
||||
BuiltInEffectEventId,
|
||||
);
|
||||
|
||||
addFunction(
|
||||
BUILTIN_SHAPES,
|
||||
[],
|
||||
{
|
||||
positionalParams: [],
|
||||
restParam: Effect.ConditionallyMutate,
|
||||
returnType: {kind: 'Poly'},
|
||||
calleeEffect: Effect.ConditionallyMutate,
|
||||
returnValueKind: ValueKind.Mutable,
|
||||
},
|
||||
BuiltInEventHandlerId,
|
||||
);
|
||||
|
||||
/**
|
||||
|
||||
@@ -954,6 +954,7 @@ function applyEffect(
|
||||
case ValueKind.Primitive: {
|
||||
break;
|
||||
}
|
||||
case ValueKind.MaybeFrozen:
|
||||
case ValueKind.Frozen: {
|
||||
sourceType = 'frozen';
|
||||
break;
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
} from '../HIR/HIR';
|
||||
import {
|
||||
BuiltInArrayId,
|
||||
BuiltInEventHandlerId,
|
||||
BuiltInFunctionId,
|
||||
BuiltInJsxId,
|
||||
BuiltInMixedReadonlyId,
|
||||
@@ -471,6 +472,41 @@ function* generateInstructionTypes(
|
||||
}
|
||||
}
|
||||
}
|
||||
if (env.config.enableInferEventHandlers) {
|
||||
if (
|
||||
value.kind === 'JsxExpression' &&
|
||||
value.tag.kind === 'BuiltinTag' &&
|
||||
!value.tag.name.includes('-')
|
||||
) {
|
||||
/*
|
||||
* Infer event handler types for built-in DOM elements.
|
||||
* Props starting with "on" (e.g., onClick, onSubmit) on primitive tags
|
||||
* are inferred as event handlers. This allows functions with ref access
|
||||
* to be passed to these props, since DOM event handlers are guaranteed
|
||||
* by React to only execute in response to events, never during render.
|
||||
*
|
||||
* We exclude tags with hyphens to avoid web components (custom elements),
|
||||
* which are required by the HTML spec to contain a hyphen. Web components
|
||||
* may call event handler props during their lifecycle methods (e.g.,
|
||||
* connectedCallback), which would be unsafe for ref access.
|
||||
*/
|
||||
for (const prop of value.props) {
|
||||
if (
|
||||
prop.kind === 'JsxAttribute' &&
|
||||
prop.name.startsWith('on') &&
|
||||
prop.name.length > 2 &&
|
||||
prop.name[2] === prop.name[2].toUpperCase()
|
||||
) {
|
||||
yield equation(prop.place.identifier.type, {
|
||||
kind: 'Function',
|
||||
shapeId: BuiltInEventHandlerId,
|
||||
return: makeType(),
|
||||
isConstructor: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
yield equation(left, {kind: 'Object', shapeId: BuiltInJsxId});
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {Result} from '../Utils/Result';
|
||||
import {CompilerDiagnostic, CompilerError, Effect} from '..';
|
||||
import {ErrorCategory} from '../CompilerError';
|
||||
import {
|
||||
@@ -20,8 +21,8 @@ import {
|
||||
isUseStateType,
|
||||
BasicBlock,
|
||||
isUseRefType,
|
||||
GeneratedSource,
|
||||
SourceLocation,
|
||||
ArrayExpression,
|
||||
} from '../HIR';
|
||||
import {eachInstructionLValue, eachInstructionOperand} from '../HIR/visitors';
|
||||
import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables';
|
||||
@@ -33,20 +34,65 @@ type DerivationMetadata = {
|
||||
typeOfValue: TypeOfValue;
|
||||
place: Place;
|
||||
sourcesIds: Set<IdentifierId>;
|
||||
isStateSource: boolean;
|
||||
};
|
||||
|
||||
type EffectMetadata = {
|
||||
effect: HIRFunction;
|
||||
dependencies: ArrayExpression;
|
||||
};
|
||||
|
||||
type ValidationContext = {
|
||||
readonly functions: Map<IdentifierId, FunctionExpression>;
|
||||
readonly candidateDependencies: Map<IdentifierId, ArrayExpression>;
|
||||
readonly errors: CompilerError;
|
||||
readonly derivationCache: DerivationCache;
|
||||
readonly effects: Set<HIRFunction>;
|
||||
readonly setStateCache: Map<string | undefined | null, Array<Place>>;
|
||||
readonly effectSetStateCache: Map<string | undefined | null, Array<Place>>;
|
||||
readonly effectsCache: Map<IdentifierId, EffectMetadata>;
|
||||
readonly setStateLoads: Map<IdentifierId, IdentifierId | null>;
|
||||
readonly setStateUsages: Map<IdentifierId, Set<SourceLocation>>;
|
||||
};
|
||||
|
||||
class DerivationCache {
|
||||
hasChanges: boolean = false;
|
||||
cache: Map<IdentifierId, DerivationMetadata> = new Map();
|
||||
private previousCache: Map<IdentifierId, DerivationMetadata> | null = null;
|
||||
|
||||
takeSnapshot(): void {
|
||||
this.previousCache = new Map();
|
||||
for (const [key, value] of this.cache.entries()) {
|
||||
this.previousCache.set(key, {
|
||||
place: value.place,
|
||||
sourcesIds: new Set(value.sourcesIds),
|
||||
typeOfValue: value.typeOfValue,
|
||||
isStateSource: value.isStateSource,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
checkForChanges(): void {
|
||||
if (this.previousCache === null) {
|
||||
this.hasChanges = true;
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [key, value] of this.cache.entries()) {
|
||||
const previousValue = this.previousCache.get(key);
|
||||
if (
|
||||
previousValue === undefined ||
|
||||
!this.isDerivationEqual(previousValue, value)
|
||||
) {
|
||||
this.hasChanges = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.cache.size !== this.previousCache.size) {
|
||||
this.hasChanges = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this.hasChanges = false;
|
||||
}
|
||||
|
||||
snapshot(): boolean {
|
||||
const hasChanges = this.hasChanges;
|
||||
@@ -58,48 +104,28 @@ class DerivationCache {
|
||||
derivedVar: Place,
|
||||
sourcesIds: Set<IdentifierId>,
|
||||
typeOfValue: TypeOfValue,
|
||||
isStateSource: boolean,
|
||||
): void {
|
||||
let newValue: DerivationMetadata = {
|
||||
place: derivedVar,
|
||||
sourcesIds: new Set(),
|
||||
typeOfValue: typeOfValue ?? 'ignored',
|
||||
};
|
||||
|
||||
if (sourcesIds !== undefined) {
|
||||
for (const id of sourcesIds) {
|
||||
const sourcePlace = this.cache.get(id)?.place;
|
||||
|
||||
if (sourcePlace === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/*
|
||||
* If the identifier of the source is a promoted identifier, then
|
||||
* we should set the target as the source.
|
||||
*/
|
||||
let finalIsSource = isStateSource;
|
||||
if (!finalIsSource) {
|
||||
for (const sourceId of sourcesIds) {
|
||||
const sourceMetadata = this.cache.get(sourceId);
|
||||
if (
|
||||
sourcePlace.identifier.name === null ||
|
||||
sourcePlace.identifier.name?.kind === 'promoted'
|
||||
sourceMetadata?.isStateSource &&
|
||||
sourceMetadata.place.identifier.name?.kind !== 'named'
|
||||
) {
|
||||
newValue.sourcesIds.add(derivedVar.identifier.id);
|
||||
} else {
|
||||
newValue.sourcesIds.add(sourcePlace.identifier.id);
|
||||
finalIsSource = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (newValue.sourcesIds.size === 0) {
|
||||
newValue.sourcesIds.add(derivedVar.identifier.id);
|
||||
}
|
||||
|
||||
const existingValue = this.cache.get(derivedVar.identifier.id);
|
||||
if (
|
||||
existingValue === undefined ||
|
||||
!this.isDerivationEqual(existingValue, newValue)
|
||||
) {
|
||||
this.cache.set(derivedVar.identifier.id, newValue);
|
||||
this.hasChanges = true;
|
||||
}
|
||||
this.cache.set(derivedVar.identifier.id, {
|
||||
place: derivedVar,
|
||||
sourcesIds: sourcesIds,
|
||||
typeOfValue: typeOfValue ?? 'ignored',
|
||||
isStateSource: finalIsSource,
|
||||
});
|
||||
}
|
||||
|
||||
private isDerivationEqual(
|
||||
@@ -121,6 +147,14 @@ class DerivationCache {
|
||||
}
|
||||
}
|
||||
|
||||
function isNamedIdentifier(place: Place): place is Place & {
|
||||
identifier: {name: NonNullable<Place['identifier']['name']>};
|
||||
} {
|
||||
return (
|
||||
place.identifier.name !== null && place.identifier.name.kind === 'named'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that useEffect is not used for derived computations which could/should
|
||||
* be performed in render.
|
||||
@@ -146,25 +180,24 @@ class DerivationCache {
|
||||
*/
|
||||
export function validateNoDerivedComputationsInEffects_exp(
|
||||
fn: HIRFunction,
|
||||
): void {
|
||||
): Result<void, CompilerError> {
|
||||
const functions: Map<IdentifierId, FunctionExpression> = new Map();
|
||||
const candidateDependencies: Map<IdentifierId, ArrayExpression> = new Map();
|
||||
const derivationCache = new DerivationCache();
|
||||
const errors = new CompilerError();
|
||||
const effects: Set<HIRFunction> = new Set();
|
||||
const effectsCache: Map<IdentifierId, EffectMetadata> = new Map();
|
||||
|
||||
const setStateCache: Map<string | undefined | null, Array<Place>> = new Map();
|
||||
const effectSetStateCache: Map<
|
||||
string | undefined | null,
|
||||
Array<Place>
|
||||
> = new Map();
|
||||
const setStateLoads: Map<IdentifierId, IdentifierId> = new Map();
|
||||
const setStateUsages: Map<IdentifierId, Set<SourceLocation>> = new Map();
|
||||
|
||||
const context: ValidationContext = {
|
||||
functions,
|
||||
candidateDependencies,
|
||||
errors,
|
||||
derivationCache,
|
||||
effects,
|
||||
setStateCache,
|
||||
effectSetStateCache,
|
||||
effectsCache,
|
||||
setStateLoads,
|
||||
setStateUsages,
|
||||
};
|
||||
|
||||
if (fn.fnType === 'Hook') {
|
||||
@@ -172,10 +205,10 @@ export function validateNoDerivedComputationsInEffects_exp(
|
||||
if (param.kind === 'Identifier') {
|
||||
context.derivationCache.cache.set(param.identifier.id, {
|
||||
place: param,
|
||||
sourcesIds: new Set([param.identifier.id]),
|
||||
sourcesIds: new Set(),
|
||||
typeOfValue: 'fromProps',
|
||||
isStateSource: true,
|
||||
});
|
||||
context.derivationCache.hasChanges = true;
|
||||
}
|
||||
}
|
||||
} else if (fn.fnType === 'Component') {
|
||||
@@ -183,15 +216,17 @@ export function validateNoDerivedComputationsInEffects_exp(
|
||||
if (props != null && props.kind === 'Identifier') {
|
||||
context.derivationCache.cache.set(props.identifier.id, {
|
||||
place: props,
|
||||
sourcesIds: new Set([props.identifier.id]),
|
||||
sourcesIds: new Set(),
|
||||
typeOfValue: 'fromProps',
|
||||
isStateSource: true,
|
||||
});
|
||||
context.derivationCache.hasChanges = true;
|
||||
}
|
||||
}
|
||||
|
||||
let isFirstPass = true;
|
||||
do {
|
||||
context.derivationCache.takeSnapshot();
|
||||
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
recordPhiDerivations(block, context);
|
||||
for (const instr of block.instructions) {
|
||||
@@ -199,16 +234,15 @@ export function validateNoDerivedComputationsInEffects_exp(
|
||||
}
|
||||
}
|
||||
|
||||
context.derivationCache.checkForChanges();
|
||||
isFirstPass = false;
|
||||
} while (context.derivationCache.snapshot());
|
||||
|
||||
for (const effect of effects) {
|
||||
validateEffect(effect, context);
|
||||
for (const [, effect] of effectsCache) {
|
||||
validateEffect(effect.effect, effect.dependencies, context);
|
||||
}
|
||||
|
||||
if (errors.hasAnyErrors()) {
|
||||
throw errors;
|
||||
}
|
||||
return errors.asResult();
|
||||
}
|
||||
|
||||
function recordPhiDerivations(
|
||||
@@ -236,6 +270,7 @@ function recordPhiDerivations(
|
||||
phi.place,
|
||||
sourcesIds,
|
||||
typeOfValue,
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -251,17 +286,69 @@ function joinValue(
|
||||
return 'fromPropsAndState';
|
||||
}
|
||||
|
||||
function getRootSetState(
|
||||
key: IdentifierId,
|
||||
loads: Map<IdentifierId, IdentifierId | null>,
|
||||
visited: Set<IdentifierId> = new Set(),
|
||||
): IdentifierId | null {
|
||||
if (visited.has(key)) {
|
||||
return null;
|
||||
}
|
||||
visited.add(key);
|
||||
|
||||
const parentId = loads.get(key);
|
||||
|
||||
if (parentId === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (parentId === null) {
|
||||
return key;
|
||||
}
|
||||
|
||||
return getRootSetState(parentId, loads, visited);
|
||||
}
|
||||
|
||||
function maybeRecordSetState(
|
||||
instr: Instruction,
|
||||
loads: Map<IdentifierId, IdentifierId | null>,
|
||||
usages: Map<IdentifierId, Set<SourceLocation>>,
|
||||
): void {
|
||||
for (const operand of eachInstructionLValue(instr)) {
|
||||
if (
|
||||
instr.value.kind === 'LoadLocal' &&
|
||||
loads.has(instr.value.place.identifier.id)
|
||||
) {
|
||||
loads.set(operand.identifier.id, instr.value.place.identifier.id);
|
||||
} else {
|
||||
if (isSetStateType(operand.identifier)) {
|
||||
// this is a root setState
|
||||
loads.set(operand.identifier.id, null);
|
||||
}
|
||||
}
|
||||
|
||||
const rootSetState = getRootSetState(operand.identifier.id, loads);
|
||||
if (rootSetState !== null && usages.get(rootSetState) === undefined) {
|
||||
usages.set(rootSetState, new Set([operand.loc]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function recordInstructionDerivations(
|
||||
instr: Instruction,
|
||||
context: ValidationContext,
|
||||
isFirstPass: boolean,
|
||||
): void {
|
||||
maybeRecordSetState(instr, context.setStateLoads, context.setStateUsages);
|
||||
|
||||
let typeOfValue: TypeOfValue = 'ignored';
|
||||
let isSource: boolean = false;
|
||||
const sources: Set<IdentifierId> = new Set();
|
||||
const {lvalue, value} = instr;
|
||||
if (value.kind === 'FunctionExpression') {
|
||||
context.functions.set(lvalue.identifier.id, value);
|
||||
for (const [, block] of value.loweredFunc.func.body.blocks) {
|
||||
recordPhiDerivations(block, context);
|
||||
for (const instr of block.instructions) {
|
||||
recordInstructionDerivations(instr, context, isFirstPass);
|
||||
}
|
||||
@@ -276,28 +363,37 @@ function recordInstructionDerivations(
|
||||
value.args[1].kind === 'Identifier'
|
||||
) {
|
||||
const effectFunction = context.functions.get(value.args[0].identifier.id);
|
||||
if (effectFunction != null) {
|
||||
context.effects.add(effectFunction.loweredFunc.func);
|
||||
const deps = context.candidateDependencies.get(
|
||||
value.args[1].identifier.id,
|
||||
);
|
||||
if (effectFunction != null && deps != null) {
|
||||
context.effectsCache.set(value.args[0].identifier.id, {
|
||||
effect: effectFunction.loweredFunc.func,
|
||||
dependencies: deps,
|
||||
});
|
||||
}
|
||||
} else if (isUseStateType(lvalue.identifier) && value.args.length > 0) {
|
||||
const stateValueSource = value.args[0];
|
||||
if (stateValueSource.kind === 'Identifier') {
|
||||
sources.add(stateValueSource.identifier.id);
|
||||
}
|
||||
typeOfValue = joinValue(typeOfValue, 'fromState');
|
||||
typeOfValue = 'fromState';
|
||||
context.derivationCache.addDerivationEntry(
|
||||
lvalue,
|
||||
new Set(),
|
||||
typeOfValue,
|
||||
true,
|
||||
);
|
||||
return;
|
||||
}
|
||||
} else if (value.kind === 'ArrayExpression') {
|
||||
context.candidateDependencies.set(lvalue.identifier.id, value);
|
||||
}
|
||||
|
||||
for (const operand of eachInstructionOperand(instr)) {
|
||||
if (
|
||||
isSetStateType(operand.identifier) &&
|
||||
operand.loc !== GeneratedSource &&
|
||||
isFirstPass
|
||||
) {
|
||||
if (context.setStateCache.has(operand.loc.identifierName)) {
|
||||
context.setStateCache.get(operand.loc.identifierName)!.push(operand);
|
||||
} else {
|
||||
context.setStateCache.set(operand.loc.identifierName, [operand]);
|
||||
if (context.setStateLoads.has(operand.identifier.id)) {
|
||||
const rootSetStateId = getRootSetState(
|
||||
operand.identifier.id,
|
||||
context.setStateLoads,
|
||||
);
|
||||
if (rootSetStateId !== null) {
|
||||
context.setStateUsages.get(rootSetStateId)?.add(operand.loc);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,9 +406,7 @@ function recordInstructionDerivations(
|
||||
}
|
||||
|
||||
typeOfValue = joinValue(typeOfValue, operandMetadata.typeOfValue);
|
||||
for (const id of operandMetadata.sourcesIds) {
|
||||
sources.add(id);
|
||||
}
|
||||
sources.add(operand.identifier.id);
|
||||
}
|
||||
|
||||
if (typeOfValue === 'ignored') {
|
||||
@@ -320,7 +414,12 @@ function recordInstructionDerivations(
|
||||
}
|
||||
|
||||
for (const lvalue of eachInstructionLValue(instr)) {
|
||||
context.derivationCache.addDerivationEntry(lvalue, sources, typeOfValue);
|
||||
context.derivationCache.addDerivationEntry(
|
||||
lvalue,
|
||||
sources,
|
||||
typeOfValue,
|
||||
isSource,
|
||||
);
|
||||
}
|
||||
|
||||
for (const operand of eachInstructionOperand(instr)) {
|
||||
@@ -331,11 +430,25 @@ function recordInstructionDerivations(
|
||||
case Effect.ConditionallyMutateIterator:
|
||||
case Effect.Mutate: {
|
||||
if (isMutable(instr, operand)) {
|
||||
context.derivationCache.addDerivationEntry(
|
||||
operand,
|
||||
sources,
|
||||
typeOfValue,
|
||||
);
|
||||
if (context.derivationCache.cache.has(operand.identifier.id)) {
|
||||
const operandMetadata = context.derivationCache.cache.get(
|
||||
operand.identifier.id,
|
||||
);
|
||||
|
||||
if (operandMetadata !== undefined) {
|
||||
operandMetadata.typeOfValue = joinValue(
|
||||
typeOfValue,
|
||||
operandMetadata.typeOfValue,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
context.derivationCache.addDerivationEntry(
|
||||
operand,
|
||||
sources,
|
||||
typeOfValue,
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -367,21 +480,183 @@ function recordInstructionDerivations(
|
||||
}
|
||||
}
|
||||
|
||||
type TreeNode = {
|
||||
name: string;
|
||||
typeOfValue: TypeOfValue;
|
||||
isSource: boolean;
|
||||
children: Array<TreeNode>;
|
||||
};
|
||||
|
||||
function buildTreeNode(
|
||||
sourceId: IdentifierId,
|
||||
context: ValidationContext,
|
||||
visited: Set<string> = new Set(),
|
||||
): Array<TreeNode> {
|
||||
const sourceMetadata = context.derivationCache.cache.get(sourceId);
|
||||
if (!sourceMetadata) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (sourceMetadata.isStateSource && isNamedIdentifier(sourceMetadata.place)) {
|
||||
return [
|
||||
{
|
||||
name: sourceMetadata.place.identifier.name.value,
|
||||
typeOfValue: sourceMetadata.typeOfValue,
|
||||
isSource: sourceMetadata.isStateSource,
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const children: Array<TreeNode> = [];
|
||||
|
||||
const namedSiblings: Set<string> = new Set();
|
||||
for (const childId of sourceMetadata.sourcesIds) {
|
||||
const childNodes = buildTreeNode(
|
||||
childId,
|
||||
context,
|
||||
new Set([
|
||||
...visited,
|
||||
...(isNamedIdentifier(sourceMetadata.place)
|
||||
? [sourceMetadata.place.identifier.name.value]
|
||||
: []),
|
||||
]),
|
||||
);
|
||||
if (childNodes) {
|
||||
for (const childNode of childNodes) {
|
||||
if (!namedSiblings.has(childNode.name)) {
|
||||
children.push(childNode);
|
||||
namedSiblings.add(childNode.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
isNamedIdentifier(sourceMetadata.place) &&
|
||||
!visited.has(sourceMetadata.place.identifier.name.value)
|
||||
) {
|
||||
return [
|
||||
{
|
||||
name: sourceMetadata.place.identifier.name.value,
|
||||
typeOfValue: sourceMetadata.typeOfValue,
|
||||
isSource: sourceMetadata.isStateSource,
|
||||
children: children,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
function renderTree(
|
||||
node: TreeNode,
|
||||
indent: string = '',
|
||||
isLast: boolean = true,
|
||||
propsSet: Set<string>,
|
||||
stateSet: Set<string>,
|
||||
): string {
|
||||
const prefix = indent + (isLast ? '└── ' : '├── ');
|
||||
const childIndent = indent + (isLast ? ' ' : '│ ');
|
||||
|
||||
let result = `${prefix}${node.name}`;
|
||||
|
||||
if (node.isSource) {
|
||||
let typeLabel: string;
|
||||
if (node.typeOfValue === 'fromProps') {
|
||||
propsSet.add(node.name);
|
||||
typeLabel = 'Prop';
|
||||
} else if (node.typeOfValue === 'fromState') {
|
||||
stateSet.add(node.name);
|
||||
typeLabel = 'State';
|
||||
} else {
|
||||
propsSet.add(node.name);
|
||||
stateSet.add(node.name);
|
||||
typeLabel = 'Prop and State';
|
||||
}
|
||||
result += ` (${typeLabel})`;
|
||||
}
|
||||
|
||||
if (node.children.length > 0) {
|
||||
result += '\n';
|
||||
node.children.forEach((child, index) => {
|
||||
const isLastChild = index === node.children.length - 1;
|
||||
result += renderTree(child, childIndent, isLastChild, propsSet, stateSet);
|
||||
if (index < node.children.length - 1) {
|
||||
result += '\n';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function getFnLocalDeps(
|
||||
fn: FunctionExpression | undefined,
|
||||
): Set<IdentifierId> | undefined {
|
||||
if (!fn) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const deps: Set<IdentifierId> = new Set();
|
||||
|
||||
for (const [, block] of fn.loweredFunc.func.body.blocks) {
|
||||
for (const instr of block.instructions) {
|
||||
if (instr.value.kind === 'LoadLocal') {
|
||||
deps.add(instr.value.place.identifier.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return deps;
|
||||
}
|
||||
|
||||
function validateEffect(
|
||||
effectFunction: HIRFunction,
|
||||
dependencies: ArrayExpression,
|
||||
context: ValidationContext,
|
||||
): void {
|
||||
const seenBlocks: Set<BlockId> = new Set();
|
||||
|
||||
const effectDerivedSetStateCalls: Array<{
|
||||
value: CallExpression;
|
||||
loc: SourceLocation;
|
||||
id: IdentifierId;
|
||||
sourceIds: Set<IdentifierId>;
|
||||
typeOfValue: TypeOfValue;
|
||||
}> = [];
|
||||
|
||||
const effectSetStateUsages: Map<
|
||||
IdentifierId,
|
||||
Set<SourceLocation>
|
||||
> = new Map();
|
||||
|
||||
// Consider setStates in the effect's dependency array as being part of effectSetStateUsages
|
||||
for (const dep of dependencies.elements) {
|
||||
if (dep.kind === 'Identifier') {
|
||||
const root = getRootSetState(dep.identifier.id, context.setStateLoads);
|
||||
if (root !== null) {
|
||||
effectSetStateUsages.set(root, new Set([dep.loc]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let cleanUpFunctionDeps: Set<IdentifierId> | undefined;
|
||||
|
||||
const globals: Set<IdentifierId> = new Set();
|
||||
for (const block of effectFunction.body.blocks.values()) {
|
||||
/*
|
||||
* if the block is in an effect and is of type return then its an effect's cleanup function
|
||||
* if the cleanup function depends on a value from which effect-set state is derived then
|
||||
* we can't validate
|
||||
*/
|
||||
if (
|
||||
block.terminal.kind === 'return' &&
|
||||
block.terminal.returnVariant === 'Explicit'
|
||||
) {
|
||||
cleanUpFunctionDeps = getFnLocalDeps(
|
||||
context.functions.get(block.terminal.value.identifier.id),
|
||||
);
|
||||
}
|
||||
for (const pred of block.preds) {
|
||||
if (!seenBlocks.has(pred)) {
|
||||
// skip if block has a back edge
|
||||
@@ -395,19 +670,16 @@ function validateEffect(
|
||||
return;
|
||||
}
|
||||
|
||||
maybeRecordSetState(instr, context.setStateLoads, effectSetStateUsages);
|
||||
|
||||
for (const operand of eachInstructionOperand(instr)) {
|
||||
if (
|
||||
isSetStateType(operand.identifier) &&
|
||||
operand.loc !== GeneratedSource
|
||||
) {
|
||||
if (context.effectSetStateCache.has(operand.loc.identifierName)) {
|
||||
context.effectSetStateCache
|
||||
.get(operand.loc.identifierName)!
|
||||
.push(operand);
|
||||
} else {
|
||||
context.effectSetStateCache.set(operand.loc.identifierName, [
|
||||
operand,
|
||||
]);
|
||||
if (context.setStateLoads.has(operand.identifier.id)) {
|
||||
const rootSetStateId = getRootSetState(
|
||||
operand.identifier.id,
|
||||
context.setStateLoads,
|
||||
);
|
||||
if (rootSetStateId !== null) {
|
||||
effectSetStateUsages.get(rootSetStateId)?.add(operand.loc);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -418,6 +690,18 @@ function validateEffect(
|
||||
instr.value.args.length === 1 &&
|
||||
instr.value.args[0].kind === 'Identifier'
|
||||
) {
|
||||
const calleeMetadata = context.derivationCache.cache.get(
|
||||
instr.value.callee.identifier.id,
|
||||
);
|
||||
|
||||
/*
|
||||
* If the setState comes from a source other than local state skip
|
||||
* since the fix is not to calculate in render
|
||||
*/
|
||||
if (calleeMetadata?.typeOfValue != 'fromState') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const argMetadata = context.derivationCache.cache.get(
|
||||
instr.value.args[0].identifier.id,
|
||||
);
|
||||
@@ -425,7 +709,7 @@ function validateEffect(
|
||||
if (argMetadata !== undefined) {
|
||||
effectDerivedSetStateCalls.push({
|
||||
value: instr.value,
|
||||
loc: instr.value.callee.loc,
|
||||
id: instr.value.callee.identifier.id,
|
||||
sourceIds: argMetadata.sourcesIds,
|
||||
typeOfValue: argMetadata.typeOfValue,
|
||||
});
|
||||
@@ -459,37 +743,74 @@ function validateEffect(
|
||||
}
|
||||
|
||||
for (const derivedSetStateCall of effectDerivedSetStateCalls) {
|
||||
const rootSetStateCall = getRootSetState(
|
||||
derivedSetStateCall.id,
|
||||
context.setStateLoads,
|
||||
);
|
||||
|
||||
if (
|
||||
derivedSetStateCall.loc !== GeneratedSource &&
|
||||
context.effectSetStateCache.has(derivedSetStateCall.loc.identifierName) &&
|
||||
context.setStateCache.has(derivedSetStateCall.loc.identifierName) &&
|
||||
context.effectSetStateCache.get(derivedSetStateCall.loc.identifierName)!
|
||||
.length ===
|
||||
context.setStateCache.get(derivedSetStateCall.loc.identifierName)!
|
||||
.length -
|
||||
1
|
||||
rootSetStateCall !== null &&
|
||||
effectSetStateUsages.has(rootSetStateCall) &&
|
||||
context.setStateUsages.has(rootSetStateCall) &&
|
||||
effectSetStateUsages.get(rootSetStateCall)!.size ===
|
||||
context.setStateUsages.get(rootSetStateCall)!.size - 1
|
||||
) {
|
||||
const derivedDepsStr = Array.from(derivedSetStateCall.sourceIds)
|
||||
.map(sourceId => {
|
||||
const sourceMetadata = context.derivationCache.cache.get(sourceId);
|
||||
return sourceMetadata?.place.identifier.name?.value;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join(', ');
|
||||
const propsSet = new Set<string>();
|
||||
const stateSet = new Set<string>();
|
||||
|
||||
let description;
|
||||
|
||||
if (derivedSetStateCall.typeOfValue === 'fromProps') {
|
||||
description = `From props: [${derivedDepsStr}]`;
|
||||
} else if (derivedSetStateCall.typeOfValue === 'fromState') {
|
||||
description = `From local state: [${derivedDepsStr}]`;
|
||||
} else {
|
||||
description = `From props and local state: [${derivedDepsStr}]`;
|
||||
const rootNodesMap = new Map<string, TreeNode>();
|
||||
for (const id of derivedSetStateCall.sourceIds) {
|
||||
const nodes = buildTreeNode(id, context);
|
||||
for (const node of nodes) {
|
||||
if (!rootNodesMap.has(node.name)) {
|
||||
rootNodesMap.set(node.name, node);
|
||||
}
|
||||
}
|
||||
}
|
||||
const rootNodes = Array.from(rootNodesMap.values());
|
||||
|
||||
const trees = rootNodes.map((node, index) =>
|
||||
renderTree(
|
||||
node,
|
||||
'',
|
||||
index === rootNodes.length - 1,
|
||||
propsSet,
|
||||
stateSet,
|
||||
),
|
||||
);
|
||||
|
||||
for (const dep of derivedSetStateCall.sourceIds) {
|
||||
if (cleanUpFunctionDeps !== undefined && cleanUpFunctionDeps.has(dep)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const propsArr = Array.from(propsSet);
|
||||
const stateArr = Array.from(stateSet);
|
||||
|
||||
let rootSources = '';
|
||||
if (propsArr.length > 0) {
|
||||
rootSources += `Props: [${propsArr.join(', ')}]`;
|
||||
}
|
||||
if (stateArr.length > 0) {
|
||||
if (rootSources) rootSources += '\n';
|
||||
rootSources += `State: [${stateArr.join(', ')}]`;
|
||||
}
|
||||
|
||||
const description = `Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user
|
||||
|
||||
This setState call is setting a derived value that depends on the following reactive sources:
|
||||
|
||||
${rootSources}
|
||||
|
||||
Data Flow Tree:
|
||||
${trees.join('\n')}
|
||||
|
||||
See: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state`;
|
||||
|
||||
context.errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
description: `Derived values (${description}) 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`,
|
||||
description: description,
|
||||
category: ErrorCategory.EffectDerivationsOfState,
|
||||
reason:
|
||||
'You might not need an effect. Derive values in render, not effects.',
|
||||
|
||||
@@ -14,12 +14,14 @@ import {
|
||||
BlockId,
|
||||
HIRFunction,
|
||||
IdentifierId,
|
||||
Identifier,
|
||||
Place,
|
||||
SourceLocation,
|
||||
getHookKindForType,
|
||||
isRefValueType,
|
||||
isUseRefType,
|
||||
} from '../HIR';
|
||||
import {BuiltInEventHandlerId} from '../HIR/ObjectShape';
|
||||
import {
|
||||
eachInstructionOperand,
|
||||
eachInstructionValueOperand,
|
||||
@@ -183,6 +185,11 @@ function refTypeOfType(place: Place): RefAccessType {
|
||||
}
|
||||
}
|
||||
|
||||
function isEventHandlerType(identifier: Identifier): boolean {
|
||||
const type = identifier.type;
|
||||
return type.kind === 'Function' && type.shapeId === BuiltInEventHandlerId;
|
||||
}
|
||||
|
||||
function tyEqual(a: RefAccessType, b: RefAccessType): boolean {
|
||||
if (a.kind !== b.kind) {
|
||||
return false;
|
||||
@@ -519,6 +526,9 @@ function validateNoRefAccessInRenderImpl(
|
||||
*/
|
||||
if (!didError) {
|
||||
const isRefLValue = isUseRefType(instr.lvalue.identifier);
|
||||
const isEventHandlerLValue = isEventHandlerType(
|
||||
instr.lvalue.identifier,
|
||||
);
|
||||
for (const operand of eachInstructionValueOperand(instr.value)) {
|
||||
/**
|
||||
* By default we check that function call operands are not refs,
|
||||
@@ -526,29 +536,16 @@ function validateNoRefAccessInRenderImpl(
|
||||
*/
|
||||
if (
|
||||
isRefLValue ||
|
||||
isEventHandlerLValue ||
|
||||
(hookKind != null &&
|
||||
hookKind !== 'useState' &&
|
||||
hookKind !== 'useReducer')
|
||||
) {
|
||||
/**
|
||||
* Special cases:
|
||||
*
|
||||
* 1. the lvalue is a ref
|
||||
* In general passing a ref to a function may access that ref
|
||||
* value during render, so we disallow it.
|
||||
*
|
||||
* The main exception is the "mergeRefs" pattern, ie a function
|
||||
* that accepts multiple refs as arguments (or an array of refs)
|
||||
* and returns a new, aggregated ref. If the lvalue is a ref,
|
||||
* we assume that the user is doing this pattern and allow passing
|
||||
* refs.
|
||||
*
|
||||
* Eg `const mergedRef = mergeRefs(ref1, ref2)`
|
||||
*
|
||||
* 2. calling hooks
|
||||
*
|
||||
* Hooks are independently checked to ensure they don't access refs
|
||||
* during render.
|
||||
* Allow passing refs or ref-accessing functions when:
|
||||
* 1. lvalue is a ref (mergeRefs pattern: `mergeRefs(ref1, ref2)`)
|
||||
* 2. lvalue is an event handler (DOM events execute outside render)
|
||||
* 3. calling hooks (independently validated for ref safety)
|
||||
*/
|
||||
validateNoDirectRefValueAccess(errors, operand, env);
|
||||
} else if (interpolatedAsJsx.has(instr.lvalue.identifier.id)) {
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
/**
|
||||
* 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 {NodePath} from '@babel/traverse';
|
||||
import * as t from '@babel/types';
|
||||
import {CompilerDiagnostic, CompilerError, ErrorCategory} from '..';
|
||||
import {CodegenFunction} from '../ReactiveScopes';
|
||||
import {Result} from '../Utils/Result';
|
||||
|
||||
/**
|
||||
* IMPORTANT: This validation is only intended for use in unit tests.
|
||||
* It is not intended for use in production.
|
||||
*
|
||||
* This validation is used to ensure that the generated AST has proper source locations
|
||||
* for "important" original nodes.
|
||||
*
|
||||
* There's one big gotcha with this validation: it only works if the "important" original nodes
|
||||
* are not optimized away by the compiler.
|
||||
*
|
||||
* When that scenario happens, we should just update the fixture to not include a node that has no
|
||||
* corresponding node in the generated AST due to being completely removed during compilation.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Some common node types that are important for coverage tracking.
|
||||
* Based on istanbul-lib-instrument
|
||||
*/
|
||||
const IMPORTANT_INSTRUMENTED_TYPES = new Set([
|
||||
'ArrowFunctionExpression',
|
||||
'AssignmentPattern',
|
||||
'ObjectMethod',
|
||||
'ExpressionStatement',
|
||||
'BreakStatement',
|
||||
'ContinueStatement',
|
||||
'ReturnStatement',
|
||||
'ThrowStatement',
|
||||
'TryStatement',
|
||||
'VariableDeclarator',
|
||||
'IfStatement',
|
||||
'ForStatement',
|
||||
'ForInStatement',
|
||||
'ForOfStatement',
|
||||
'WhileStatement',
|
||||
'DoWhileStatement',
|
||||
'SwitchStatement',
|
||||
'SwitchCase',
|
||||
'WithStatement',
|
||||
'FunctionDeclaration',
|
||||
'FunctionExpression',
|
||||
'LabeledStatement',
|
||||
'ConditionalExpression',
|
||||
'LogicalExpression',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Check if a node is a manual memoization call that the compiler optimizes away.
|
||||
* These include useMemo and useCallback calls, which are intentionally removed
|
||||
* by the DropManualMemoization pass.
|
||||
*/
|
||||
function isManualMemoization(node: t.Node): boolean {
|
||||
// Check if this is a useMemo/useCallback call expression
|
||||
if (t.isCallExpression(node)) {
|
||||
const callee = node.callee;
|
||||
if (t.isIdentifier(callee)) {
|
||||
return callee.name === 'useMemo' || callee.name === 'useCallback';
|
||||
}
|
||||
if (
|
||||
t.isMemberExpression(callee) &&
|
||||
t.isIdentifier(callee.property) &&
|
||||
t.isIdentifier(callee.object)
|
||||
) {
|
||||
return (
|
||||
callee.object.name === 'React' &&
|
||||
(callee.property.name === 'useMemo' ||
|
||||
callee.property.name === 'useCallback')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a location key for comparison. We compare by line/column/source,
|
||||
* not by object identity.
|
||||
*/
|
||||
function locationKey(loc: t.SourceLocation): string {
|
||||
return `${loc.start.line}:${loc.start.column}-${loc.end.line}:${loc.end.column}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that important source locations from the original code are preserved
|
||||
* in the generated AST. This ensures that Istanbul coverage instrumentation can
|
||||
* properly map back to the original source code.
|
||||
*
|
||||
* The validator:
|
||||
* 1. Collects locations from "important" nodes in the original AST (those that
|
||||
* Istanbul instruments for coverage tracking)
|
||||
* 2. Exempts known compiler optimizations (useMemo/useCallback removal)
|
||||
* 3. Verifies that all important locations appear somewhere in the generated AST
|
||||
*
|
||||
* Missing locations can cause Istanbul to fail to track coverage for certain
|
||||
* code paths, leading to inaccurate coverage reports.
|
||||
*/
|
||||
export function validateSourceLocations(
|
||||
func: NodePath<
|
||||
t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
|
||||
>,
|
||||
generatedAst: CodegenFunction,
|
||||
): Result<void, CompilerError> {
|
||||
const errors = new CompilerError();
|
||||
|
||||
// Step 1: Collect important locations from the original source
|
||||
const importantOriginalLocations = new Map<
|
||||
string,
|
||||
{loc: t.SourceLocation; nodeType: string}
|
||||
>();
|
||||
|
||||
func.traverse({
|
||||
enter(path) {
|
||||
const node = path.node;
|
||||
|
||||
// Only track node types that Istanbul instruments
|
||||
if (!IMPORTANT_INSTRUMENTED_TYPES.has(node.type)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip manual memoization that the compiler intentionally removes
|
||||
if (isManualMemoization(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Collect the location if it exists
|
||||
if (node.loc) {
|
||||
const key = locationKey(node.loc);
|
||||
importantOriginalLocations.set(key, {
|
||||
loc: node.loc,
|
||||
nodeType: node.type,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Step 2: Collect all locations from the generated AST
|
||||
const generatedLocations = new Set<string>();
|
||||
|
||||
function collectGeneratedLocations(node: t.Node): void {
|
||||
if (node.loc) {
|
||||
generatedLocations.add(locationKey(node.loc));
|
||||
}
|
||||
|
||||
// Use Babel's VISITOR_KEYS to traverse only actual node properties
|
||||
const keys = t.VISITOR_KEYS[node.type as keyof typeof t.VISITOR_KEYS];
|
||||
|
||||
if (!keys) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const key of keys) {
|
||||
const value = (node as any)[key];
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
for (const item of value) {
|
||||
if (t.isNode(item)) {
|
||||
collectGeneratedLocations(item);
|
||||
}
|
||||
}
|
||||
} else if (t.isNode(value)) {
|
||||
collectGeneratedLocations(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Collect from main function body
|
||||
collectGeneratedLocations(generatedAst.body);
|
||||
|
||||
// Collect from outlined functions
|
||||
for (const outlined of generatedAst.outlined) {
|
||||
collectGeneratedLocations(outlined.fn.body);
|
||||
}
|
||||
|
||||
// Step 3: Validate that all important locations are preserved
|
||||
for (const [key, {loc, nodeType}] of importantOriginalLocations) {
|
||||
if (!generatedLocations.has(key)) {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Todo,
|
||||
reason: 'Important source location missing in generated code',
|
||||
description:
|
||||
`Source location for ${nodeType} is missing in the generated output. This can cause coverage instrumentation ` +
|
||||
`to fail to track this code properly, resulting in inaccurate coverage reports.`,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc,
|
||||
message: null,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return errors.asResult();
|
||||
}
|
||||
@@ -12,4 +12,5 @@ export {validateNoCapitalizedCalls} from './ValidateNoCapitalizedCalls';
|
||||
export {validateNoRefAccessInRender} from './ValidateNoRefAccessInRender';
|
||||
export {validateNoSetStateInRender} from './ValidateNoSetStateInRender';
|
||||
export {validatePreservedManualMemoization} from './ValidatePreservedManualMemoization';
|
||||
export {validateSourceLocations} from './ValidateSourceLocations';
|
||||
export {validateUseMemo} from './ValidateUseMemo';
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableInferEventHandlers
|
||||
import {useRef} from 'react';
|
||||
|
||||
// Simulates react-hook-form's handleSubmit
|
||||
function handleSubmit<T>(callback: (data: T) => void | Promise<void>) {
|
||||
return (event: any) => {
|
||||
event.preventDefault();
|
||||
callback({} as T);
|
||||
};
|
||||
}
|
||||
|
||||
// Simulates an upload function
|
||||
async function upload(file: any): Promise<{blob: {url: string}}> {
|
||||
return {blob: {url: 'https://example.com/file.jpg'}};
|
||||
}
|
||||
|
||||
interface SignatureRef {
|
||||
toFile(): any;
|
||||
}
|
||||
|
||||
function Component() {
|
||||
const ref = useRef<SignatureRef>(null);
|
||||
|
||||
const onSubmit = async (value: any) => {
|
||||
// This should be allowed: accessing ref.current in an async event handler
|
||||
// that's wrapped and passed to onSubmit prop
|
||||
let sigUrl: string;
|
||||
if (value.hasSignature) {
|
||||
const {blob} = await upload(ref.current?.toFile());
|
||||
sigUrl = blob?.url || '';
|
||||
} else {
|
||||
sigUrl = value.signature;
|
||||
}
|
||||
console.log('Signature URL:', sigUrl);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<input type="text" name="signature" />
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableInferEventHandlers
|
||||
import { useRef } from "react";
|
||||
|
||||
// Simulates react-hook-form's handleSubmit
|
||||
function handleSubmit(callback) {
|
||||
const $ = _c(2);
|
||||
let t0;
|
||||
if ($[0] !== callback) {
|
||||
t0 = (event) => {
|
||||
event.preventDefault();
|
||||
callback({} as T);
|
||||
};
|
||||
$[0] = callback;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
|
||||
// Simulates an upload function
|
||||
async function upload(file) {
|
||||
const $ = _c(1);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = { blob: { url: "https://example.com/file.jpg" } };
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
|
||||
interface SignatureRef {
|
||||
toFile(): any;
|
||||
}
|
||||
|
||||
function Component() {
|
||||
const $ = _c(4);
|
||||
const ref = useRef(null);
|
||||
|
||||
const onSubmit = async (value) => {
|
||||
let sigUrl;
|
||||
if (value.hasSignature) {
|
||||
const { blob } = await upload(ref.current?.toFile());
|
||||
sigUrl = blob?.url || "";
|
||||
} else {
|
||||
sigUrl = value.signature;
|
||||
}
|
||||
|
||||
console.log("Signature URL:", sigUrl);
|
||||
};
|
||||
|
||||
const t0 = handleSubmit(onSubmit);
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <input type="text" name="signature" />;
|
||||
t2 = <button type="submit">Submit</button>;
|
||||
$[0] = t1;
|
||||
$[1] = t2;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
t2 = $[1];
|
||||
}
|
||||
let t3;
|
||||
if ($[2] !== t0) {
|
||||
t3 = (
|
||||
<form onSubmit={t0}>
|
||||
{t1}
|
||||
{t2}
|
||||
</form>
|
||||
);
|
||||
$[2] = t0;
|
||||
$[3] = t3;
|
||||
} else {
|
||||
t3 = $[3];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <form><input type="text" name="signature"><button type="submit">Submit</button></form>
|
||||
@@ -0,0 +1,48 @@
|
||||
// @enableInferEventHandlers
|
||||
import {useRef} from 'react';
|
||||
|
||||
// Simulates react-hook-form's handleSubmit
|
||||
function handleSubmit<T>(callback: (data: T) => void | Promise<void>) {
|
||||
return (event: any) => {
|
||||
event.preventDefault();
|
||||
callback({} as T);
|
||||
};
|
||||
}
|
||||
|
||||
// Simulates an upload function
|
||||
async function upload(file: any): Promise<{blob: {url: string}}> {
|
||||
return {blob: {url: 'https://example.com/file.jpg'}};
|
||||
}
|
||||
|
||||
interface SignatureRef {
|
||||
toFile(): any;
|
||||
}
|
||||
|
||||
function Component() {
|
||||
const ref = useRef<SignatureRef>(null);
|
||||
|
||||
const onSubmit = async (value: any) => {
|
||||
// This should be allowed: accessing ref.current in an async event handler
|
||||
// that's wrapped and passed to onSubmit prop
|
||||
let sigUrl: string;
|
||||
if (value.hasSignature) {
|
||||
const {blob} = await upload(ref.current?.toFile());
|
||||
sigUrl = blob?.url || '';
|
||||
} else {
|
||||
sigUrl = value.signature;
|
||||
}
|
||||
console.log('Signature URL:', sigUrl);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<input type="text" name="signature" />
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{}],
|
||||
};
|
||||
@@ -0,0 +1,101 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableInferEventHandlers
|
||||
import {useRef} from 'react';
|
||||
|
||||
// Simulates react-hook-form's handleSubmit or similar event handler wrappers
|
||||
function handleSubmit<T>(callback: (data: T) => void) {
|
||||
return (event: any) => {
|
||||
event.preventDefault();
|
||||
callback({} as T);
|
||||
};
|
||||
}
|
||||
|
||||
function Component() {
|
||||
const ref = useRef<HTMLInputElement>(null);
|
||||
|
||||
const onSubmit = (data: any) => {
|
||||
// This should be allowed: accessing ref.current in an event handler
|
||||
// that's wrapped by handleSubmit and passed to onSubmit prop
|
||||
if (ref.current !== null) {
|
||||
console.log(ref.current.value);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<input ref={ref} />
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableInferEventHandlers
|
||||
import { useRef } from "react";
|
||||
|
||||
// Simulates react-hook-form's handleSubmit or similar event handler wrappers
|
||||
function handleSubmit(callback) {
|
||||
const $ = _c(2);
|
||||
let t0;
|
||||
if ($[0] !== callback) {
|
||||
t0 = (event) => {
|
||||
event.preventDefault();
|
||||
callback({} as T);
|
||||
};
|
||||
$[0] = callback;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
|
||||
function Component() {
|
||||
const $ = _c(1);
|
||||
const ref = useRef(null);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
const onSubmit = (data) => {
|
||||
if (ref.current !== null) {
|
||||
console.log(ref.current.value);
|
||||
}
|
||||
};
|
||||
|
||||
t0 = (
|
||||
<>
|
||||
<input ref={ref} />
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <input><form><button type="submit">Submit</button></form>
|
||||
@@ -0,0 +1,36 @@
|
||||
// @enableInferEventHandlers
|
||||
import {useRef} from 'react';
|
||||
|
||||
// Simulates react-hook-form's handleSubmit or similar event handler wrappers
|
||||
function handleSubmit<T>(callback: (data: T) => void) {
|
||||
return (event: any) => {
|
||||
event.preventDefault();
|
||||
callback({} as T);
|
||||
};
|
||||
}
|
||||
|
||||
function Component() {
|
||||
const ref = useRef<HTMLInputElement>(null);
|
||||
|
||||
const onSubmit = (data: any) => {
|
||||
// This should be allowed: accessing ref.current in an event handler
|
||||
// that's wrapped by handleSubmit and passed to onSubmit prop
|
||||
if (ref.current !== null) {
|
||||
console.log(ref.current.value);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<input ref={ref} />
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{}],
|
||||
};
|
||||
@@ -0,0 +1,86 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({value, enabled}) {
|
||||
const [localValue, setLocalValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (enabled) {
|
||||
setLocalValue(value);
|
||||
} else {
|
||||
setLocalValue('disabled');
|
||||
}
|
||||
}, [value, enabled]);
|
||||
|
||||
return <div>{localValue}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 'test', enabled: true}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(6);
|
||||
const { value, enabled } = t0;
|
||||
const [localValue, setLocalValue] = useState("");
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== enabled || $[1] !== value) {
|
||||
t1 = () => {
|
||||
if (enabled) {
|
||||
setLocalValue(value);
|
||||
} else {
|
||||
setLocalValue("disabled");
|
||||
}
|
||||
};
|
||||
|
||||
t2 = [value, enabled];
|
||||
$[0] = enabled;
|
||||
$[1] = value;
|
||||
$[2] = t1;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
t2 = $[3];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[4] !== localValue) {
|
||||
t3 = <div>{localValue}</div>;
|
||||
$[4] = localValue;
|
||||
$[5] = t3;
|
||||
} else {
|
||||
t3 = $[5];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ value: "test", enabled: true }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```
|
||||
{"kind":"CompileError","detail":{"options":{"description":"Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user\n\nThis setState call is setting a derived value that depends on the following reactive sources:\n\nProps: [value]\n\nData Flow Tree:\n└── value (Prop)\n\nSee: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state","category":"EffectDerivationsOfState","reason":"You might not need an effect. Derive values in render, not effects.","details":[{"kind":"error","loc":{"start":{"line":9,"column":6,"index":244},"end":{"line":9,"column":19,"index":257},"filename":"derived-state-conditionally-in-effect.ts","identifierName":"setLocalValue"},"message":"This should be computed during render, not in an effect"}]}},"fnLoc":null}
|
||||
{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":0,"index":107},"end":{"line":16,"column":1,"index":378},"filename":"derived-state-conditionally-in-effect.ts"},"fnName":"Component","memoSlots":6,"memoBlocks":2,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>test</div>
|
||||
@@ -1,4 +1,4 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({value, enabled}) {
|
||||
@@ -0,0 +1,78 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
export default function Component({input = 'empty'}) {
|
||||
const [currInput, setCurrInput] = useState(input);
|
||||
const localConst = 'local const';
|
||||
|
||||
useEffect(() => {
|
||||
setCurrInput(input + localConst);
|
||||
}, [input, localConst]);
|
||||
|
||||
return <div>{currInput}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{input: 'test'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function Component(t0) {
|
||||
const $ = _c(5);
|
||||
const { input: t1 } = t0;
|
||||
const input = t1 === undefined ? "empty" : t1;
|
||||
const [currInput, setCurrInput] = useState(input);
|
||||
let t2;
|
||||
let t3;
|
||||
if ($[0] !== input) {
|
||||
t2 = () => {
|
||||
setCurrInput(input + "local const");
|
||||
};
|
||||
t3 = [input, "local const"];
|
||||
$[0] = input;
|
||||
$[1] = t2;
|
||||
$[2] = t3;
|
||||
} else {
|
||||
t2 = $[1];
|
||||
t3 = $[2];
|
||||
}
|
||||
useEffect(t2, t3);
|
||||
let t4;
|
||||
if ($[3] !== currInput) {
|
||||
t4 = <div>{currInput}</div>;
|
||||
$[3] = currInput;
|
||||
$[4] = t4;
|
||||
} else {
|
||||
t4 = $[4];
|
||||
}
|
||||
return t4;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ input: "test" }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```
|
||||
{"kind":"CompileError","detail":{"options":{"description":"Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user\n\nThis setState call is setting a derived value that depends on the following reactive sources:\n\nProps: [input]\n\nData Flow Tree:\n└── input (Prop)\n\nSee: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state","category":"EffectDerivationsOfState","reason":"You might not need an effect. Derive values in render, not effects.","details":[{"kind":"error","loc":{"start":{"line":9,"column":4,"index":276},"end":{"line":9,"column":16,"index":288},"filename":"derived-state-from-default-props.ts","identifierName":"setCurrInput"},"message":"This should be computed during render, not in an effect"}]}},"fnLoc":null}
|
||||
{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":15,"index":122},"end":{"line":13,"column":1,"index":372},"filename":"derived-state-from-default-props.ts"},"fnName":"Component","memoSlots":5,"memoBlocks":2,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>testlocal const</div>
|
||||
@@ -1,4 +1,4 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
export default function Component({input = 'empty'}) {
|
||||
@@ -0,0 +1,77 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({shouldChange}) {
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldChange) {
|
||||
setCount(count + 1);
|
||||
}
|
||||
}, [count]);
|
||||
|
||||
return <div>{count}</div>;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(7);
|
||||
const { shouldChange } = t0;
|
||||
const [count, setCount] = useState(0);
|
||||
let t1;
|
||||
if ($[0] !== count || $[1] !== shouldChange) {
|
||||
t1 = () => {
|
||||
if (shouldChange) {
|
||||
setCount(count + 1);
|
||||
}
|
||||
};
|
||||
$[0] = count;
|
||||
$[1] = shouldChange;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
let t2;
|
||||
if ($[3] !== count) {
|
||||
t2 = [count];
|
||||
$[3] = count;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[5] !== count) {
|
||||
t3 = <div>{count}</div>;
|
||||
$[5] = count;
|
||||
$[6] = t3;
|
||||
} else {
|
||||
t3 = $[6];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```
|
||||
{"kind":"CompileError","detail":{"options":{"description":"Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user\n\nThis setState call is setting a derived value that depends on the following reactive sources:\n\nState: [count]\n\nData Flow Tree:\n└── count (State)\n\nSee: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state","category":"EffectDerivationsOfState","reason":"You might not need an effect. Derive values in render, not effects.","details":[{"kind":"error","loc":{"start":{"line":10,"column":6,"index":237},"end":{"line":10,"column":14,"index":245},"filename":"derived-state-from-local-state-in-effect.ts","identifierName":"setCount"},"message":"This should be computed during render, not in an effect"}]}},"fnLoc":null}
|
||||
{"kind":"CompileSuccess","fnLoc":{"start":{"line":5,"column":0,"index":108},"end":{"line":15,"column":1,"index":310},"filename":"derived-state-from-local-state-in-effect.ts"},"fnName":"Component","memoSlots":7,"memoBlocks":3,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -1,4 +1,4 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({firstName}) {
|
||||
const [lastName, setLastName] = useState('Doe');
|
||||
const [fullName, setFullName] = useState('John');
|
||||
|
||||
const middleName = 'D.';
|
||||
|
||||
useEffect(() => {
|
||||
setFullName(firstName + ' ' + middleName + ' ' + lastName);
|
||||
}, [firstName, middleName, lastName]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input value={lastName} onChange={e => setLastName(e.target.value)} />
|
||||
<div>{fullName}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{firstName: 'John'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(12);
|
||||
const { firstName } = t0;
|
||||
const [lastName, setLastName] = useState("Doe");
|
||||
const [fullName, setFullName] = useState("John");
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== firstName || $[1] !== lastName) {
|
||||
t1 = () => {
|
||||
setFullName(firstName + " " + "D." + " " + lastName);
|
||||
};
|
||||
t2 = [firstName, "D.", lastName];
|
||||
$[0] = firstName;
|
||||
$[1] = lastName;
|
||||
$[2] = t1;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
t2 = $[3];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t3 = (e) => setLastName(e.target.value);
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
let t4;
|
||||
if ($[5] !== lastName) {
|
||||
t4 = <input value={lastName} onChange={t3} />;
|
||||
$[5] = lastName;
|
||||
$[6] = t4;
|
||||
} else {
|
||||
t4 = $[6];
|
||||
}
|
||||
let t5;
|
||||
if ($[7] !== fullName) {
|
||||
t5 = <div>{fullName}</div>;
|
||||
$[7] = fullName;
|
||||
$[8] = t5;
|
||||
} else {
|
||||
t5 = $[8];
|
||||
}
|
||||
let t6;
|
||||
if ($[9] !== t4 || $[10] !== t5) {
|
||||
t6 = (
|
||||
<div>
|
||||
{t4}
|
||||
{t5}
|
||||
</div>
|
||||
);
|
||||
$[9] = t4;
|
||||
$[10] = t5;
|
||||
$[11] = t6;
|
||||
} else {
|
||||
t6 = $[11];
|
||||
}
|
||||
return t6;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ firstName: "John" }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```
|
||||
{"kind":"CompileError","detail":{"options":{"description":"Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user\n\nThis setState call is setting a derived value that depends on the following reactive sources:\n\nProps: [firstName]\nState: [lastName]\n\nData Flow Tree:\n├── firstName (Prop)\n└── lastName (State)\n\nSee: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state","category":"EffectDerivationsOfState","reason":"You might not need an effect. Derive values in render, not effects.","details":[{"kind":"error","loc":{"start":{"line":11,"column":4,"index":297},"end":{"line":11,"column":15,"index":308},"filename":"derived-state-from-prop-local-state-and-component-scope.ts","identifierName":"setFullName"},"message":"This should be computed during render, not in an effect"}]}},"fnLoc":null}
|
||||
{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":0,"index":107},"end":{"line":20,"column":1,"index":542},"filename":"derived-state-from-prop-local-state-and-component-scope.ts"},"fnName":"Component","memoSlots":12,"memoBlocks":5,"memoValues":6,"prunedMemoBlocks":0,"prunedMemoValues":0}
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div><input value="Doe"><div>John D. Doe</div></div>
|
||||
@@ -1,4 +1,4 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({firstName}) {
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({initialName}) {
|
||||
@@ -29,7 +29,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
function Component(t0) {
|
||||
@@ -79,6 +79,12 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```
|
||||
{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":0,"index":107},"end":{"line":16,"column":1,"index":359},"filename":"derived-state-from-prop-setter-call-outside-effect-no-error.ts"},"fnName":"Component","memoSlots":6,"memoBlocks":3,"memoValues":4,"prunedMemoBlocks":0,"prunedMemoValues":0}
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div><input value="John"></div>
|
||||
@@ -1,4 +1,4 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({initialName}) {
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
|
||||
function Component({value}) {
|
||||
const [checked, setChecked] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setChecked(value === '' ? [] : value.split(','));
|
||||
}, [value]);
|
||||
|
||||
return <div>{checked}</div>;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(5);
|
||||
const { value } = t0;
|
||||
const [checked, setChecked] = useState("");
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== value) {
|
||||
t1 = () => {
|
||||
setChecked(value === "" ? [] : value.split(","));
|
||||
};
|
||||
t2 = [value];
|
||||
$[0] = value;
|
||||
$[1] = t1;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
t2 = $[2];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[3] !== checked) {
|
||||
t3 = <div>{checked}</div>;
|
||||
$[3] = checked;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -0,0 +1,11 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
|
||||
function Component({value}) {
|
||||
const [checked, setChecked] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setChecked(value === '' ? [] : value.split(','));
|
||||
}, [value]);
|
||||
|
||||
return <div>{checked}</div>;
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function MockComponent({onSet}) {
|
||||
@@ -28,7 +28,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
function MockComponent(t0) {
|
||||
@@ -80,6 +80,13 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```
|
||||
{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":0,"index":107},"end":{"line":6,"column":1,"index":211},"filename":"derived-state-from-prop-setter-used-outside-effect-no-error.ts"},"fnName":"MockComponent","memoSlots":2,"memoBlocks":1,"memoValues":1,"prunedMemoBlocks":0,"prunedMemoValues":0}
|
||||
{"kind":"CompileSuccess","fnLoc":{"start":{"line":8,"column":0,"index":213},"end":{"line":15,"column":1,"index":402},"filename":"derived-state-from-prop-setter-used-outside-effect-no-error.ts"},"fnName":"Component","memoSlots":4,"memoBlocks":2,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>Mock Component</div>
|
||||
@@ -1,4 +1,4 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function MockComponent({onSet}) {
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({value}) {
|
||||
const [localValue, setLocalValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setLocalValue(value);
|
||||
document.title = `Value: ${value}`;
|
||||
}, [value]);
|
||||
|
||||
return <div>{localValue}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 'test'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(5);
|
||||
const { value } = t0;
|
||||
const [localValue, setLocalValue] = useState("");
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== value) {
|
||||
t1 = () => {
|
||||
setLocalValue(value);
|
||||
document.title = `Value: ${value}`;
|
||||
};
|
||||
t2 = [value];
|
||||
$[0] = value;
|
||||
$[1] = t1;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
t2 = $[2];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[3] !== localValue) {
|
||||
t3 = <div>{localValue}</div>;
|
||||
$[3] = localValue;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ value: "test" }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```
|
||||
{"kind":"CompileError","detail":{"options":{"description":"Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user\n\nThis setState call is setting a derived value that depends on the following reactive sources:\n\nProps: [value]\n\nData Flow Tree:\n└── value (Prop)\n\nSee: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state","category":"EffectDerivationsOfState","reason":"You might not need an effect. Derive values in render, not effects.","details":[{"kind":"error","loc":{"start":{"line":8,"column":4,"index":214},"end":{"line":8,"column":17,"index":227},"filename":"derived-state-from-prop-with-side-effect.ts","identifierName":"setLocalValue"},"message":"This should be computed during render, not in an effect"}]}},"fnLoc":null}
|
||||
{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":0,"index":107},"end":{"line":13,"column":1,"index":327},"filename":"derived-state-from-prop-with-side-effect.ts"},"fnName":"Component","memoSlots":5,"memoBlocks":2,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>test</div>
|
||||
@@ -1,4 +1,4 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({value}) {
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import {useEffect, useState, useRef} from 'react';
|
||||
|
||||
export default function Component({test}) {
|
||||
@@ -27,7 +27,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
|
||||
export default function Component(t0) {
|
||||
@@ -68,6 +68,12 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```
|
||||
{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":15,"index":130},"end":{"line":14,"column":1,"index":328},"filename":"derived-state-from-ref-and-state-no-error.ts"},"fnName":"Component","memoSlots":5,"memoBlocks":2,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) nulltestString
|
||||
@@ -1,4 +1,4 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import {useEffect, useState, useRef} from 'react';
|
||||
|
||||
export default function Component({test}) {
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({propValue}) {
|
||||
const [value, setValue] = useState(null);
|
||||
|
||||
function localFunction() {
|
||||
console.log('local function');
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setValue(propValue);
|
||||
localFunction();
|
||||
}, [propValue]);
|
||||
|
||||
return <div>{value}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{propValue: 'test'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(6);
|
||||
const { propValue } = t0;
|
||||
const [value, setValue] = useState(null);
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = function localFunction() {
|
||||
console.log("local function");
|
||||
};
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
}
|
||||
const localFunction = t1;
|
||||
let t2;
|
||||
let t3;
|
||||
if ($[1] !== propValue) {
|
||||
t2 = () => {
|
||||
setValue(propValue);
|
||||
localFunction();
|
||||
};
|
||||
t3 = [propValue];
|
||||
$[1] = propValue;
|
||||
$[2] = t2;
|
||||
$[3] = t3;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
t3 = $[3];
|
||||
}
|
||||
useEffect(t2, t3);
|
||||
let t4;
|
||||
if ($[4] !== value) {
|
||||
t4 = <div>{value}</div>;
|
||||
$[4] = value;
|
||||
$[5] = t4;
|
||||
} else {
|
||||
t4 = $[5];
|
||||
}
|
||||
return t4;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ propValue: "test" }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```
|
||||
{"kind":"CompileError","detail":{"options":{"description":"Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user\n\nThis setState call is setting a derived value that depends on the following reactive sources:\n\nProps: [propValue]\n\nData Flow Tree:\n└── propValue (Prop)\n\nSee: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state","category":"EffectDerivationsOfState","reason":"You might not need an effect. Derive values in render, not effects.","details":[{"kind":"error","loc":{"start":{"line":12,"column":4,"index":279},"end":{"line":12,"column":12,"index":287},"filename":"effect-contains-local-function-call.ts","identifierName":"setValue"},"message":"This should be computed during render, not in an effect"}]}},"fnLoc":null}
|
||||
{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":0,"index":107},"end":{"line":17,"column":1,"index":371},"filename":"effect-contains-local-function-call.ts"},"fnName":"Component","memoSlots":6,"memoBlocks":3,"memoValues":4,"prunedMemoBlocks":0,"prunedMemoValues":0}
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>test</div>
|
||||
logs: ['local function']
|
||||
@@ -1,4 +1,4 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({propValue}) {
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({propValue, onChange}) {
|
||||
@@ -25,7 +25,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
function Component(t0) {
|
||||
@@ -70,6 +70,13 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```
|
||||
{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":0,"index":107},"end":{"line":12,"column":1,"index":306},"filename":"effect-contains-prop-function-call-no-error.ts"},"fnName":"Component","memoSlots":7,"memoBlocks":3,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
|
||||
{"kind":"CompileSuccess","fnLoc":{"start":{"line":16,"column":41,"index":402},"end":{"line":16,"column":49,"index":410},"filename":"effect-contains-prop-function-call-no-error.ts"},"fnName":null,"memoSlots":0,"memoBlocks":0,"memoValues":0,"prunedMemoBlocks":0,"prunedMemoValues":0}
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>test</div>
|
||||
@@ -1,4 +1,4 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({propValue, onChange}) {
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
|
||||
function Component({prop}) {
|
||||
const [s, setS] = useState(0);
|
||||
useEffect(() => {
|
||||
setS(prop);
|
||||
}, [prop, setS]);
|
||||
|
||||
return <div>{prop}</div>;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(5);
|
||||
const { prop } = t0;
|
||||
const [, setS] = useState(0);
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== prop) {
|
||||
t1 = () => {
|
||||
setS(prop);
|
||||
};
|
||||
t2 = [prop, setS];
|
||||
$[0] = prop;
|
||||
$[1] = t1;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
t2 = $[2];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[3] !== prop) {
|
||||
t3 = <div>{prop}</div>;
|
||||
$[3] = prop;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```
|
||||
{"kind":"CompileError","detail":{"options":{"description":"Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user\n\nThis setState call is setting a derived value that depends on the following reactive sources:\n\nProps: [prop]\n\nData Flow Tree:\n└── prop (Prop)\n\nSee: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state","category":"EffectDerivationsOfState","reason":"You might not need an effect. Derive values in render, not effects.","details":[{"kind":"error","loc":{"start":{"line":6,"column":4,"index":150},"end":{"line":6,"column":8,"index":154},"filename":"effect-used-in-dep-array-still-errors.ts","identifierName":"setS"},"message":"This should be computed during render, not in an effect"}]}},"fnLoc":null}
|
||||
{"kind":"CompileSuccess","fnLoc":{"start":{"line":3,"column":0,"index":64},"end":{"line":10,"column":1,"index":212},"filename":"effect-used-in-dep-array-still-errors.ts"},"fnName":"Component","memoSlots":5,"memoBlocks":2,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -0,0 +1,10 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
|
||||
function Component({prop}) {
|
||||
const [s, setS] = useState(0);
|
||||
useEffect(() => {
|
||||
setS(prop);
|
||||
}, [prop, setS]);
|
||||
|
||||
return <div>{prop}</div>;
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component(file: File) {
|
||||
const [imageUrl, setImageUrl] = useState(null);
|
||||
|
||||
/*
|
||||
* Cleaning up the variable or a source of the variable used to setState
|
||||
* inside the effect communicates that we always need to clean up something
|
||||
* which is a valid use case for useEffect. In which case we want to
|
||||
* avoid an throwing
|
||||
*/
|
||||
useEffect(() => {
|
||||
const imageUrlPrepared = URL.createObjectURL(file);
|
||||
setImageUrl(imageUrlPrepared);
|
||||
return () => URL.revokeObjectURL(imageUrlPrepared);
|
||||
}, [file]);
|
||||
|
||||
return <Image src={imageUrl} xstyle={styles.imageSizeLimits} />;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
function Component(file) {
|
||||
const $ = _c(5);
|
||||
const [imageUrl, setImageUrl] = useState(null);
|
||||
let t0;
|
||||
let t1;
|
||||
if ($[0] !== file) {
|
||||
t0 = () => {
|
||||
const imageUrlPrepared = URL.createObjectURL(file);
|
||||
setImageUrl(imageUrlPrepared);
|
||||
return () => URL.revokeObjectURL(imageUrlPrepared);
|
||||
};
|
||||
t1 = [file];
|
||||
$[0] = file;
|
||||
$[1] = t0;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
t1 = $[2];
|
||||
}
|
||||
useEffect(t0, t1);
|
||||
let t2;
|
||||
if ($[3] !== imageUrl) {
|
||||
t2 = <Image src={imageUrl} xstyle={styles.imageSizeLimits} />;
|
||||
$[3] = imageUrl;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```
|
||||
{"kind":"CompileSuccess","fnLoc":{"start":{"line":5,"column":0,"index":108},"end":{"line":21,"column":1,"index":700},"filename":"effect-with-cleanup-function-depending-on-derived-computation-value.ts"},"fnName":"Component","memoSlots":5,"memoBlocks":2,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -0,0 +1,21 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component(file: File) {
|
||||
const [imageUrl, setImageUrl] = useState(null);
|
||||
|
||||
/*
|
||||
* Cleaning up the variable or a source of the variable used to setState
|
||||
* inside the effect communicates that we always need to clean up something
|
||||
* which is a valid use case for useEffect. In which case we want to
|
||||
* avoid an throwing
|
||||
*/
|
||||
useEffect(() => {
|
||||
const imageUrlPrepared = URL.createObjectURL(file);
|
||||
setImageUrl(imageUrlPrepared);
|
||||
return () => URL.revokeObjectURL(imageUrlPrepared);
|
||||
}, [file]);
|
||||
|
||||
return <Image src={imageUrl} xstyle={styles.imageSizeLimits} />;
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({propValue}) {
|
||||
@@ -25,7 +25,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
function Component(t0) {
|
||||
@@ -65,6 +65,12 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```
|
||||
{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":0,"index":107},"end":{"line":12,"column":1,"index":298},"filename":"effect-with-global-function-call-no-error.ts"},"fnName":"Component","memoSlots":5,"memoBlocks":2,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) globalCall is not defined
|
||||
@@ -1,4 +1,4 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({propValue}) {
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({value, enabled}) {
|
||||
const [localValue, setLocalValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (enabled) {
|
||||
setLocalValue(value);
|
||||
} else {
|
||||
setLocalValue('disabled');
|
||||
}
|
||||
}, [value, enabled]);
|
||||
|
||||
return <div>{localValue}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 'test', enabled: true}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: You might not need an effect. Derive values in render, not effects.
|
||||
|
||||
Derived values (From props: [value]) 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-conditionally-in-effect.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 | }
|
||||
```
|
||||
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
export default function Component({input = 'empty'}) {
|
||||
const [currInput, setCurrInput] = useState(input);
|
||||
const localConst = 'local const';
|
||||
|
||||
useEffect(() => {
|
||||
setCurrInput(input + localConst);
|
||||
}, [input, localConst]);
|
||||
|
||||
return <div>{currInput}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{input: 'test'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: You might not need an effect. Derive values in render, not effects.
|
||||
|
||||
Derived values (From props: [input]) 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-from-default-props.ts:9:4
|
||||
7 |
|
||||
8 | useEffect(() => {
|
||||
> 9 | setCurrInput(input + localConst);
|
||||
| ^^^^^^^^^^^^ This should be computed during render, not in an effect
|
||||
10 | }, [input, localConst]);
|
||||
11 |
|
||||
12 | return <div>{currInput}</div>;
|
||||
```
|
||||
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({shouldChange}) {
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldChange) {
|
||||
setCount(count + 1);
|
||||
}
|
||||
}, [count]);
|
||||
|
||||
return <div>{count}</div>;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: You might not need an effect. Derive values in render, not effects.
|
||||
|
||||
Derived values (From local state: [count]) 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-from-local-state-in-effect.ts:10:6
|
||||
8 | useEffect(() => {
|
||||
9 | if (shouldChange) {
|
||||
> 10 | setCount(count + 1);
|
||||
| ^^^^^^^^ This should be computed during render, not in an effect
|
||||
11 | }
|
||||
12 | }, [count]);
|
||||
13 |
|
||||
```
|
||||
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({firstName}) {
|
||||
const [lastName, setLastName] = useState('Doe');
|
||||
const [fullName, setFullName] = useState('John');
|
||||
|
||||
const middleName = 'D.';
|
||||
|
||||
useEffect(() => {
|
||||
setFullName(firstName + ' ' + middleName + ' ' + lastName);
|
||||
}, [firstName, middleName, lastName]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input value={lastName} onChange={e => setLastName(e.target.value)} />
|
||||
<div>{fullName}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{firstName: 'John'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: You might not need an effect. Derive values in render, not effects.
|
||||
|
||||
Derived values (From props and local state: [firstName, lastName]) 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-from-prop-local-state-and-component-scope.ts:11:4
|
||||
9 |
|
||||
10 | useEffect(() => {
|
||||
> 11 | setFullName(firstName + ' ' + middleName + ' ' + lastName);
|
||||
| ^^^^^^^^^^^ This should be computed during render, not in an effect
|
||||
12 | }, [firstName, middleName, lastName]);
|
||||
13 |
|
||||
14 | return (
|
||||
```
|
||||
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({value}) {
|
||||
const [localValue, setLocalValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setLocalValue(value);
|
||||
document.title = `Value: ${value}`;
|
||||
}, [value]);
|
||||
|
||||
return <div>{localValue}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 'test'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: You might not need an effect. Derive values in render, not effects.
|
||||
|
||||
Derived values (From props: [value]) 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-from-prop-with-side-effect.ts:8:4
|
||||
6 |
|
||||
7 | useEffect(() => {
|
||||
> 8 | setLocalValue(value);
|
||||
| ^^^^^^^^^^^^^ This should be computed during render, not in an effect
|
||||
9 | document.title = `Value: ${value}`;
|
||||
10 | }, [value]);
|
||||
11 |
|
||||
```
|
||||
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({propValue}) {
|
||||
const [value, setValue] = useState(null);
|
||||
|
||||
function localFunction() {
|
||||
console.log('local function');
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setValue(propValue);
|
||||
localFunction();
|
||||
}, [propValue]);
|
||||
|
||||
return <div>{value}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{propValue: 'test'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: You might not need an effect. Derive values in render, not effects.
|
||||
|
||||
Derived values (From props: [propValue]) 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.effect-contains-local-function-call.ts:12:4
|
||||
10 |
|
||||
11 | useEffect(() => {
|
||||
> 12 | setValue(propValue);
|
||||
| ^^^^^^^^ This should be computed during render, not in an effect
|
||||
13 | localFunction();
|
||||
14 | }, [propValue]);
|
||||
15 |
|
||||
```
|
||||
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component() {
|
||||
const [firstName, setFirstName] = useState('Taylor');
|
||||
const lastName = 'Swift';
|
||||
|
||||
// 🔴 Avoid: redundant state and unnecessary Effect
|
||||
const [fullName, setFullName] = useState('');
|
||||
useEffect(() => {
|
||||
setFullName(firstName + ' ' + lastName);
|
||||
}, [firstName, lastName]);
|
||||
|
||||
return <div>{fullName}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: You might not need an effect. Derive values in render, not effects.
|
||||
|
||||
Derived values (From local state: [firstName]) 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-computation-in-effect.ts:11:4
|
||||
9 | const [fullName, setFullName] = useState('');
|
||||
10 | useEffect(() => {
|
||||
> 11 | setFullName(firstName + ' ' + lastName);
|
||||
| ^^^^^^^^^^^ This should be computed during render, not in an effect
|
||||
12 | }, [firstName, lastName]);
|
||||
13 |
|
||||
14 | return <div>{fullName}</div>;
|
||||
```
|
||||
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
export default function Component(props) {
|
||||
const [displayValue, setDisplayValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const computed = props.prefix + props.value + props.suffix;
|
||||
setDisplayValue(computed);
|
||||
}, [props.prefix, props.value, props.suffix]);
|
||||
|
||||
return <div>{displayValue}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{prefix: '[', value: 'test', suffix: ']'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: You might not need an effect. Derive values in render, not effects.
|
||||
|
||||
Derived values (From props: [props]) 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-computed-props.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>;
|
||||
```
|
||||
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
export default function Component({props}) {
|
||||
const [fullName, setFullName] = useState(
|
||||
props.firstName + ' ' + props.lastName
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setFullName(props.firstName + ' ' + props.lastName);
|
||||
}, [props.firstName, props.lastName]);
|
||||
|
||||
return <div>{fullName}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{props: {firstName: 'John', lastName: 'Doe'}}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: You might not need an effect. Derive values in render, not effects.
|
||||
|
||||
Derived values (From props: [props]) 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-destructured-props.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>;
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp @enableTreatSetIdentifiersAsStateSetters @loggerTestOnly
|
||||
|
||||
function Component({setParentState, prop}) {
|
||||
useEffect(() => {
|
||||
setParentState(prop);
|
||||
}, [prop]);
|
||||
|
||||
return <div>{prop}</div>;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @enableTreatSetIdentifiersAsStateSetters @loggerTestOnly
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(7);
|
||||
const { setParentState, prop } = t0;
|
||||
let t1;
|
||||
if ($[0] !== prop || $[1] !== setParentState) {
|
||||
t1 = () => {
|
||||
setParentState(prop);
|
||||
};
|
||||
$[0] = prop;
|
||||
$[1] = setParentState;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
let t2;
|
||||
if ($[3] !== prop) {
|
||||
t2 = [prop];
|
||||
$[3] = prop;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[5] !== prop) {
|
||||
t3 = <div>{prop}</div>;
|
||||
$[5] = prop;
|
||||
$[6] = t3;
|
||||
} else {
|
||||
t3 = $[6];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```
|
||||
{"kind":"CompileSuccess","fnLoc":{"start":{"line":3,"column":0,"index":105},"end":{"line":9,"column":1,"index":240},"filename":"from-props-setstate-in-effect-no-error.ts"},"fnName":"Component","memoSlots":7,"memoBlocks":3,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -0,0 +1,9 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp @enableTreatSetIdentifiersAsStateSetters @loggerTestOnly
|
||||
|
||||
function Component({setParentState, prop}) {
|
||||
useEffect(() => {
|
||||
setParentState(prop);
|
||||
}, [prop]);
|
||||
|
||||
return <div>{prop}</div>;
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component() {
|
||||
const [firstName, setFirstName] = useState('Taylor');
|
||||
const lastName = 'Swift';
|
||||
|
||||
// 🔴 Avoid: redundant state and unnecessary Effect
|
||||
const [fullName, setFullName] = useState('');
|
||||
useEffect(() => {
|
||||
setFullName(firstName + ' ' + lastName);
|
||||
}, [firstName, lastName]);
|
||||
|
||||
return <div>{fullName}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
function Component() {
|
||||
const $ = _c(5);
|
||||
const [firstName] = useState("Taylor");
|
||||
|
||||
const [fullName, setFullName] = useState("");
|
||||
let t0;
|
||||
let t1;
|
||||
if ($[0] !== firstName) {
|
||||
t0 = () => {
|
||||
setFullName(firstName + " " + "Swift");
|
||||
};
|
||||
t1 = [firstName, "Swift"];
|
||||
$[0] = firstName;
|
||||
$[1] = t0;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
t1 = $[2];
|
||||
}
|
||||
useEffect(t0, t1);
|
||||
let t2;
|
||||
if ($[3] !== fullName) {
|
||||
t2 = <div>{fullName}</div>;
|
||||
$[3] = fullName;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```
|
||||
{"kind":"CompileError","detail":{"options":{"description":"Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user\n\nThis setState call is setting a derived value that depends on the following reactive sources:\n\nState: [firstName]\n\nData Flow Tree:\n└── firstName (State)\n\nSee: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state","category":"EffectDerivationsOfState","reason":"You might not need an effect. Derive values in render, not effects.","details":[{"kind":"error","loc":{"start":{"line":11,"column":4,"index":341},"end":{"line":11,"column":15,"index":352},"filename":"invalid-derived-computation-in-effect.ts","identifierName":"setFullName"},"message":"This should be computed during render, not in an effect"}]}},"fnLoc":null}
|
||||
{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":0,"index":107},"end":{"line":15,"column":1,"index":445},"filename":"invalid-derived-computation-in-effect.ts"},"fnName":"Component","memoSlots":5,"memoBlocks":2,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>Taylor Swift</div>
|
||||
@@ -1,4 +1,4 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component() {
|
||||
@@ -0,0 +1,79 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
export default function Component(props) {
|
||||
const [displayValue, setDisplayValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const computed = props.prefix + props.value + props.suffix;
|
||||
setDisplayValue(computed);
|
||||
}, [props.prefix, props.value, props.suffix]);
|
||||
|
||||
return <div>{displayValue}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{prefix: '[', value: 'test', suffix: ']'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function Component(props) {
|
||||
const $ = _c(7);
|
||||
const [displayValue, setDisplayValue] = useState("");
|
||||
let t0;
|
||||
let t1;
|
||||
if ($[0] !== props.prefix || $[1] !== props.suffix || $[2] !== props.value) {
|
||||
t0 = () => {
|
||||
const computed = props.prefix + props.value + props.suffix;
|
||||
setDisplayValue(computed);
|
||||
};
|
||||
t1 = [props.prefix, props.value, props.suffix];
|
||||
$[0] = props.prefix;
|
||||
$[1] = props.suffix;
|
||||
$[2] = props.value;
|
||||
$[3] = t0;
|
||||
$[4] = t1;
|
||||
} else {
|
||||
t0 = $[3];
|
||||
t1 = $[4];
|
||||
}
|
||||
useEffect(t0, t1);
|
||||
let t2;
|
||||
if ($[5] !== displayValue) {
|
||||
t2 = <div>{displayValue}</div>;
|
||||
$[5] = displayValue;
|
||||
$[6] = t2;
|
||||
} else {
|
||||
t2 = $[6];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ prefix: "[", value: "test", suffix: "]" }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```
|
||||
{"kind":"CompileError","detail":{"options":{"description":"Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user\n\nThis setState call is setting a derived value that depends on the following reactive sources:\n\nProps: [props]\n\nData Flow Tree:\n└── computed\n └── props (Prop)\n\nSee: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state","category":"EffectDerivationsOfState","reason":"You might not need an effect. Derive values in render, not effects.","details":[{"kind":"error","loc":{"start":{"line":9,"column":4,"index":295},"end":{"line":9,"column":19,"index":310},"filename":"invalid-derived-state-from-computed-props.ts","identifierName":"setDisplayValue"},"message":"This should be computed during render, not in an effect"}]}},"fnLoc":null}
|
||||
{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":15,"index":122},"end":{"line":13,"column":1,"index":409},"filename":"invalid-derived-state-from-computed-props.ts"},"fnName":"Component","memoSlots":7,"memoBlocks":2,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>[test]</div>
|
||||
@@ -1,4 +1,4 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
export default function Component(props) {
|
||||
@@ -0,0 +1,81 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
export default function Component({props}) {
|
||||
const [fullName, setFullName] = useState(
|
||||
props.firstName + ' ' + props.lastName
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setFullName(props.firstName + ' ' + props.lastName);
|
||||
}, [props.firstName, props.lastName]);
|
||||
|
||||
return <div>{fullName}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{props: {firstName: 'John', lastName: 'Doe'}}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function Component(t0) {
|
||||
const $ = _c(6);
|
||||
const { props } = t0;
|
||||
const [fullName, setFullName] = useState(
|
||||
props.firstName + " " + props.lastName,
|
||||
);
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== props.firstName || $[1] !== props.lastName) {
|
||||
t1 = () => {
|
||||
setFullName(props.firstName + " " + props.lastName);
|
||||
};
|
||||
t2 = [props.firstName, props.lastName];
|
||||
$[0] = props.firstName;
|
||||
$[1] = props.lastName;
|
||||
$[2] = t1;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
t2 = $[3];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[4] !== fullName) {
|
||||
t3 = <div>{fullName}</div>;
|
||||
$[4] = fullName;
|
||||
$[5] = t3;
|
||||
} else {
|
||||
t3 = $[5];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ props: { firstName: "John", lastName: "Doe" } }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```
|
||||
{"kind":"CompileError","detail":{"options":{"description":"Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user\n\nThis setState call is setting a derived value that depends on the following reactive sources:\n\nProps: [props]\n\nData Flow Tree:\n└── props (Prop)\n\nSee: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state","category":"EffectDerivationsOfState","reason":"You might not need an effect. Derive values in render, not effects.","details":[{"kind":"error","loc":{"start":{"line":10,"column":4,"index":269},"end":{"line":10,"column":15,"index":280},"filename":"invalid-derived-state-from-destructured-props.ts","identifierName":"setFullName"},"message":"This should be computed during render, not in an effect"}]}},"fnLoc":null}
|
||||
{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":15,"index":122},"end":{"line":14,"column":1,"index":397},"filename":"invalid-derived-state-from-destructured-props.ts"},"fnName":"Component","memoSlots":6,"memoBlocks":2,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>John Doe</div>
|
||||
@@ -1,4 +1,4 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
export default function Component({props}) {
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import {useEffect, useState, useRef} from 'react';
|
||||
|
||||
export default function Component({test}) {
|
||||
@@ -31,7 +31,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
|
||||
export default function Component(t0) {
|
||||
@@ -77,6 +77,12 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```
|
||||
{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":15,"index":130},"end":{"line":18,"column":1,"index":386},"filename":"ref-conditional-in-effect-no-error.ts"},"fnName":"Component","memoSlots":5,"memoBlocks":2,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) 8
|
||||
@@ -1,4 +1,4 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import {useEffect, useState, useRef} from 'react';
|
||||
|
||||
export default function Component({test}) {
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
|
||||
function Component({prop}) {
|
||||
const [s, setS] = useState();
|
||||
const [second, setSecond] = useState(prop);
|
||||
|
||||
/*
|
||||
* `second` is a source of state. It will inherit the value of `prop` in
|
||||
* the first render, but after that it will no longer be updated when
|
||||
* `prop` changes. So we shouldn't consider `second` as being derived from
|
||||
* `prop`
|
||||
*/
|
||||
useEffect(() => {
|
||||
setS(second);
|
||||
}, [second]);
|
||||
|
||||
return <div>{s}</div>;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(5);
|
||||
const { prop } = t0;
|
||||
const [s, setS] = useState();
|
||||
const [second] = useState(prop);
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== second) {
|
||||
t1 = () => {
|
||||
setS(second);
|
||||
};
|
||||
t2 = [second];
|
||||
$[0] = second;
|
||||
$[1] = t1;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
t2 = $[2];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[3] !== s) {
|
||||
t3 = <div>{s}</div>;
|
||||
$[3] = s;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```
|
||||
{"kind":"CompileSuccess","fnLoc":{"start":{"line":3,"column":0,"index":64},"end":{"line":18,"column":1,"index":500},"filename":"usestate-derived-from-prop-no-show-in-data-flow-tree.ts"},"fnName":"Component","memoSlots":5,"memoBlocks":2,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -0,0 +1,18 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
|
||||
function Component({prop}) {
|
||||
const [s, setS] = useState();
|
||||
const [second, setSecond] = useState(prop);
|
||||
|
||||
/*
|
||||
* `second` is a source of state. It will inherit the value of `prop` in
|
||||
* the first render, but after that it will no longer be updated when
|
||||
* `prop` changes. So we shouldn't consider `second` as being derived from
|
||||
* `prop`
|
||||
*/
|
||||
useEffect(() => {
|
||||
setS(second);
|
||||
}, [second]);
|
||||
|
||||
return <div>{s}</div>;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableInferEventHandlers
|
||||
import {useRef} from 'react';
|
||||
|
||||
// Simulates a custom component wrapper
|
||||
function CustomForm({onSubmit, children}: any) {
|
||||
return <form onSubmit={onSubmit}>{children}</form>;
|
||||
}
|
||||
|
||||
// Simulates react-hook-form's handleSubmit
|
||||
function handleSubmit<T>(callback: (data: T) => void) {
|
||||
return (event: any) => {
|
||||
event.preventDefault();
|
||||
callback({} as T);
|
||||
};
|
||||
}
|
||||
|
||||
function Component() {
|
||||
const ref = useRef<HTMLInputElement>(null);
|
||||
|
||||
const onSubmit = (data: any) => {
|
||||
// This should error: passing function with ref access to custom component
|
||||
// event handler, even though it would be safe on a native <form>
|
||||
if (ref.current !== null) {
|
||||
console.log(ref.current.value);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<input ref={ref} />
|
||||
<CustomForm onSubmit={handleSubmit(onSubmit)}>
|
||||
<button type="submit">Submit</button>
|
||||
</CustomForm>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Cannot access refs during render
|
||||
|
||||
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
|
||||
|
||||
error.ref-value-in-custom-component-event-handler-wrapper.ts:31:41
|
||||
29 | <>
|
||||
30 | <input ref={ref} />
|
||||
> 31 | <CustomForm onSubmit={handleSubmit(onSubmit)}>
|
||||
| ^^^^^^^^ Passing a ref to a function may read its value during render
|
||||
32 | <button type="submit">Submit</button>
|
||||
33 | </CustomForm>
|
||||
34 | </>
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
// @enableInferEventHandlers
|
||||
import {useRef} from 'react';
|
||||
|
||||
// Simulates a custom component wrapper
|
||||
function CustomForm({onSubmit, children}: any) {
|
||||
return <form onSubmit={onSubmit}>{children}</form>;
|
||||
}
|
||||
|
||||
// Simulates react-hook-form's handleSubmit
|
||||
function handleSubmit<T>(callback: (data: T) => void) {
|
||||
return (event: any) => {
|
||||
event.preventDefault();
|
||||
callback({} as T);
|
||||
};
|
||||
}
|
||||
|
||||
function Component() {
|
||||
const ref = useRef<HTMLInputElement>(null);
|
||||
|
||||
const onSubmit = (data: any) => {
|
||||
// This should error: passing function with ref access to custom component
|
||||
// event handler, even though it would be safe on a native <form>
|
||||
if (ref.current !== null) {
|
||||
console.log(ref.current.value);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<input ref={ref} />
|
||||
<CustomForm onSubmit={handleSubmit(onSubmit)}>
|
||||
<button type="submit">Submit</button>
|
||||
</CustomForm>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{}],
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableInferEventHandlers
|
||||
import {useRef} from 'react';
|
||||
|
||||
// Simulates a handler wrapper
|
||||
function handleClick(value: any) {
|
||||
return () => {
|
||||
console.log(value);
|
||||
};
|
||||
}
|
||||
|
||||
function Component() {
|
||||
const ref = useRef(null);
|
||||
|
||||
// This should still error: passing ref.current directly to a wrapper
|
||||
// The ref value is accessed during render, not in the event handler
|
||||
return (
|
||||
<>
|
||||
<input ref={ref} />
|
||||
<button onClick={handleClick(ref.current)}>Click</button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Cannot access refs during render
|
||||
|
||||
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
|
||||
|
||||
error.ref-value-in-event-handler-wrapper.ts:19:35
|
||||
17 | <>
|
||||
18 | <input ref={ref} />
|
||||
> 19 | <button onClick={handleClick(ref.current)}>Click</button>
|
||||
| ^^^^^^^^^^^ Cannot access ref value during render
|
||||
20 | </>
|
||||
21 | );
|
||||
22 | }
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
// @enableInferEventHandlers
|
||||
import {useRef} from 'react';
|
||||
|
||||
// Simulates a handler wrapper
|
||||
function handleClick(value: any) {
|
||||
return () => {
|
||||
console.log(value);
|
||||
};
|
||||
}
|
||||
|
||||
function Component() {
|
||||
const ref = useRef(null);
|
||||
|
||||
// This should still error: passing ref.current directly to a wrapper
|
||||
// The ref value is accessed during render, not in the event handler
|
||||
return (
|
||||
<>
|
||||
<input ref={ref} />
|
||||
<button onClick={handleClick(ref.current)}>Click</button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{}],
|
||||
};
|
||||
@@ -0,0 +1,224 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateSourceLocations
|
||||
import {useEffect, useCallback} from 'react';
|
||||
|
||||
function Component({prop1, prop2}) {
|
||||
const x = prop1 + prop2;
|
||||
const y = x * 2;
|
||||
const arr = [x, y];
|
||||
const obj = {x, y};
|
||||
const [a, b] = arr;
|
||||
const {x: c, y: d} = obj;
|
||||
|
||||
useEffect(() => {
|
||||
if (a > 10) {
|
||||
console.log(a);
|
||||
}
|
||||
}, [a]);
|
||||
|
||||
const foo = useCallback(() => {
|
||||
return a + b;
|
||||
}, [a, b]);
|
||||
|
||||
function bar() {
|
||||
return (c + d) * 2;
|
||||
}
|
||||
|
||||
console.log('Hello, world!');
|
||||
|
||||
return [y, foo, bar];
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 13 errors:
|
||||
|
||||
Todo: Important source location missing in generated code
|
||||
|
||||
Source location for VariableDeclarator is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
|
||||
|
||||
error.todo-missing-source-locations.ts:5:8
|
||||
3 |
|
||||
4 | function Component({prop1, prop2}) {
|
||||
> 5 | const x = prop1 + prop2;
|
||||
| ^^^^^^^^^^^^^^^^^
|
||||
6 | const y = x * 2;
|
||||
7 | const arr = [x, y];
|
||||
8 | const obj = {x, y};
|
||||
|
||||
Todo: Important source location missing in generated code
|
||||
|
||||
Source location for VariableDeclarator is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
|
||||
|
||||
error.todo-missing-source-locations.ts:6:8
|
||||
4 | function Component({prop1, prop2}) {
|
||||
5 | const x = prop1 + prop2;
|
||||
> 6 | const y = x * 2;
|
||||
| ^^^^^^^^^
|
||||
7 | const arr = [x, y];
|
||||
8 | const obj = {x, y};
|
||||
9 | const [a, b] = arr;
|
||||
|
||||
Todo: Important source location missing in generated code
|
||||
|
||||
Source location for VariableDeclarator is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
|
||||
|
||||
error.todo-missing-source-locations.ts:7:8
|
||||
5 | const x = prop1 + prop2;
|
||||
6 | const y = x * 2;
|
||||
> 7 | const arr = [x, y];
|
||||
| ^^^^^^^^^^^^
|
||||
8 | const obj = {x, y};
|
||||
9 | const [a, b] = arr;
|
||||
10 | const {x: c, y: d} = obj;
|
||||
|
||||
Todo: Important source location missing in generated code
|
||||
|
||||
Source location for VariableDeclarator is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
|
||||
|
||||
error.todo-missing-source-locations.ts:8:8
|
||||
6 | const y = x * 2;
|
||||
7 | const arr = [x, y];
|
||||
> 8 | const obj = {x, y};
|
||||
| ^^^^^^^^^^^^
|
||||
9 | const [a, b] = arr;
|
||||
10 | const {x: c, y: d} = obj;
|
||||
11 |
|
||||
|
||||
Todo: Important source location missing in generated code
|
||||
|
||||
Source location for VariableDeclarator is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
|
||||
|
||||
error.todo-missing-source-locations.ts:9:8
|
||||
7 | const arr = [x, y];
|
||||
8 | const obj = {x, y};
|
||||
> 9 | const [a, b] = arr;
|
||||
| ^^^^^^^^^^^^
|
||||
10 | const {x: c, y: d} = obj;
|
||||
11 |
|
||||
12 | useEffect(() => {
|
||||
|
||||
Todo: Important source location missing in generated code
|
||||
|
||||
Source location for VariableDeclarator is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
|
||||
|
||||
error.todo-missing-source-locations.ts:10:8
|
||||
8 | const obj = {x, y};
|
||||
9 | const [a, b] = arr;
|
||||
> 10 | const {x: c, y: d} = obj;
|
||||
| ^^^^^^^^^^^^^^^^^^
|
||||
11 |
|
||||
12 | useEffect(() => {
|
||||
13 | if (a > 10) {
|
||||
|
||||
Todo: Important source location missing in generated code
|
||||
|
||||
Source location for ExpressionStatement is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
|
||||
|
||||
error.todo-missing-source-locations.ts:12:2
|
||||
10 | const {x: c, y: d} = obj;
|
||||
11 |
|
||||
> 12 | useEffect(() => {
|
||||
| ^^^^^^^^^^^^^^^^^
|
||||
> 13 | if (a > 10) {
|
||||
| ^^^^^^^^^^^^^^^^^
|
||||
> 14 | console.log(a);
|
||||
| ^^^^^^^^^^^^^^^^^
|
||||
> 15 | }
|
||||
| ^^^^^^^^^^^^^^^^^
|
||||
> 16 | }, [a]);
|
||||
| ^^^^^^^^^^^
|
||||
17 |
|
||||
18 | const foo = useCallback(() => {
|
||||
19 | return a + b;
|
||||
|
||||
Todo: Important source location missing in generated code
|
||||
|
||||
Source location for ExpressionStatement is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
|
||||
|
||||
error.todo-missing-source-locations.ts:14:6
|
||||
12 | useEffect(() => {
|
||||
13 | if (a > 10) {
|
||||
> 14 | console.log(a);
|
||||
| ^^^^^^^^^^^^^^^
|
||||
15 | }
|
||||
16 | }, [a]);
|
||||
17 |
|
||||
|
||||
Todo: Important source location missing in generated code
|
||||
|
||||
Source location for VariableDeclarator is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
|
||||
|
||||
error.todo-missing-source-locations.ts:18:8
|
||||
16 | }, [a]);
|
||||
17 |
|
||||
> 18 | const foo = useCallback(() => {
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 19 | return a + b;
|
||||
| ^^^^^^^^^^^^^^^^^
|
||||
> 20 | }, [a, b]);
|
||||
| ^^^^^^^^^^^^^
|
||||
21 |
|
||||
22 | function bar() {
|
||||
23 | return (c + d) * 2;
|
||||
|
||||
Todo: Important source location missing in generated code
|
||||
|
||||
Source location for ReturnStatement is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
|
||||
|
||||
error.todo-missing-source-locations.ts:19:4
|
||||
17 |
|
||||
18 | const foo = useCallback(() => {
|
||||
> 19 | return a + b;
|
||||
| ^^^^^^^^^^^^^
|
||||
20 | }, [a, b]);
|
||||
21 |
|
||||
22 | function bar() {
|
||||
|
||||
Todo: Important source location missing in generated code
|
||||
|
||||
Source location for ReturnStatement is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
|
||||
|
||||
error.todo-missing-source-locations.ts:23:4
|
||||
21 |
|
||||
22 | function bar() {
|
||||
> 23 | return (c + d) * 2;
|
||||
| ^^^^^^^^^^^^^^^^^^^
|
||||
24 | }
|
||||
25 |
|
||||
26 | console.log('Hello, world!');
|
||||
|
||||
Todo: Important source location missing in generated code
|
||||
|
||||
Source location for ExpressionStatement is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
|
||||
|
||||
error.todo-missing-source-locations.ts:26:2
|
||||
24 | }
|
||||
25 |
|
||||
> 26 | console.log('Hello, world!');
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
27 |
|
||||
28 | return [y, foo, bar];
|
||||
29 | }
|
||||
|
||||
Todo: Important source location missing in generated code
|
||||
|
||||
Source location for ReturnStatement is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
|
||||
|
||||
error.todo-missing-source-locations.ts:28:2
|
||||
26 | console.log('Hello, world!');
|
||||
27 |
|
||||
> 28 | return [y, foo, bar];
|
||||
| ^^^^^^^^^^^^^^^^^^^^^
|
||||
29 | }
|
||||
30 |
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
// @validateSourceLocations
|
||||
import {useEffect, useCallback} from 'react';
|
||||
|
||||
function Component({prop1, prop2}) {
|
||||
const x = prop1 + prop2;
|
||||
const y = x * 2;
|
||||
const arr = [x, y];
|
||||
const obj = {x, y};
|
||||
const [a, b] = arr;
|
||||
const {x: c, y: d} = obj;
|
||||
|
||||
useEffect(() => {
|
||||
if (a > 10) {
|
||||
console.log(a);
|
||||
}
|
||||
}, [a]);
|
||||
|
||||
const foo = useCallback(() => {
|
||||
return a + b;
|
||||
}, [a, b]);
|
||||
|
||||
function bar() {
|
||||
return (c + d) * 2;
|
||||
}
|
||||
|
||||
console.log('Hello, world!');
|
||||
|
||||
return [y, foo, bar];
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
export function useFormatRelativeTime(opts = {}) {
|
||||
const {timeZone, minimal} = opts;
|
||||
const format = useCallback(function formatWithUnit() {}, [minimal]);
|
||||
// We previously recorded `{timeZone}` as capturing timeZone into the object,
|
||||
// then assumed that dateTimeFormat() mutates that object,
|
||||
// which in turn could mutate timeZone and the object it came from,
|
||||
// which meanteans that the value `minimal` is derived from can change.
|
||||
//
|
||||
// The fix was to record a Capture from a maybefrozen value as an ImmutableCapture
|
||||
// which doesn't propagate mutations
|
||||
dateTimeFormat({timeZone});
|
||||
return format;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
export function useFormatRelativeTime(t0) {
|
||||
const $ = _c(1);
|
||||
const opts = t0 === undefined ? {} : t0;
|
||||
const { timeZone, minimal } = opts;
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = function formatWithUnit() {};
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
}
|
||||
const format = t1;
|
||||
|
||||
dateTimeFormat({ timeZone });
|
||||
return format;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -0,0 +1,13 @@
|
||||
export function useFormatRelativeTime(opts = {}) {
|
||||
const {timeZone, minimal} = opts;
|
||||
const format = useCallback(function formatWithUnit() {}, [minimal]);
|
||||
// We previously recorded `{timeZone}` as capturing timeZone into the object,
|
||||
// then assumed that dateTimeFormat() mutates that object,
|
||||
// which in turn could mutate timeZone and the object it came from,
|
||||
// which meanteans that the value `minimal` is derived from can change.
|
||||
//
|
||||
// The fix was to record a Capture from a maybefrozen value as an ImmutableCapture
|
||||
// which doesn't propagate mutations
|
||||
dateTimeFormat({timeZone});
|
||||
return format;
|
||||
}
|
||||
164
packages/react-client/src/ReactFlightClient.js
vendored
164
packages/react-client/src/ReactFlightClient.js
vendored
@@ -39,12 +39,9 @@ import type {
|
||||
EncodeFormActionCallback,
|
||||
} from './ReactFlightReplyClient';
|
||||
|
||||
import type {Postpone} from 'react/src/ReactPostpone';
|
||||
|
||||
import type {TemporaryReferenceSet} from './ReactFlightTemporaryReferences';
|
||||
|
||||
import {
|
||||
enablePostpone,
|
||||
enableProfilerTimer,
|
||||
enableComponentPerformanceTrack,
|
||||
enableAsyncDebugInfo,
|
||||
@@ -89,7 +86,6 @@ import {
|
||||
import {
|
||||
REACT_LAZY_TYPE,
|
||||
REACT_ELEMENT_TYPE,
|
||||
REACT_POSTPONE_TYPE,
|
||||
ASYNC_ITERATOR,
|
||||
REACT_FRAGMENT_TYPE,
|
||||
} from 'shared/ReactSymbols';
|
||||
@@ -3460,88 +3456,6 @@ function resolveErrorDev(
|
||||
return error;
|
||||
}
|
||||
|
||||
function resolvePostponeProd(
|
||||
response: Response,
|
||||
id: number,
|
||||
streamState: StreamState,
|
||||
): void {
|
||||
if (__DEV__) {
|
||||
// These errors should never make it into a build so we don't need to encode them in codes.json
|
||||
// eslint-disable-next-line react-internal/prod-error-codes
|
||||
throw new Error(
|
||||
'resolvePostponeProd should never be called in development mode. Use resolvePostponeDev instead. This is a bug in React.',
|
||||
);
|
||||
}
|
||||
const error = new Error(
|
||||
'A Server Component was postponed. The reason is omitted in production' +
|
||||
' builds to avoid leaking sensitive details.',
|
||||
);
|
||||
const postponeInstance: Postpone = (error: any);
|
||||
postponeInstance.$$typeof = REACT_POSTPONE_TYPE;
|
||||
postponeInstance.stack = 'Error: ' + error.message;
|
||||
const chunks = response._chunks;
|
||||
const chunk = chunks.get(id);
|
||||
if (!chunk) {
|
||||
const newChunk: ErroredChunk<any> = createErrorChunk(
|
||||
response,
|
||||
postponeInstance,
|
||||
);
|
||||
chunks.set(id, newChunk);
|
||||
} else {
|
||||
triggerErrorOnChunk(response, chunk, postponeInstance);
|
||||
}
|
||||
}
|
||||
|
||||
function resolvePostponeDev(
|
||||
response: Response,
|
||||
id: number,
|
||||
reason: string,
|
||||
stack: ReactStackTrace,
|
||||
env: string,
|
||||
streamState: StreamState,
|
||||
): void {
|
||||
if (!__DEV__) {
|
||||
// These errors should never make it into a build so we don't need to encode them in codes.json
|
||||
// eslint-disable-next-line react-internal/prod-error-codes
|
||||
throw new Error(
|
||||
'resolvePostponeDev should never be called in production mode. Use resolvePostponeProd instead. This is a bug in React.',
|
||||
);
|
||||
}
|
||||
let postponeInstance: Postpone;
|
||||
const callStack = buildFakeCallStack(
|
||||
response,
|
||||
stack,
|
||||
env,
|
||||
false,
|
||||
// $FlowFixMe[incompatible-use]
|
||||
Error.bind(null, reason || ''),
|
||||
);
|
||||
const rootTask = response._debugRootTask;
|
||||
if (rootTask != null) {
|
||||
postponeInstance = rootTask.run(callStack);
|
||||
} else {
|
||||
postponeInstance = callStack();
|
||||
}
|
||||
postponeInstance.$$typeof = REACT_POSTPONE_TYPE;
|
||||
const chunks = response._chunks;
|
||||
const chunk = chunks.get(id);
|
||||
if (!chunk) {
|
||||
const newChunk: ErroredChunk<any> = createErrorChunk(
|
||||
response,
|
||||
postponeInstance,
|
||||
);
|
||||
if (__DEV__) {
|
||||
resolveChunkDebugInfo(response, streamState, newChunk);
|
||||
}
|
||||
chunks.set(id, newChunk);
|
||||
} else {
|
||||
if (__DEV__) {
|
||||
resolveChunkDebugInfo(response, streamState, chunk);
|
||||
}
|
||||
triggerErrorOnChunk(response, chunk, postponeInstance);
|
||||
}
|
||||
}
|
||||
|
||||
function resolveErrorModel(
|
||||
response: Response,
|
||||
id: number,
|
||||
@@ -4893,25 +4807,6 @@ function processFullStringRow(
|
||||
return;
|
||||
}
|
||||
// Fallthrough
|
||||
case 80 /* "P" */: {
|
||||
if (enablePostpone) {
|
||||
if (__DEV__) {
|
||||
const postponeInfo = JSON.parse(row);
|
||||
resolvePostponeDev(
|
||||
response,
|
||||
id,
|
||||
postponeInfo.reason,
|
||||
postponeInfo.stack,
|
||||
postponeInfo.env,
|
||||
streamState,
|
||||
);
|
||||
} else {
|
||||
resolvePostponeProd(response, id, streamState);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Fallthrough
|
||||
default: /* """ "{" "[" "t" "f" "n" "0" - "9" */ {
|
||||
if (__DEV__ && row === '') {
|
||||
resolveDebugHalt(response, id);
|
||||
@@ -4962,6 +4857,7 @@ export function processBinaryChunk(
|
||||
resolvedRowTag === 65 /* "A" */ ||
|
||||
resolvedRowTag === 79 /* "O" */ ||
|
||||
resolvedRowTag === 111 /* "o" */ ||
|
||||
resolvedRowTag === 98 /* "b" */ ||
|
||||
resolvedRowTag === 85 /* "U" */ ||
|
||||
resolvedRowTag === 83 /* "S" */ ||
|
||||
resolvedRowTag === 115 /* "s" */ ||
|
||||
@@ -5021,14 +4917,31 @@ export function processBinaryChunk(
|
||||
// We found the last chunk of the row
|
||||
const length = lastIdx - i;
|
||||
const lastChunk = new Uint8Array(chunk.buffer, offset, length);
|
||||
processFullBinaryRow(
|
||||
response,
|
||||
streamState,
|
||||
rowID,
|
||||
rowTag,
|
||||
buffer,
|
||||
lastChunk,
|
||||
);
|
||||
|
||||
// Check if this is a Uint8Array for a byte stream. We enqueue it
|
||||
// immediately but need to determine if we can use zero-copy or must copy.
|
||||
if (rowTag === 98 /* "b" */) {
|
||||
resolveBuffer(
|
||||
response,
|
||||
rowID,
|
||||
// If we're at the end of the RSC chunk, no more parsing will access
|
||||
// this buffer and we don't need to copy the chunk to allow detaching
|
||||
// the buffer, otherwise we need to copy.
|
||||
lastIdx === chunkLength ? lastChunk : lastChunk.slice(),
|
||||
streamState,
|
||||
);
|
||||
} else {
|
||||
// Process all other row types.
|
||||
processFullBinaryRow(
|
||||
response,
|
||||
streamState,
|
||||
rowID,
|
||||
rowTag,
|
||||
buffer,
|
||||
lastChunk,
|
||||
);
|
||||
}
|
||||
|
||||
// Reset state machine for a new row
|
||||
i = lastIdx;
|
||||
if (rowState === ROW_CHUNK_BY_NEWLINE) {
|
||||
@@ -5041,14 +4954,27 @@ export function processBinaryChunk(
|
||||
rowLength = 0;
|
||||
buffer.length = 0;
|
||||
} else {
|
||||
// The rest of this row is in a future chunk. We stash the rest of the
|
||||
// current chunk until we can process the full row.
|
||||
// The rest of this row is in a future chunk.
|
||||
const length = chunk.byteLength - i;
|
||||
const remainingSlice = new Uint8Array(chunk.buffer, offset, length);
|
||||
buffer.push(remainingSlice);
|
||||
// Update how many bytes we're still waiting for. If we're looking for
|
||||
// a newline, this doesn't hurt since we'll just ignore it.
|
||||
rowLength -= remainingSlice.byteLength;
|
||||
|
||||
// For byte streams, we can enqueue the partial row immediately without
|
||||
// copying since we're at the end of the RSC chunk and no more parsing
|
||||
// will access this buffer.
|
||||
if (rowTag === 98 /* "b" */) {
|
||||
// Update how many bytes we're still waiting for. We need to do this
|
||||
// before enqueueing, as enqueue will detach the buffer and byteLength
|
||||
// will become 0.
|
||||
rowLength -= remainingSlice.byteLength;
|
||||
resolveBuffer(response, rowID, remainingSlice, streamState);
|
||||
} else {
|
||||
// For other row types, stash the rest of the current chunk until we can
|
||||
// process the full row.
|
||||
buffer.push(remainingSlice);
|
||||
// Update how many bytes we're still waiting for. If we're looking for
|
||||
// a newline, this doesn't hurt since we'll just ignore it.
|
||||
rowLength -= remainingSlice.byteLength;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3283,8 +3283,6 @@ describe('Store', () => {
|
||||
<Suspense name="Outer" rects={null}>
|
||||
`);
|
||||
|
||||
console.log('...........................');
|
||||
|
||||
await actAsync(() => {
|
||||
resolve('loaded');
|
||||
});
|
||||
@@ -3300,4 +3298,100 @@ describe('Store', () => {
|
||||
<Suspense name="Inner" rects={[{x:1,y:2,width:6,height:1}]}>
|
||||
`);
|
||||
});
|
||||
|
||||
// @reactVersion >= 19.0
|
||||
it('measures rects when reconnecting', async () => {
|
||||
function Component({children, promise}) {
|
||||
let content = '';
|
||||
if (promise) {
|
||||
const value = readValue(promise);
|
||||
if (typeof value === 'string') {
|
||||
content += value;
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
{content}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function App({outer, inner}) {
|
||||
return (
|
||||
<React.Suspense
|
||||
name="outer"
|
||||
fallback={<Component key="outer-fallback">loading outer</Component>}>
|
||||
<Component key="outer-content" promise={outer}>
|
||||
outer content
|
||||
</Component>
|
||||
<React.Suspense
|
||||
name="inner"
|
||||
fallback={
|
||||
<Component key="inner-fallback">loading inner</Component>
|
||||
}>
|
||||
<Component key="inner-content" promise={inner}>
|
||||
inner content
|
||||
</Component>
|
||||
</React.Suspense>
|
||||
</React.Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
await actAsync(() => {
|
||||
render(<App outer={null} inner={null} />);
|
||||
});
|
||||
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <App>
|
||||
▾ <Suspense name="outer">
|
||||
<Component key="outer-content">
|
||||
▾ <Suspense name="inner">
|
||||
<Component key="inner-content">
|
||||
[suspense-root] rects={[{x:1,y:2,width:13,height:1}, {x:1,y:2,width:13,height:1}]}
|
||||
<Suspense name="outer" rects={[{x:1,y:2,width:13,height:1}, {x:1,y:2,width:13,height:1}]}>
|
||||
<Suspense name="inner" rects={[{x:1,y:2,width:13,height:1}]}>
|
||||
`);
|
||||
|
||||
let outerResolve;
|
||||
const outerPromise = new Promise(resolve => {
|
||||
outerResolve = resolve;
|
||||
});
|
||||
|
||||
let innerResolve;
|
||||
const innerPromise = new Promise(resolve => {
|
||||
innerResolve = resolve;
|
||||
});
|
||||
await actAsync(() => {
|
||||
render(<App outer={outerPromise} inner={innerPromise} />);
|
||||
});
|
||||
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <App>
|
||||
▾ <Suspense name="outer">
|
||||
<Component key="outer-fallback">
|
||||
[suspense-root] rects={[{x:1,y:2,width:13,height:1}, {x:1,y:2,width:13,height:1}, {x:1,y:2,width:13,height:1}]}
|
||||
<Suspense name="outer" rects={[{x:1,y:2,width:13,height:1}, {x:1,y:2,width:13,height:1}]}>
|
||||
<Suspense name="inner" rects={[{x:1,y:2,width:13,height:1}]}>
|
||||
`);
|
||||
|
||||
await actAsync(() => {
|
||||
outerResolve('..');
|
||||
innerResolve('.');
|
||||
});
|
||||
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <App>
|
||||
▾ <Suspense name="outer">
|
||||
<Component key="outer-content">
|
||||
▾ <Suspense name="inner">
|
||||
<Component key="inner-content">
|
||||
[suspense-root] rects={[{x:1,y:2,width:15,height:1}, {x:1,y:2,width:14,height:1}]}
|
||||
<Suspense name="outer" rects={[{x:1,y:2,width:15,height:1}, {x:1,y:2,width:14,height:1}]}>
|
||||
<Suspense name="inner" rects={[{x:1,y:2,width:14,height:1}]}>
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,6 +24,16 @@ describe('Store component filters', () => {
|
||||
let utils;
|
||||
let actAsync;
|
||||
|
||||
beforeAll(() => {
|
||||
// JSDDOM doesn't implement getClientRects so we're just faking one for testing purposes
|
||||
Element.prototype.getClientRects = function (this: Element) {
|
||||
const textContent = this.textContent;
|
||||
return [
|
||||
new DOMRect(1, 2, textContent.length, textContent.split('\n').length),
|
||||
];
|
||||
};
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
agent = global.agent;
|
||||
bridge = global.bridge;
|
||||
@@ -158,9 +168,9 @@ describe('Store component filters', () => {
|
||||
<div>
|
||||
▾ <Suspense>
|
||||
<div>
|
||||
[suspense-root] rects={[]}
|
||||
<Suspense name="Unknown" rects={[]}>
|
||||
<Suspense name="Unknown" rects={[]}>
|
||||
[suspense-root] rects={[{x:1,y:2,width:7,height:1}, {x:1,y:2,width:6,height:1}]}
|
||||
<Suspense name="Unknown" rects={[{x:1,y:2,width:7,height:1}]}>
|
||||
<Suspense name="Unknown" rects={[{x:1,y:2,width:6,height:1}]}>
|
||||
`);
|
||||
|
||||
await actAsync(
|
||||
@@ -176,9 +186,9 @@ describe('Store component filters', () => {
|
||||
<div>
|
||||
▾ <Suspense>
|
||||
<div>
|
||||
[suspense-root] rects={[]}
|
||||
<Suspense name="Unknown" rects={[]}>
|
||||
<Suspense name="Unknown" rects={[]}>
|
||||
[suspense-root] rects={[{x:1,y:2,width:7,height:1}, {x:1,y:2,width:6,height:1}]}
|
||||
<Suspense name="Unknown" rects={[{x:1,y:2,width:7,height:1}]}>
|
||||
<Suspense name="Unknown" rects={[{x:1,y:2,width:6,height:1}]}>
|
||||
`);
|
||||
|
||||
await actAsync(
|
||||
@@ -194,9 +204,9 @@ describe('Store component filters', () => {
|
||||
<div>
|
||||
▾ <Suspense>
|
||||
<div>
|
||||
[suspense-root] rects={[]}
|
||||
<Suspense name="Unknown" rects={[]}>
|
||||
<Suspense name="Unknown" rects={[]}>
|
||||
[suspense-root] rects={[{x:1,y:2,width:7,height:1}, {x:1,y:2,width:6,height:1}]}
|
||||
<Suspense name="Unknown" rects={[{x:1,y:2,width:7,height:1}]}>
|
||||
<Suspense name="Unknown" rects={[{x:1,y:2,width:6,height:1}]}>
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -798,8 +808,8 @@ describe('Store component filters', () => {
|
||||
<div key="loading">
|
||||
▾ <ErrorBoundary>
|
||||
<div key="did-error">
|
||||
[suspense-root] rects={[]}
|
||||
<Suspense name="App" rects={[]}>
|
||||
[suspense-root] rects={[{x:1,y:2,width:0,height:1}, {x:1,y:2,width:0,height:1}, {x:1,y:2,width:0,height:1}]}
|
||||
<Suspense name="App" rects={[{x:1,y:2,width:0,height:1}]}>
|
||||
`);
|
||||
|
||||
await actAsync(() => {
|
||||
@@ -814,8 +824,108 @@ describe('Store component filters', () => {
|
||||
<div key="suspense-content">
|
||||
▾ <ErrorBoundary>
|
||||
<div key="error-content">
|
||||
[suspense-root] rects={[]}
|
||||
<Suspense name="Unknown" rects={[]}>
|
||||
[suspense-root] rects={[{x:1,y:2,width:0,height:1}, {x:1,y:2,width:0,height:1}]}
|
||||
<Suspense name="Unknown" rects={[{x:1,y:2,width:0,height:1}]}>
|
||||
`);
|
||||
});
|
||||
|
||||
// @reactVersion >= 19.2
|
||||
it('can filter by Activity slices', async () => {
|
||||
const Activity = React.Activity;
|
||||
const immediate = Promise.resolve(<div>Immediate</div>);
|
||||
|
||||
function Root({children}) {
|
||||
return (
|
||||
<Activity name="/" mode="visible">
|
||||
<React.Suspense fallback="Loading...">
|
||||
<h1>Root</h1>
|
||||
<main>{children}</main>
|
||||
</React.Suspense>
|
||||
</Activity>
|
||||
);
|
||||
}
|
||||
|
||||
function Layout({children}) {
|
||||
return (
|
||||
<Activity name="/blog" mode="visible">
|
||||
<h2>Blog</h2>
|
||||
<section>{children}</section>
|
||||
</Activity>
|
||||
);
|
||||
}
|
||||
|
||||
function Page() {
|
||||
return <React.Suspense fallback="Loading...">{immediate}</React.Suspense>;
|
||||
}
|
||||
|
||||
await actAsync(async () =>
|
||||
render(
|
||||
<Root>
|
||||
<Layout>
|
||||
<Page />
|
||||
</Layout>
|
||||
</Root>,
|
||||
),
|
||||
);
|
||||
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <Root>
|
||||
▾ <Activity name="/">
|
||||
▾ <Suspense>
|
||||
<h1>
|
||||
▾ <main>
|
||||
▾ <Layout>
|
||||
▾ <Activity name="/blog">
|
||||
<h2>
|
||||
▾ <section>
|
||||
▾ <Page>
|
||||
▾ <Suspense>
|
||||
<div>
|
||||
[suspense-root] rects={[{x:1,y:2,width:4,height:1}, {x:1,y:2,width:13,height:1}]}
|
||||
<Suspense name="Root" rects={[{x:1,y:2,width:4,height:1}, {x:1,y:2,width:13,height:1}]}>
|
||||
<Suspense name="Page" rects={[{x:1,y:2,width:9,height:1}]}>
|
||||
`);
|
||||
|
||||
await actAsync(
|
||||
async () =>
|
||||
(store.componentFilters = [
|
||||
utils.createActivitySliceFilter(store.getElementIDAtIndex(1)),
|
||||
]),
|
||||
);
|
||||
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <Activity name="/">
|
||||
▾ <Suspense>
|
||||
<h1>
|
||||
▾ <main>
|
||||
▾ <Layout>
|
||||
▸ <Activity name="/blog">
|
||||
[suspense-root] rects={[{x:1,y:2,width:4,height:1}, {x:1,y:2,width:13,height:1}]}
|
||||
<Suspense name="Unknown" rects={[{x:1,y:2,width:4,height:1}, {x:1,y:2,width:13,height:1}]}>
|
||||
<Suspense name="Page" rects={[{x:1,y:2,width:9,height:1}]}>
|
||||
`);
|
||||
|
||||
await actAsync(async () => (store.componentFilters = []));
|
||||
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <Root>
|
||||
▾ <Activity name="/">
|
||||
▾ <Suspense>
|
||||
<h1>
|
||||
▾ <main>
|
||||
▾ <Layout>
|
||||
▾ <Activity name="/blog">
|
||||
<h2>
|
||||
▾ <section>
|
||||
▾ <Page>
|
||||
▾ <Suspense>
|
||||
<div>
|
||||
[suspense-root] rects={[{x:1,y:2,width:4,height:1}, {x:1,y:2,width:13,height:1}]}
|
||||
<Suspense name="Root" rects={[{x:1,y:2,width:4,height:1}, {x:1,y:2,width:13,height:1}]}>
|
||||
<Suspense name="Page" rects={[{x:1,y:2,width:9,height:1}]}>
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -328,6 +328,19 @@ export function createLocationFilter(
|
||||
};
|
||||
}
|
||||
|
||||
export function createActivitySliceFilter(
|
||||
activityID: Element['id'],
|
||||
isEnabled: boolean = true,
|
||||
) {
|
||||
const Types = require('react-devtools-shared/src/frontend/types');
|
||||
return {
|
||||
type: Types.ComponentFilterActivitySlice,
|
||||
isEnabled,
|
||||
isValid: true,
|
||||
activityID: activityID,
|
||||
};
|
||||
}
|
||||
|
||||
export function getRendererID(): number {
|
||||
if (global.agent == null) {
|
||||
throw Error('Agent unavailable.');
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
ComponentFilterHOC,
|
||||
ComponentFilterLocation,
|
||||
ComponentFilterEnvironmentName,
|
||||
ComponentFilterActivitySlice,
|
||||
ElementTypeClass,
|
||||
ElementTypeContext,
|
||||
ElementTypeFunction,
|
||||
@@ -53,7 +54,7 @@ import {
|
||||
renamePathInObject,
|
||||
setInObject,
|
||||
utfEncodeString,
|
||||
filterOutLocationComponentFilters,
|
||||
persistableComponentFilters,
|
||||
} from 'react-devtools-shared/src/utils';
|
||||
import {
|
||||
formatConsoleArgumentsToSingleString,
|
||||
@@ -85,6 +86,7 @@ import {
|
||||
TREE_OPERATION_SET_SUBTREE_MODE,
|
||||
TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS,
|
||||
TREE_OPERATION_UPDATE_TREE_BASE_DURATION,
|
||||
TREE_OPERATION_APPLIED_ACTIVITY_SLICE_CHANGE,
|
||||
SUSPENSE_TREE_OPERATION_ADD,
|
||||
SUSPENSE_TREE_OPERATION_REMOVE,
|
||||
SUSPENSE_TREE_OPERATION_REORDER_CHILDREN,
|
||||
@@ -170,6 +172,7 @@ import type {
|
||||
} from '../types';
|
||||
import type {
|
||||
ComponentFilter,
|
||||
ActivitySliceFilter,
|
||||
ElementType,
|
||||
Plugins,
|
||||
} from 'react-devtools-shared/src/frontend/types';
|
||||
@@ -868,6 +871,9 @@ const idToDevToolsInstanceMap: Map<
|
||||
FiberInstance | VirtualInstance,
|
||||
> = new Map();
|
||||
|
||||
let focusedActivityID: null | FiberInstance['id'] = null;
|
||||
let focusedActivity: null | Fiber = null;
|
||||
|
||||
const idToSuspenseNodeMap: Map<FiberInstance['id'], SuspenseNode> = new Map();
|
||||
|
||||
// Map of canonical HostInstances to the nearest parent DevToolsInstance.
|
||||
@@ -1435,16 +1441,25 @@ export function attach(
|
||||
const hideElementsWithPaths: Set<RegExp> = new Set();
|
||||
const hideElementsWithTypes: Set<ElementType> = new Set();
|
||||
const hideElementsWithEnvs: Set<string> = new Set();
|
||||
let isInFocusedActivity: boolean = true;
|
||||
|
||||
// Highlight updates
|
||||
let traceUpdatesEnabled: boolean = false;
|
||||
const traceUpdatesForNodes: Set<HostInstance> = new Set();
|
||||
|
||||
function applyComponentFilters(componentFilters: Array<ComponentFilter>) {
|
||||
function applyComponentFilters(
|
||||
componentFilters: Array<ComponentFilter>,
|
||||
nextActivitySlice: null | Fiber,
|
||||
) {
|
||||
hideElementsWithTypes.clear();
|
||||
hideElementsWithDisplayNames.clear();
|
||||
hideElementsWithPaths.clear();
|
||||
hideElementsWithEnvs.clear();
|
||||
const previousFocusedActivityID = focusedActivityID;
|
||||
focusedActivityID = null;
|
||||
focusedActivity = null;
|
||||
// Consider everything in the slice by default
|
||||
isInFocusedActivity = true;
|
||||
|
||||
componentFilters.forEach(componentFilter => {
|
||||
if (!componentFilter.isEnabled) {
|
||||
@@ -1473,6 +1488,25 @@ export function attach(
|
||||
case ComponentFilterEnvironmentName:
|
||||
hideElementsWithEnvs.add(componentFilter.value);
|
||||
break;
|
||||
case ComponentFilterActivitySlice:
|
||||
if (
|
||||
nextActivitySlice !== null &&
|
||||
nextActivitySlice.tag === ActivityComponent
|
||||
) {
|
||||
focusedActivity = nextActivitySlice;
|
||||
isInFocusedActivity = false;
|
||||
if (componentFilter.rendererID !== rendererID) {
|
||||
// We filtered an Activity from another renderer.
|
||||
// We need to restore the instance ID since we won't be mounting it
|
||||
// in this renderer.
|
||||
focusedActivityID = previousFocusedActivityID;
|
||||
}
|
||||
} else {
|
||||
// We're not filtering by activity slice after all.
|
||||
// Don't mark the filter as disabled here.
|
||||
// Otherwise updateComponentFilters() will think no enabled filter was changed.
|
||||
}
|
||||
break;
|
||||
default:
|
||||
console.warn(
|
||||
`Invalid component filter type "${componentFilter.type}"`,
|
||||
@@ -1486,11 +1520,9 @@ export function attach(
|
||||
// because they are stored in localStorage within the context of the extension.
|
||||
// Instead it relies on the extension to pass filters through.
|
||||
if (window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ != null) {
|
||||
const componentFiltersWithoutLocationBasedOnes =
|
||||
filterOutLocationComponentFilters(
|
||||
window.__REACT_DEVTOOLS_COMPONENT_FILTERS__,
|
||||
);
|
||||
applyComponentFilters(componentFiltersWithoutLocationBasedOnes);
|
||||
const restoredComponentFilters: Array<ComponentFilter> =
|
||||
persistableComponentFilters(window.__REACT_DEVTOOLS_COMPONENT_FILTERS__);
|
||||
applyComponentFilters(restoredComponentFilters, null);
|
||||
} else {
|
||||
// Unfortunately this feature is not expected to work for React Native for now.
|
||||
// It would be annoying for us to spam YellowBox warnings with unactionable stuff,
|
||||
@@ -1498,7 +1530,7 @@ export function attach(
|
||||
//console.warn('⚛ DevTools: Could not locate saved component filters');
|
||||
|
||||
// Fallback to assuming the default filters in this case.
|
||||
applyComponentFilters(getDefaultComponentFilters());
|
||||
applyComponentFilters(getDefaultComponentFilters(), null);
|
||||
}
|
||||
|
||||
// If necessary, we can revisit optimizing this operation.
|
||||
@@ -1517,6 +1549,22 @@ export function attach(
|
||||
const previousForcedErrors =
|
||||
forceErrorForFibers.size > 0 ? new Map(forceErrorForFibers) : null;
|
||||
|
||||
// The ID will be based on the old tree. We need to find the Fiber based on
|
||||
// that ID before we unmount everything. We set the activity slice ID once
|
||||
// we mount it again.
|
||||
let nextFocusedActivity: null | Fiber = null;
|
||||
let focusedActivityFilter: null | ActivitySliceFilter = null;
|
||||
for (let i = 0; i < componentFilters.length; i++) {
|
||||
const filter = componentFilters[i];
|
||||
if (filter.type === ComponentFilterActivitySlice && filter.isEnabled) {
|
||||
focusedActivityFilter = filter;
|
||||
const instance = idToDevToolsInstanceMap.get(filter.activityID);
|
||||
if (instance !== undefined && instance.kind === FIBER_INSTANCE) {
|
||||
nextFocusedActivity = instance.data;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively unmount all roots.
|
||||
hook.getFiberRoots(rendererID).forEach(root => {
|
||||
const rootInstance = rootToFiberInstanceMap.get(root);
|
||||
@@ -1528,11 +1576,20 @@ export function attach(
|
||||
currentRoot = rootInstance;
|
||||
unmountInstanceRecursively(rootInstance);
|
||||
rootToFiberInstanceMap.delete(root);
|
||||
flushPendingEvents();
|
||||
currentRoot = (null: any);
|
||||
});
|
||||
|
||||
applyComponentFilters(componentFilters);
|
||||
if (
|
||||
nextFocusedActivity !== focusedActivity &&
|
||||
(focusedActivityFilter === null ||
|
||||
focusedActivityFilter.rendererID === rendererID)
|
||||
) {
|
||||
// When we find the applied instance during mount we will send the actual ID.
|
||||
// Otherwise 0 will indicate that we unfocused the activity slice.
|
||||
pushOperation(TREE_OPERATION_APPLIED_ACTIVITY_SLICE_CHANGE);
|
||||
pushOperation(0);
|
||||
}
|
||||
applyComponentFilters(componentFilters, nextFocusedActivity);
|
||||
|
||||
// Reset pseudo counters so that new path selections will be persisted.
|
||||
rootDisplayNameCounter.clear();
|
||||
@@ -1588,10 +1645,16 @@ export function attach(
|
||||
currentRoot = newRoot;
|
||||
setRootPseudoKey(currentRoot.id, root.current);
|
||||
mountFiberRecursively(root.current, false);
|
||||
flushPendingEvents();
|
||||
currentRoot = (null: any);
|
||||
});
|
||||
|
||||
// We need to write back the new ID for the focused Fiber.
|
||||
// Otherwise subsequent filter applications will try to focus based on the old ID.
|
||||
// This is also relevant to filter across renderers.
|
||||
if (focusedActivityFilter !== null && focusedActivityID !== null) {
|
||||
focusedActivityFilter.activityID = focusedActivityID;
|
||||
}
|
||||
|
||||
flushPendingEvents();
|
||||
|
||||
needsToFlushComponentLogs = false;
|
||||
@@ -1621,6 +1684,10 @@ export function attach(
|
||||
data: ReactComponentInfo,
|
||||
secondaryEnv: null | string,
|
||||
): boolean {
|
||||
if (!isInFocusedActivity) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// For purposes of filtering Server Components are always Function Components.
|
||||
// Environment will be used to filter Server vs Client.
|
||||
// Technically they can be forwardRef and memo too but those filters will go away
|
||||
@@ -1656,6 +1723,11 @@ export function attach(
|
||||
function shouldFilterFiber(fiber: Fiber): boolean {
|
||||
const {tag, type, key} = fiber;
|
||||
|
||||
// It is never valid to filter the root element.
|
||||
if (tag !== HostRoot && !isInFocusedActivity) {
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (tag) {
|
||||
case DehydratedSuspenseComponent:
|
||||
// TODO: ideally we would show dehydrated Suspense immediately.
|
||||
@@ -2085,7 +2157,6 @@ export function attach(
|
||||
let pendingOperationsQueue: Array<OperationsArray> | null = [];
|
||||
const pendingStringTable: Map<string, StringTableEntry> = new Map();
|
||||
let pendingStringTableLength: number = 0;
|
||||
let pendingUnmountedRootID: FiberInstance['id'] | null = null;
|
||||
|
||||
function pushOperation(op: number): void {
|
||||
if (__DEV__) {
|
||||
@@ -2113,8 +2184,7 @@ export function attach(
|
||||
pendingOperations.length === 0 &&
|
||||
pendingRealUnmountedIDs.length === 0 &&
|
||||
pendingRealUnmountedSuspenseIDs.length === 0 &&
|
||||
pendingSuspenderChanges.size === 0 &&
|
||||
pendingUnmountedRootID === null
|
||||
pendingSuspenderChanges.size === 0
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2176,9 +2246,7 @@ export function attach(
|
||||
return;
|
||||
}
|
||||
|
||||
const numUnmountIDs =
|
||||
pendingRealUnmountedIDs.length +
|
||||
(pendingUnmountedRootID === null ? 0 : 1);
|
||||
const numUnmountIDs = pendingRealUnmountedIDs.length;
|
||||
const numUnmountSuspenseIDs = pendingRealUnmountedSuspenseIDs.length;
|
||||
const numSuspenderChanges = pendingSuspenderChanges.size;
|
||||
|
||||
@@ -2256,11 +2324,6 @@ export function attach(
|
||||
for (let j = 0; j < pendingRealUnmountedIDs.length; j++) {
|
||||
operations[i++] = pendingRealUnmountedIDs[j];
|
||||
}
|
||||
// The root ID should always be unmounted last.
|
||||
if (pendingUnmountedRootID !== null) {
|
||||
operations[i] = pendingUnmountedRootID;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
// Fill in pending operations.
|
||||
@@ -2308,7 +2371,6 @@ export function attach(
|
||||
pendingRealUnmountedIDs.length = 0;
|
||||
pendingRealUnmountedSuspenseIDs.length = 0;
|
||||
pendingSuspenderChanges.clear();
|
||||
pendingUnmountedRootID = null;
|
||||
pendingStringTable.clear();
|
||||
pendingStringTableLength = 0;
|
||||
}
|
||||
@@ -2512,6 +2574,17 @@ export function attach(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const suspenseNode = fiberInstance.suspenseNode;
|
||||
if (suspenseNode !== null && fiber.memoizedState === null) {
|
||||
// We're reconnecting an unsuspended Suspense. Measure to see if anything changed.
|
||||
const prevRects = suspenseNode.rects;
|
||||
const nextRects = measureInstance(fiberInstance);
|
||||
if (!areEqualRects(prevRects, nextRects)) {
|
||||
suspenseNode.rects = nextRects;
|
||||
recordSuspenseResize(suspenseNode);
|
||||
}
|
||||
}
|
||||
|
||||
const {key} = fiber;
|
||||
const displayName = getDisplayNameForFiber(fiber);
|
||||
const elementType = getElementTypeForFiber(fiber);
|
||||
@@ -2783,7 +2856,6 @@ export function attach(
|
||||
// Already disconnected.
|
||||
return;
|
||||
}
|
||||
const fiber = fiberInstance.data;
|
||||
|
||||
if (trackedPathMatchInstance === fiberInstance) {
|
||||
// We're in the process of trying to restore previous selection.
|
||||
@@ -2793,17 +2865,7 @@ export function attach(
|
||||
}
|
||||
|
||||
const id = fiberInstance.id;
|
||||
const isRoot = fiber.tag === HostRoot;
|
||||
if (isRoot) {
|
||||
// Roots must be removed only after all children have been removed.
|
||||
// So we track it separately.
|
||||
pendingUnmountedRootID = id;
|
||||
} else {
|
||||
// To maintain child-first ordering,
|
||||
// we'll push it into one of these queues,
|
||||
// and later arrange them in the correct order.
|
||||
pendingRealUnmountedIDs.push(id);
|
||||
}
|
||||
pendingRealUnmountedIDs.push(id);
|
||||
}
|
||||
|
||||
function recordSuspenseResize(suspenseNode: SuspenseNode): void {
|
||||
@@ -4020,11 +4082,23 @@ export function attach(
|
||||
fiber: Fiber,
|
||||
traceNearestHostComponentUpdate: boolean,
|
||||
): void {
|
||||
const isFocusedActivityEntry =
|
||||
focusedActivity !== null &&
|
||||
(fiber === focusedActivity || fiber.alternate === focusedActivity);
|
||||
if (isFocusedActivityEntry) {
|
||||
isInFocusedActivity = true;
|
||||
}
|
||||
|
||||
const shouldIncludeInTree = !shouldFilterFiber(fiber);
|
||||
let newInstance = null;
|
||||
let newSuspenseNode = null;
|
||||
if (shouldIncludeInTree) {
|
||||
newInstance = recordMount(fiber, reconcilingParent);
|
||||
if (isFocusedActivityEntry) {
|
||||
focusedActivityID = newInstance.id;
|
||||
pushOperation(TREE_OPERATION_APPLIED_ACTIVITY_SLICE_CHANGE);
|
||||
pushOperation(newInstance.id);
|
||||
}
|
||||
if (fiber.tag === SuspenseComponent || fiber.tag === HostRoot) {
|
||||
newSuspenseNode = createSuspenseNode(newInstance);
|
||||
// Measure this Suspense node. In general we shouldn't do this until we have
|
||||
@@ -4140,6 +4214,7 @@ export function attach(
|
||||
const stashedSuspenseParent = reconcilingParentSuspenseNode;
|
||||
const stashedSuspensePrevious = previouslyReconciledSiblingSuspenseNode;
|
||||
const stashedSuspenseRemaining = remainingReconcilingChildrenSuspenseNodes;
|
||||
const stashedIsInActivitySlice = isInFocusedActivity;
|
||||
if (newInstance !== null) {
|
||||
// Push a new DevTools instance parent while reconciling this subtree.
|
||||
reconcilingParent = newInstance;
|
||||
@@ -4153,6 +4228,17 @@ export function attach(
|
||||
remainingReconcilingChildrenSuspenseNodes = null;
|
||||
shouldPopSuspenseNode = true;
|
||||
}
|
||||
if (
|
||||
!isFocusedActivityEntry &&
|
||||
focusedActivity !== null &&
|
||||
fiber.tag === ActivityComponent
|
||||
) {
|
||||
// We're not filtering how Activity within the focused activity.
|
||||
// We cut of the bottom in the Frontend if we want to just show the
|
||||
// Activity slice instead of all Activity descendants.
|
||||
// The filtering in the backend only happens because filtering out
|
||||
// everything above the focused Activity is hard to implement in the frontend.
|
||||
}
|
||||
try {
|
||||
if (traceUpdatesEnabled) {
|
||||
if (traceNearestHostComponentUpdate) {
|
||||
@@ -4280,6 +4366,7 @@ export function attach(
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
isInFocusedActivity = stashedIsInActivitySlice;
|
||||
if (newInstance !== null) {
|
||||
reconcilingParent = stashedParent;
|
||||
previouslyReconciledSibling = stashedPrevious;
|
||||
@@ -4311,6 +4398,7 @@ export function attach(
|
||||
const stashedSuspenseParent = reconcilingParentSuspenseNode;
|
||||
const stashedSuspensePrevious = previouslyReconciledSiblingSuspenseNode;
|
||||
const stashedSuspenseRemaining = remainingReconcilingChildrenSuspenseNodes;
|
||||
const stashedIsInActivitySlice = isInFocusedActivity;
|
||||
const previousSuspendedBy = instance.suspendedBy;
|
||||
// Push a new DevTools instance parent while reconciling this subtree.
|
||||
reconcilingParent = instance;
|
||||
@@ -4329,6 +4417,19 @@ export function attach(
|
||||
shouldPopSuspenseNode = true;
|
||||
}
|
||||
|
||||
if (focusedActivity !== null) {
|
||||
if (instance.id === focusedActivityID) {
|
||||
isInFocusedActivity = true;
|
||||
} else if (
|
||||
instance.kind === FIBER_INSTANCE &&
|
||||
instance.data !== null &&
|
||||
instance.data.tag === ActivityComponent
|
||||
) {
|
||||
// Filtering nested Activity components inside the focused activity
|
||||
// is done in the frontend.
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Unmount the remaining set.
|
||||
if (
|
||||
@@ -4379,6 +4480,7 @@ export function attach(
|
||||
previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious;
|
||||
remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining;
|
||||
}
|
||||
isInFocusedActivity = stashedIsInActivitySlice;
|
||||
}
|
||||
if (instance.kind === FIBER_INSTANCE) {
|
||||
recordUnmount(instance);
|
||||
@@ -5059,6 +5161,7 @@ export function attach(
|
||||
const stashedSuspenseParent = reconcilingParentSuspenseNode;
|
||||
const stashedSuspensePrevious = previouslyReconciledSiblingSuspenseNode;
|
||||
const stashedSuspenseRemaining = remainingReconcilingChildrenSuspenseNodes;
|
||||
const stashedIsInActivitySlice = isInFocusedActivity;
|
||||
let updateFlags = NoUpdate;
|
||||
let shouldMeasureSuspenseNode = false;
|
||||
let shouldPopSuspenseNode = false;
|
||||
@@ -5098,6 +5201,15 @@ export function attach(
|
||||
shouldMeasureSuspenseNode = true;
|
||||
shouldPopSuspenseNode = true;
|
||||
}
|
||||
|
||||
if (focusedActivity !== null) {
|
||||
if (fiberInstance.id === focusedActivityID) {
|
||||
isInFocusedActivity = true;
|
||||
} else if (nextFiber.tag === ActivityComponent) {
|
||||
// Filtering nested Activity components inside the focused activity
|
||||
// is done in the frontend.
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
trackDebugInfoFromLazyType(nextFiber);
|
||||
@@ -5522,6 +5634,7 @@ export function attach(
|
||||
previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious;
|
||||
remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining;
|
||||
}
|
||||
isInFocusedActivity = stashedIsInActivitySlice;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5636,11 +5749,12 @@ export function attach(
|
||||
|
||||
mountFiberRecursively(root.current, false);
|
||||
|
||||
flushPendingEvents();
|
||||
|
||||
needsToFlushComponentLogs = false;
|
||||
currentRoot = (null: any);
|
||||
});
|
||||
|
||||
flushPendingEvents();
|
||||
|
||||
needsToFlushComponentLogs = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ export const SUSPENSE_TREE_OPERATION_REMOVE = 9;
|
||||
export const SUSPENSE_TREE_OPERATION_REORDER_CHILDREN = 10;
|
||||
export const SUSPENSE_TREE_OPERATION_RESIZE = 11;
|
||||
export const SUSPENSE_TREE_OPERATION_SUSPENDERS = 12;
|
||||
export const TREE_OPERATION_APPLIED_ACTIVITY_SLICE_CHANGE = 13;
|
||||
|
||||
export const PROFILING_FLAG_BASIC_SUPPORT /*. */ = 0b001;
|
||||
export const PROFILING_FLAG_TIMELINE_SUPPORT /* */ = 0b010;
|
||||
|
||||
104
packages/react-devtools-shared/src/devtools/store.js
vendored
104
packages/react-devtools-shared/src/devtools/store.js
vendored
@@ -21,13 +21,18 @@ import {
|
||||
TREE_OPERATION_SET_SUBTREE_MODE,
|
||||
TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS,
|
||||
TREE_OPERATION_UPDATE_TREE_BASE_DURATION,
|
||||
TREE_OPERATION_APPLIED_ACTIVITY_SLICE_CHANGE,
|
||||
SUSPENSE_TREE_OPERATION_ADD,
|
||||
SUSPENSE_TREE_OPERATION_REMOVE,
|
||||
SUSPENSE_TREE_OPERATION_REORDER_CHILDREN,
|
||||
SUSPENSE_TREE_OPERATION_RESIZE,
|
||||
SUSPENSE_TREE_OPERATION_SUSPENDERS,
|
||||
} from '../constants';
|
||||
import {ElementTypeRoot} from '../frontend/types';
|
||||
import {
|
||||
ElementTypeRoot,
|
||||
ElementTypeActivity,
|
||||
ComponentFilterActivitySlice,
|
||||
} from '../frontend/types';
|
||||
import {
|
||||
getSavedComponentFilters,
|
||||
setSavedComponentFilters,
|
||||
@@ -144,7 +149,13 @@ export default class Store extends EventEmitter<{
|
||||
hookSettings: [$ReadOnly<DevToolsHookSettings>],
|
||||
hostInstanceSelected: [Element['id']],
|
||||
settingsUpdated: [$ReadOnly<DevToolsHookSettings>],
|
||||
mutated: [[Array<Element['id']>, Map<Element['id'], Element['id']>]],
|
||||
mutated: [
|
||||
[
|
||||
Array<Element['id']>,
|
||||
Map<Element['id'], Element['id']>,
|
||||
Element['id'] | null,
|
||||
],
|
||||
],
|
||||
recordChangeDescriptions: [],
|
||||
roots: [],
|
||||
rootSupportsBasicProfiling: [],
|
||||
@@ -658,6 +669,10 @@ export default class Store extends EventEmitter<{
|
||||
return element;
|
||||
}
|
||||
|
||||
containsSuspense(id: SuspenseNode['id']): boolean {
|
||||
return this._idToSuspense.has(id);
|
||||
}
|
||||
|
||||
getSuspenseByID(id: SuspenseNode['id']): SuspenseNode | null {
|
||||
const suspense = this._idToSuspense.get(id);
|
||||
if (suspense === undefined) {
|
||||
@@ -1156,7 +1171,7 @@ export default class Store extends EventEmitter<{
|
||||
// The Tree context's search reducer expects an explicit list of ids for nodes that were added or removed.
|
||||
// In this case, we can pass it empty arrays since nodes in a collapsed tree are still there (just hidden).
|
||||
// Updating the selected search index later may require auto-expanding a collapsed subtree though.
|
||||
this.emit('mutated', [[], new Map()]);
|
||||
this.emit('mutated', [[], new Map(), null]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1225,10 +1240,11 @@ export default class Store extends EventEmitter<{
|
||||
|
||||
const addedElementIDs: Array<number> = [];
|
||||
// This is a mapping of removed ID -> parent ID:
|
||||
// We'll use the parent ID to adjust selection if it gets deleted.
|
||||
const removedElementIDs: Map<number, number> = new Map();
|
||||
const removedSuspenseIDs: Map<SuspenseNode['id'], SuspenseNode['id']> =
|
||||
new Map();
|
||||
// We'll use the parent ID to adjust selection if it gets deleted.
|
||||
let nextActivitySliceID = null;
|
||||
|
||||
let i = 2;
|
||||
|
||||
@@ -1962,6 +1978,11 @@ export default class Store extends EventEmitter<{
|
||||
|
||||
break;
|
||||
}
|
||||
case TREE_OPERATION_APPLIED_ACTIVITY_SLICE_CHANGE: {
|
||||
i++;
|
||||
nextActivitySliceID = operations[i++];
|
||||
break;
|
||||
}
|
||||
default:
|
||||
this._throwAndEmitError(
|
||||
new UnsupportedBridgeOperationError(
|
||||
@@ -2060,9 +2081,80 @@ export default class Store extends EventEmitter<{
|
||||
console.groupEnd();
|
||||
}
|
||||
|
||||
this.emit('mutated', [addedElementIDs, removedElementIDs]);
|
||||
if (nextActivitySliceID !== null && nextActivitySliceID !== 0) {
|
||||
let didCollapse = false;
|
||||
// The backend filtered everything above the Activity slice.
|
||||
// We need to hide everything below the Activity slice by collapsing
|
||||
// the Activities that are descendants of the next Activity slice.
|
||||
const nextActivitySlice = this._idToElement.get(nextActivitySliceID);
|
||||
if (nextActivitySlice === undefined) {
|
||||
throw new Error('Next Activity slice not found in Store.');
|
||||
}
|
||||
|
||||
for (let j = 0; j < nextActivitySlice.children.length; j++) {
|
||||
didCollapse ||= this._collapseActivitiesRecursively(
|
||||
nextActivitySlice.children[j],
|
||||
);
|
||||
}
|
||||
|
||||
if (didCollapse) {
|
||||
let weightAcrossRoots = 0;
|
||||
this._roots.forEach(rootID => {
|
||||
const {weight} = ((this.getElementByID(rootID): any): Element);
|
||||
weightAcrossRoots += weight;
|
||||
});
|
||||
this._weightAcrossRoots = weightAcrossRoots;
|
||||
}
|
||||
}
|
||||
|
||||
for (let j = 0; j < this._componentFilters.length; j++) {
|
||||
const filter = this._componentFilters[j];
|
||||
// If we're focusing an Activity, IDs may have changed.
|
||||
if (filter.type === ComponentFilterActivitySlice) {
|
||||
if (nextActivitySliceID === null || nextActivitySliceID === 0) {
|
||||
filter.isValid = false;
|
||||
} else {
|
||||
filter.activityID = nextActivitySliceID;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.emit('mutated', [
|
||||
addedElementIDs,
|
||||
removedElementIDs,
|
||||
nextActivitySliceID,
|
||||
]);
|
||||
};
|
||||
|
||||
_collapseActivitiesRecursively(elementID: number): boolean {
|
||||
let didMutate = false;
|
||||
const element = this._idToElement.get(elementID);
|
||||
if (element === undefined) {
|
||||
throw new Error('Element not found in Store.');
|
||||
}
|
||||
|
||||
if (element.type === ElementTypeActivity) {
|
||||
if (!element.isCollapsed) {
|
||||
element.isCollapsed = true;
|
||||
|
||||
const weightDelta = 1 - element.weight;
|
||||
|
||||
let parentElement = this._idToElement.get(element.parentID);
|
||||
while (parentElement !== undefined) {
|
||||
parentElement.weight += weightDelta;
|
||||
parentElement = this._idToElement.get(parentElement.parentID);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i < element.children.length; i++) {
|
||||
didMutate ||= this._collapseActivitiesRecursively(element.children[i]);
|
||||
}
|
||||
return didMutate;
|
||||
}
|
||||
|
||||
// Certain backends save filters on a per-domain basis.
|
||||
// In order to prevent filter preferences and applied filters from being out of sync,
|
||||
// this message enables the backend to override the frontend's current ("saved") filters.
|
||||
@@ -2228,7 +2320,7 @@ export default class Store extends EventEmitter<{
|
||||
|
||||
if (previousStatus !== status) {
|
||||
// Propagate to subscribers, although tree state has not changed
|
||||
this.emit('mutated', [[], new Map()]);
|
||||
this.emit('mutated', [[], new Map(), null]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
.ActivitySlice {
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ActivitySliceButton {
|
||||
color: var(--color-button-active);
|
||||
font-family: var(--font-family-monospace);
|
||||
font-size: var(--font-size-monospace-normal);
|
||||
}
|
||||
|
||||
.Bar {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.VRule {
|
||||
flex: 0 0 auto;
|
||||
height: 20px;
|
||||
width: 1px;
|
||||
background-color: var(--color-border);
|
||||
margin: 0 0.5rem;
|
||||
}
|
||||
52
packages/react-devtools-shared/src/devtools/views/Components/ActivitySlice.js
vendored
Normal file
52
packages/react-devtools-shared/src/devtools/views/Components/ActivitySlice.js
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
import * as React from 'react';
|
||||
import {startTransition, useContext} from 'react';
|
||||
import Button from '../Button';
|
||||
import ButtonIcon from '../ButtonIcon';
|
||||
import {StoreContext} from '../context';
|
||||
import {useChangeActivitySliceAction} from '../SuspenseTab/ActivityList';
|
||||
import {TreeDispatcherContext, TreeStateContext} from './TreeContext';
|
||||
import styles from './ActivitySlice.css';
|
||||
|
||||
export default function ActivitySlice(): React.Node {
|
||||
const dispatch = useContext(TreeDispatcherContext);
|
||||
const {activityID} = useContext(TreeStateContext);
|
||||
const store = useContext(StoreContext);
|
||||
|
||||
const activity =
|
||||
activityID === null ? null : store.getElementByID(activityID);
|
||||
const name = activity ? activity.nameProp : null;
|
||||
|
||||
const changeActivitySliceAction = useChangeActivitySliceAction();
|
||||
|
||||
return (
|
||||
<div className={styles.ActivitySlice}>
|
||||
<div className={styles.Bar}>
|
||||
<Button
|
||||
className={styles.ActivitySliceButton}
|
||||
onClick={dispatch.bind(null, {
|
||||
type: 'SELECT_ELEMENT_BY_ID',
|
||||
payload: activityID,
|
||||
})}>
|
||||
"{name || 'Unknown'}"
|
||||
</Button>
|
||||
</div>
|
||||
<div className={styles.VRule} />
|
||||
<Button
|
||||
onClick={startTransition.bind(
|
||||
null,
|
||||
changeActivitySliceAction.bind(null, null),
|
||||
)}
|
||||
title="Back to tree view">
|
||||
<ButtonIcon type="close" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -8,8 +8,9 @@
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import {Fragment, useContext, useMemo, useState} from 'react';
|
||||
import {Fragment, startTransition, useContext, useMemo, useState} from 'react';
|
||||
import Store from 'react-devtools-shared/src/devtools/store';
|
||||
import {ElementTypeActivity} from 'react-devtools-shared/src/frontend/types';
|
||||
import ButtonIcon from '../ButtonIcon';
|
||||
import {TreeDispatcherContext, TreeStateContext} from './TreeContext';
|
||||
import {StoreContext} from '../context';
|
||||
@@ -25,6 +26,7 @@ import styles from './Element.css';
|
||||
import Icon from '../Icon';
|
||||
import {useChangeOwnerAction} from './OwnersListContext';
|
||||
import Tooltip from './reach-ui/tooltip';
|
||||
import {useChangeActivitySliceAction} from '../SuspenseTab/ActivityList';
|
||||
|
||||
type Props = {
|
||||
data: ItemData,
|
||||
@@ -65,6 +67,7 @@ export default function Element({data, index, style}: Props): React.Node {
|
||||
}>(errorsAndWarningsSubscription);
|
||||
|
||||
const changeOwnerAction = useChangeOwnerAction();
|
||||
const changeActivitySliceAction = useChangeActivitySliceAction();
|
||||
|
||||
// Handle elements that are removed from the tree while an async render is in progress.
|
||||
if (element == null) {
|
||||
@@ -75,9 +78,13 @@ export default function Element({data, index, style}: Props): React.Node {
|
||||
}
|
||||
|
||||
const handleDoubleClick = () => {
|
||||
if (id !== null) {
|
||||
changeOwnerAction(id);
|
||||
}
|
||||
startTransition(() => {
|
||||
if (element.type === ElementTypeActivity) {
|
||||
changeActivitySliceAction(element.id);
|
||||
} else {
|
||||
changeOwnerAction(element.id);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// $FlowFixMe[missing-local-annot]
|
||||
|
||||
@@ -11,6 +11,7 @@ import * as React from 'react';
|
||||
import {
|
||||
Fragment,
|
||||
Suspense,
|
||||
startTransition,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
@@ -37,7 +38,10 @@ import ButtonIcon from '../ButtonIcon';
|
||||
import Button from '../Button';
|
||||
import {logEvent} from 'react-devtools-shared/src/Logger';
|
||||
import {useExtensionComponentsPanelVisibility} from 'react-devtools-shared/src/frontend/hooks/useExtensionComponentsPanelVisibility';
|
||||
import {ElementTypeActivity} from 'react-devtools-shared/src/frontend/types';
|
||||
import {useChangeOwnerAction} from './OwnersListContext';
|
||||
import {useChangeActivitySliceAction} from '../SuspenseTab/ActivityList';
|
||||
import ActivitySlice from './ActivitySlice';
|
||||
|
||||
// Indent for each node at level N, compared to node at level N - 1.
|
||||
const INDENTATION_SIZE = 10;
|
||||
@@ -72,6 +76,7 @@ function calculateInitialScrollOffset(
|
||||
export default function Tree(): React.Node {
|
||||
const dispatch = useContext(TreeDispatcherContext);
|
||||
const {
|
||||
activityID,
|
||||
numElements,
|
||||
ownerID,
|
||||
searchIndex,
|
||||
@@ -302,6 +307,7 @@ export default function Tree(): React.Node {
|
||||
const handleBlur = useCallback(() => setTreeFocused(false), []);
|
||||
const handleFocus = useCallback(() => setTreeFocused(true), []);
|
||||
|
||||
const changeActivitySliceAction = useChangeActivitySliceAction();
|
||||
const changeOwnerAction = useChangeOwnerAction();
|
||||
const handleKeyPress = useCallback(
|
||||
(event: $FlowFixMe) => {
|
||||
@@ -309,7 +315,17 @@ export default function Tree(): React.Node {
|
||||
case 'Enter':
|
||||
case ' ':
|
||||
if (inspectedElementID !== null) {
|
||||
changeOwnerAction(inspectedElementID);
|
||||
const inspectedElement = store.getElementByID(inspectedElementID);
|
||||
startTransition(() => {
|
||||
if (
|
||||
inspectedElement !== null &&
|
||||
inspectedElement.type === ElementTypeActivity
|
||||
) {
|
||||
changeActivitySliceAction(inspectedElementID);
|
||||
} else {
|
||||
changeOwnerAction(inspectedElementID);
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
default:
|
||||
@@ -444,7 +460,13 @@ export default function Tree(): React.Node {
|
||||
</Fragment>
|
||||
)}
|
||||
<Suspense fallback={<Loading />}>
|
||||
{ownerID !== null ? <OwnersStack /> : <ComponentSearchInput />}
|
||||
{ownerID !== null ? (
|
||||
<OwnersStack />
|
||||
) : activityID !== null ? (
|
||||
<ActivitySlice />
|
||||
) : (
|
||||
<ComponentSearchInput />
|
||||
)}
|
||||
</Suspense>
|
||||
{ownerID === null && (errors > 0 || warnings > 0) && (
|
||||
<React.Fragment>
|
||||
|
||||
@@ -57,6 +57,9 @@ export type StateContext = {
|
||||
ownerID: number | null,
|
||||
ownerFlatTree: Array<Element> | null,
|
||||
|
||||
// Activity slice
|
||||
activityID: Element['id'] | null,
|
||||
|
||||
// Inspection element panel
|
||||
inspectedElementID: number | null,
|
||||
inspectedElementIndex: number | null,
|
||||
@@ -70,7 +73,7 @@ type ACTION_GO_TO_PREVIOUS_SEARCH_RESULT = {
|
||||
};
|
||||
type ACTION_HANDLE_STORE_MUTATION = {
|
||||
type: 'HANDLE_STORE_MUTATION',
|
||||
payload: [Array<number>, Map<number, number>],
|
||||
payload: [Array<number>, Map<number, number>, null | Element['id']],
|
||||
};
|
||||
type ACTION_RESET_OWNER_STACK = {
|
||||
type: 'RESET_OWNER_STACK',
|
||||
@@ -167,6 +170,9 @@ type State = {
|
||||
ownerID: number | null,
|
||||
ownerFlatTree: Array<Element> | null,
|
||||
|
||||
// Activity slice
|
||||
activityID: Element['id'] | null,
|
||||
|
||||
// Inspection element panel
|
||||
inspectedElementID: number | null,
|
||||
inspectedElementIndex: number | null,
|
||||
@@ -794,6 +800,33 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
|
||||
};
|
||||
}
|
||||
|
||||
function reduceActivityState(
|
||||
store: Store,
|
||||
state: State,
|
||||
action: Action,
|
||||
): State {
|
||||
switch (action.type) {
|
||||
case 'HANDLE_STORE_MUTATION':
|
||||
let {activityID} = state;
|
||||
const [, , activitySliceIDChange] = action.payload;
|
||||
if (activitySliceIDChange === 0 && activityID !== null) {
|
||||
activityID = null;
|
||||
} else if (
|
||||
activitySliceIDChange !== null &&
|
||||
activitySliceIDChange !== activityID
|
||||
) {
|
||||
activityID = activitySliceIDChange;
|
||||
}
|
||||
if (activityID !== state.activityID) {
|
||||
return {
|
||||
...state,
|
||||
activityID,
|
||||
};
|
||||
}
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
type Props = {
|
||||
children: React$Node,
|
||||
|
||||
@@ -828,6 +861,9 @@ function getInitialState({
|
||||
ownerID: defaultOwnerID == null ? null : defaultOwnerID,
|
||||
ownerFlatTree: null,
|
||||
|
||||
// Activity slice
|
||||
activityID: null,
|
||||
|
||||
// Inspection element panel
|
||||
inspectedElementID:
|
||||
defaultInspectedElementID != null
|
||||
@@ -882,6 +918,7 @@ function TreeContextController({
|
||||
state = reduceTreeState(store, state, action);
|
||||
state = reduceSearchState(store, state, action);
|
||||
state = reduceOwnersState(store, state, action);
|
||||
state = reduceActivityState(store, state, action);
|
||||
|
||||
// TODO(hoxyq): review
|
||||
// If the selected ID is in a collapsed subtree, reset the selected index to null.
|
||||
@@ -950,13 +987,14 @@ function TreeContextController({
|
||||
|
||||
// Mutations to the underlying tree may impact this context (e.g. search results, selection state).
|
||||
useEffect(() => {
|
||||
const handleStoreMutated = ([addedElementIDs, removedElementIDs]: [
|
||||
Array<number>,
|
||||
Map<number, number>,
|
||||
]) => {
|
||||
const handleStoreMutated = ([
|
||||
addedElementIDs,
|
||||
removedElementIDs,
|
||||
activitySliceIDChange,
|
||||
]: [Array<number>, Map<number, number>, null | Element['id']]) => {
|
||||
dispatch({
|
||||
type: 'HANDLE_STORE_MUTATION',
|
||||
payload: [addedElementIDs, removedElementIDs],
|
||||
payload: [addedElementIDs, removedElementIDs, activitySliceIDChange],
|
||||
});
|
||||
};
|
||||
|
||||
@@ -967,7 +1005,7 @@ function TreeContextController({
|
||||
// It would only impact the search state, which is unlikely to exist yet at this point.
|
||||
dispatch({
|
||||
type: 'HANDLE_STORE_MUTATION',
|
||||
payload: [[], new Map()],
|
||||
payload: [[], new Map(), null],
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
TREE_OPERATION_SET_SUBTREE_MODE,
|
||||
TREE_OPERATION_UPDATE_TREE_BASE_DURATION,
|
||||
TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS,
|
||||
TREE_OPERATION_APPLIED_ACTIVITY_SLICE_CHANGE,
|
||||
SUSPENSE_TREE_OPERATION_ADD,
|
||||
SUSPENSE_TREE_OPERATION_REMOVE,
|
||||
SUSPENSE_TREE_OPERATION_REORDER_CHILDREN,
|
||||
@@ -475,6 +476,20 @@ function updateTree(
|
||||
break;
|
||||
}
|
||||
|
||||
case TREE_OPERATION_APPLIED_ACTIVITY_SLICE_CHANGE: {
|
||||
i++;
|
||||
const activitySliceIDChange = operations[i++];
|
||||
if (__DEBUG__) {
|
||||
debug(
|
||||
'Applied activity slice change',
|
||||
activitySliceIDChange === 0
|
||||
? 'Reset applied activity slice'
|
||||
: `Changed to activity slice ID ${activitySliceIDChange}`,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
throw Error(`Unsupported Bridge operation "${operation}"`);
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
ComponentFilterHOC,
|
||||
ComponentFilterLocation,
|
||||
ComponentFilterEnvironmentName,
|
||||
ComponentFilterActivitySlice,
|
||||
ElementTypeClass,
|
||||
ElementTypeContext,
|
||||
ElementTypeFunction,
|
||||
@@ -171,6 +172,8 @@ export default function ComponentsSettings({
|
||||
isValid: true,
|
||||
value: 'Client',
|
||||
};
|
||||
} else if (type === ComponentFilterActivitySlice) {
|
||||
// TODO: Allow changing type
|
||||
}
|
||||
}
|
||||
return cloned;
|
||||
@@ -364,34 +367,39 @@ export default function ComponentsSettings({
|
||||
{componentFilters.map((componentFilter, index) => (
|
||||
<tr className={styles.TableRow} key={index}>
|
||||
<td className={styles.TableCell}>
|
||||
<Toggle
|
||||
className={
|
||||
componentFilter.isValid !== false
|
||||
? ''
|
||||
: styles.InvalidRegExp
|
||||
}
|
||||
isChecked={componentFilter.isEnabled}
|
||||
onChange={isEnabled =>
|
||||
toggleFilterIsEnabled(componentFilter, isEnabled)
|
||||
}
|
||||
title={
|
||||
componentFilter.isValid === false
|
||||
? 'Filter invalid'
|
||||
: componentFilter.isEnabled
|
||||
? 'Filter enabled'
|
||||
: 'Filter disabled'
|
||||
}>
|
||||
<ToggleIcon
|
||||
isEnabled={componentFilter.isEnabled}
|
||||
isValid={
|
||||
componentFilter.isValid == null ||
|
||||
componentFilter.isValid === true
|
||||
{componentFilter.type !== ComponentFilterActivitySlice && (
|
||||
<Toggle
|
||||
className={
|
||||
componentFilter.isValid !== false
|
||||
? ''
|
||||
: styles.InvalidRegExp
|
||||
}
|
||||
/>
|
||||
</Toggle>
|
||||
isChecked={componentFilter.isEnabled}
|
||||
onChange={isEnabled =>
|
||||
toggleFilterIsEnabled(componentFilter, isEnabled)
|
||||
}
|
||||
title={
|
||||
componentFilter.isValid === false
|
||||
? 'Filter invalid'
|
||||
: componentFilter.isEnabled
|
||||
? 'Filter enabled'
|
||||
: 'Filter disabled'
|
||||
}>
|
||||
<ToggleIcon
|
||||
isEnabled={componentFilter.isEnabled}
|
||||
isValid={
|
||||
componentFilter.isValid == null ||
|
||||
componentFilter.isValid === true
|
||||
}
|
||||
/>
|
||||
</Toggle>
|
||||
)}
|
||||
</td>
|
||||
<td className={styles.TableCell}>
|
||||
<select
|
||||
disabled={
|
||||
componentFilter.type === ComponentFilterActivitySlice
|
||||
}
|
||||
value={componentFilter.type}
|
||||
onChange={({currentTarget}) =>
|
||||
changeFilterType(
|
||||
@@ -413,6 +421,11 @@ export default function ComponentsSettings({
|
||||
environment
|
||||
</option>
|
||||
)}
|
||||
{componentFilter.type === ComponentFilterActivitySlice && (
|
||||
<option value={ComponentFilterActivitySlice}>
|
||||
component
|
||||
</option>
|
||||
)}
|
||||
</select>
|
||||
</td>
|
||||
<td className={styles.TableCell}>
|
||||
@@ -422,6 +435,8 @@ export default function ComponentsSettings({
|
||||
{(componentFilter.type === ComponentFilterLocation ||
|
||||
componentFilter.type === ComponentFilterDisplayName) &&
|
||||
'matches'}
|
||||
{componentFilter.type === ComponentFilterActivitySlice &&
|
||||
'within'}
|
||||
</td>
|
||||
<td className={styles.TableCell}>
|
||||
{componentFilter.type === ComponentFilterElementType && (
|
||||
@@ -487,6 +502,9 @@ export default function ComponentsSettings({
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
{componentFilter.type === ComponentFilterActivitySlice && (
|
||||
<span>Activity Slice</span>
|
||||
)}
|
||||
</td>
|
||||
<td className={styles.TableCell}>
|
||||
<Button
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
.ActivityList {
|
||||
cursor: default;
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.ActivityList[data-pending-activity-slice-selection="true"] {
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
.ActivityList:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.ActivityListItem {
|
||||
color: var(--color-component-name);
|
||||
padding: 0 0.25rem;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.ActivityListItem:hover {
|
||||
background-color: var(--color-background-hover);
|
||||
}
|
||||
|
||||
.ActivityListItem[aria-selected="true"] {
|
||||
background-color: var(--color-background-inactive);
|
||||
}
|
||||
|
||||
.ActivityList:focus .ActivityListItem[aria-selected="true"] {
|
||||
background-color: var(--color-background-selected);
|
||||
color: var(--color-text-selected);
|
||||
|
||||
/* Invert colors */
|
||||
--color-component-name: var(--color-component-name-inverted);
|
||||
--color-text: var(--color-text-selected);
|
||||
--color-component-badge-background: var(
|
||||
--color-component-badge-background-inverted
|
||||
);
|
||||
--color-forget-badge-background: var(--color-forget-badge-background-inverted);
|
||||
--color-component-badge-count: var(--color-component-badge-count-inverted);
|
||||
--color-attribute-name: var(--color-attribute-name-inverted);
|
||||
--color-attribute-value: var(--color-attribute-value-inverted);
|
||||
--color-expand-collapse-toggle: var(--color-component-name-inverted);
|
||||
}
|
||||
173
packages/react-devtools-shared/src/devtools/views/SuspenseTab/ActivityList.js
vendored
Normal file
173
packages/react-devtools-shared/src/devtools/views/SuspenseTab/ActivityList.js
vendored
Normal file
@@ -0,0 +1,173 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
import type {
|
||||
Element,
|
||||
ActivitySliceFilter,
|
||||
ComponentFilter,
|
||||
} from 'react-devtools-shared/src/frontend/types';
|
||||
import typeof {
|
||||
SyntheticMouseEvent,
|
||||
SyntheticKeyboardEvent,
|
||||
} from 'react-dom-bindings/src/events/SyntheticEvent';
|
||||
|
||||
import * as React from 'react';
|
||||
import {useContext, useTransition} from 'react';
|
||||
import {ComponentFilterActivitySlice} from 'react-devtools-shared/src/frontend/types';
|
||||
import styles from './ActivityList.css';
|
||||
import {
|
||||
TreeStateContext,
|
||||
TreeDispatcherContext,
|
||||
} from '../Components/TreeContext';
|
||||
import {useHighlightHostInstance} from '../hooks';
|
||||
import {StoreContext} from '../context';
|
||||
|
||||
export function useChangeActivitySliceAction(): (
|
||||
id: Element['id'] | null,
|
||||
) => void {
|
||||
const store = useContext(StoreContext);
|
||||
|
||||
function changeActivitySliceAction(activityID: Element['id'] | null) {
|
||||
const nextFilters: ComponentFilter[] = [];
|
||||
// Remove any existing activity slice filter
|
||||
for (let i = 0; i < store.componentFilters.length; i++) {
|
||||
const filter = store.componentFilters[i];
|
||||
if (filter.type !== ComponentFilterActivitySlice) {
|
||||
nextFilters.push(filter);
|
||||
}
|
||||
}
|
||||
|
||||
if (activityID !== null) {
|
||||
const rendererID = store.getRendererIDForElement(activityID);
|
||||
if (rendererID === null) {
|
||||
throw new Error('Expected to find renderer.');
|
||||
}
|
||||
const activityFilter: ActivitySliceFilter = {
|
||||
type: ComponentFilterActivitySlice,
|
||||
activityID,
|
||||
rendererID,
|
||||
isValid: true,
|
||||
isEnabled: true,
|
||||
};
|
||||
nextFilters.push(activityFilter);
|
||||
}
|
||||
store.componentFilters = nextFilters;
|
||||
}
|
||||
|
||||
return changeActivitySliceAction;
|
||||
}
|
||||
|
||||
export default function ActivityList({
|
||||
activities,
|
||||
}: {
|
||||
activities: $ReadOnlyArray<Element>,
|
||||
}): React$Node {
|
||||
const {inspectedElementID} = useContext(TreeStateContext);
|
||||
const treeDispatch = useContext(TreeDispatcherContext);
|
||||
// TODO: Derive from inspected element
|
||||
const selectedActivityID = inspectedElementID;
|
||||
const {highlightHostInstance, clearHighlightHostInstance} =
|
||||
useHighlightHostInstance();
|
||||
|
||||
const [isPendingActivitySliceSelection, startActivitySliceSelection] =
|
||||
useTransition();
|
||||
const changeActivitySliceAction = useChangeActivitySliceAction();
|
||||
|
||||
function handleKeyDown(event: SyntheticKeyboardEvent) {
|
||||
// TODO: Implement keyboard navigation
|
||||
switch (event.key) {
|
||||
case 'Enter':
|
||||
case ' ':
|
||||
if (inspectedElementID !== null) {
|
||||
startActivitySliceSelection(() => {
|
||||
changeActivitySliceAction(inspectedElementID);
|
||||
});
|
||||
}
|
||||
event.preventDefault();
|
||||
break;
|
||||
case 'Home':
|
||||
treeDispatch({type: 'SELECT_ELEMENT_BY_ID', payload: activities[0].id});
|
||||
event.preventDefault();
|
||||
break;
|
||||
case 'End':
|
||||
treeDispatch({
|
||||
type: 'SELECT_ELEMENT_BY_ID',
|
||||
payload: activities[activities.length - 1].id,
|
||||
});
|
||||
event.preventDefault();
|
||||
break;
|
||||
case 'ArrowUp': {
|
||||
const currentIndex = activities.findIndex(
|
||||
activity => activity.id === selectedActivityID,
|
||||
);
|
||||
if (currentIndex !== undefined) {
|
||||
const nextIndex =
|
||||
(currentIndex + activities.length - 1) % activities.length;
|
||||
|
||||
treeDispatch({
|
||||
type: 'SELECT_ELEMENT_BY_ID',
|
||||
payload: activities[nextIndex].id,
|
||||
});
|
||||
}
|
||||
event.preventDefault();
|
||||
break;
|
||||
}
|
||||
case 'ArrowDown': {
|
||||
const currentIndex = activities.findIndex(
|
||||
activity => activity.id === selectedActivityID,
|
||||
);
|
||||
if (currentIndex !== undefined) {
|
||||
const nextIndex = (currentIndex + 1) % activities.length;
|
||||
|
||||
treeDispatch({
|
||||
type: 'SELECT_ELEMENT_BY_ID',
|
||||
payload: activities[nextIndex].id,
|
||||
});
|
||||
}
|
||||
event.preventDefault();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function handleClick(id: Element['id'], event: SyntheticMouseEvent) {
|
||||
event.preventDefault();
|
||||
treeDispatch({type: 'SELECT_ELEMENT_BY_ID', payload: id});
|
||||
}
|
||||
|
||||
function handleDoubleClick() {
|
||||
if (inspectedElementID !== null) {
|
||||
changeActivitySliceAction(inspectedElementID);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ol
|
||||
role="listbox"
|
||||
className={styles.ActivityList}
|
||||
data-pending-activity-slice-selection={isPendingActivitySliceSelection}
|
||||
tabIndex={0}
|
||||
onKeyDown={handleKeyDown}>
|
||||
{activities.map(activity => (
|
||||
<li
|
||||
key={activity.id}
|
||||
role="option"
|
||||
aria-selected={activity.id === selectedActivityID ? 'true' : 'false'}
|
||||
className={styles.ActivityListItem}
|
||||
onClick={handleClick.bind(null, activity.id)}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
onPointerOver={highlightHostInstance.bind(null, activity.id, false)}
|
||||
onPointerLeave={clearHighlightHostInstance}>
|
||||
{activity.nameProp}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
);
|
||||
}
|
||||
@@ -12,6 +12,11 @@
|
||||
background-color: color-mix(in srgb, var(--color-transition) 5%, transparent);
|
||||
}
|
||||
|
||||
.SuspenseRectsRootOutline {
|
||||
outline-width: 4px;
|
||||
border-radius: 0.125rem;
|
||||
}
|
||||
|
||||
.SuspenseRectsRoot[data-hovered='true'] {
|
||||
background-color: color-mix(in srgb, var(--color-transition) 15%, transparent);
|
||||
}
|
||||
@@ -100,10 +105,6 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.SuspenseRectOutlineRoot {
|
||||
outline-color: var(--color-transition);
|
||||
}
|
||||
|
||||
.SuspenseRectsBoundary[data-selected='true'] > .SuspenseRectsRect {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
@@ -510,9 +510,12 @@ function SuspenseRectsContainer({
|
||||
let selectedBoundingBox = null;
|
||||
let selectedEnvironment = null;
|
||||
if (isRootSelected) {
|
||||
selectedBoundingBox = boundingBox;
|
||||
selectedEnvironment = rootEnvironment;
|
||||
} else if (inspectedElementID !== null) {
|
||||
} else if (
|
||||
inspectedElementID !== null &&
|
||||
// TODO: Separate inspected element and inspected Suspense and use the inspected Suspense ID here.
|
||||
store.containsSuspense(inspectedElementID)
|
||||
) {
|
||||
const selectedSuspenseNode = store.getSuspenseByID(inspectedElementID);
|
||||
if (
|
||||
selectedSuspenseNode !== null &&
|
||||
@@ -534,6 +537,7 @@ function SuspenseRectsContainer({
|
||||
className={
|
||||
styles.SuspenseRectsContainer +
|
||||
(hasRootSuspenders ? ' ' + styles.SuspenseRectsRoot : '') +
|
||||
(isRootSelected ? ' ' + styles.SuspenseRectsRootOutline : '') +
|
||||
' ' +
|
||||
getClassNameForEnvironment(rootEnvironment)
|
||||
}
|
||||
@@ -551,7 +555,6 @@ function SuspenseRectsContainer({
|
||||
<ScaledRect
|
||||
className={
|
||||
styles.SuspenseRectOutline +
|
||||
(isRootSelected ? ' ' + styles.SuspenseRectOutlineRoot : '') +
|
||||
' ' +
|
||||
getClassNameForEnvironment(selectedEnvironment)
|
||||
}
|
||||
|
||||
@@ -91,10 +91,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
.TreeList {
|
||||
.ActivityList {
|
||||
flex: 0 0 var(--horizontal-resize-tree-list-percentage);
|
||||
border-right: 1px solid var(--color-border);
|
||||
padding: 0.25rem;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
@@ -142,4 +141,4 @@
|
||||
|
||||
.SuspenseTreeViewFooterButtons {
|
||||
padding: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,12 +6,14 @@
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
import type {Element} from 'react-devtools-shared/src/frontend/types';
|
||||
|
||||
import * as React from 'react';
|
||||
import {
|
||||
useContext,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useReducer,
|
||||
useRef,
|
||||
Fragment,
|
||||
@@ -30,7 +32,7 @@ import styles from './SuspenseTab.css';
|
||||
import SuspenseBreadcrumbs from './SuspenseBreadcrumbs';
|
||||
import SuspenseRects from './SuspenseRects';
|
||||
import SuspenseTimeline from './SuspenseTimeline';
|
||||
import SuspenseTreeList from './SuspenseTreeList';
|
||||
import ActivityList from './ActivityList';
|
||||
import {
|
||||
SuspenseTreeDispatcherContext,
|
||||
SuspenseTreeStateContext,
|
||||
@@ -270,6 +272,17 @@ function SynchronizedScrollContainer({
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: Get this from the store directly.
|
||||
// The backend needs to keep a separate tree so that resuspending keeps Activity around.
|
||||
function useActivities(): $ReadOnlyArray<Element> {
|
||||
const activities = useMemo(() => {
|
||||
const items: Array<Element> = [];
|
||||
return items;
|
||||
}, []);
|
||||
|
||||
return activities;
|
||||
}
|
||||
|
||||
function SuspenseTab(_: {}) {
|
||||
const store = useContext(StoreContext);
|
||||
const {hideSettings} = useContext(OptionsContext);
|
||||
@@ -279,10 +292,10 @@ function SuspenseTab(_: {}) {
|
||||
initLayoutState,
|
||||
);
|
||||
|
||||
const activities = useActivities();
|
||||
// If there are no named Activity boundaries, we don't have any tree list and we should hide
|
||||
// both the panel and the button to toggle it. Since we currently don't support it yet, it's
|
||||
// always disabled.
|
||||
const treeListDisabled = true;
|
||||
// both the panel and the button to toggle it.
|
||||
const treeListDisabled = activities.length === 0;
|
||||
|
||||
const wrapperTreeRef = useRef<null | HTMLElement>(null);
|
||||
const resizeTreeRef = useRef<null | HTMLElement>(null);
|
||||
@@ -462,10 +475,10 @@ function SuspenseTab(_: {}) {
|
||||
<div className={styles.TreeWrapper} ref={resizeTreeRef}>
|
||||
{treeListDisabled ? null : (
|
||||
<div
|
||||
className={styles.TreeList}
|
||||
className={styles.ActivityList}
|
||||
hidden={treeListHidden}
|
||||
ref={resizeTreeListRef}>
|
||||
<SuspenseTreeList />
|
||||
<ActivityList activities={activities} />
|
||||
</div>
|
||||
)}
|
||||
{treeListDisabled ? null : (
|
||||
|
||||
@@ -1,14 +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.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
export default function SuspenseTreeList(_: {}): React$Node {
|
||||
return <div>Activity slices not implemented yet</div>;
|
||||
}
|
||||
@@ -82,8 +82,9 @@ export const ComponentFilterDisplayName = 2;
|
||||
export const ComponentFilterLocation = 3;
|
||||
export const ComponentFilterHOC = 4;
|
||||
export const ComponentFilterEnvironmentName = 5;
|
||||
export const ComponentFilterActivitySlice = 6;
|
||||
|
||||
export type ComponentFilterType = 1 | 2 | 3 | 4 | 5;
|
||||
export type ComponentFilterType = 1 | 2 | 3 | 4 | 5 | 6;
|
||||
|
||||
// Hide all elements of types in this Set.
|
||||
// We hide host components only by default.
|
||||
@@ -115,11 +116,20 @@ export type EnvironmentNameComponentFilter = {
|
||||
value: string,
|
||||
};
|
||||
|
||||
export type ActivitySliceFilter = {
|
||||
type: 6,
|
||||
activityID: Element['id'],
|
||||
rendererID: number,
|
||||
isValid: boolean,
|
||||
isEnabled: boolean,
|
||||
};
|
||||
|
||||
export type ComponentFilter =
|
||||
| BooleanComponentFilter
|
||||
| ElementTypeComponentFilter
|
||||
| RegExpComponentFilter
|
||||
| EnvironmentNameComponentFilter;
|
||||
| EnvironmentNameComponentFilter
|
||||
| ActivitySliceFilter;
|
||||
|
||||
export type HookName = string | null;
|
||||
// Map of hook source ("<filename>:<line-number>:<column-number>") to name.
|
||||
|
||||
36
packages/react-devtools-shared/src/utils.js
vendored
36
packages/react-devtools-shared/src/utils.js
vendored
@@ -33,6 +33,7 @@ import {
|
||||
TREE_OPERATION_SET_SUBTREE_MODE,
|
||||
TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS,
|
||||
TREE_OPERATION_UPDATE_TREE_BASE_DURATION,
|
||||
TREE_OPERATION_APPLIED_ACTIVITY_SLICE_CHANGE,
|
||||
LOCAL_STORAGE_COMPONENT_FILTER_PREFERENCES_KEY,
|
||||
LOCAL_STORAGE_OPEN_IN_EDITOR_URL,
|
||||
LOCAL_STORAGE_OPEN_IN_EDITOR_URL_PRESET,
|
||||
@@ -47,6 +48,7 @@ import {
|
||||
SUSPENSE_TREE_OPERATION_SUSPENDERS,
|
||||
} from './constants';
|
||||
import {
|
||||
ComponentFilterActivitySlice,
|
||||
ComponentFilterElementType,
|
||||
ComponentFilterLocation,
|
||||
ElementTypeHostComponent,
|
||||
@@ -443,6 +445,16 @@ export function printOperationsArray(operations: Array<number>) {
|
||||
|
||||
break;
|
||||
}
|
||||
case TREE_OPERATION_APPLIED_ACTIVITY_SLICE_CHANGE: {
|
||||
i++;
|
||||
const activitySliceIDChange = operations[i + 1];
|
||||
logs.push(
|
||||
activitySliceIDChange === 0
|
||||
? 'Reset applied activity slice'
|
||||
: 'Applied activity slice change to ' + activitySliceIDChange,
|
||||
);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw Error(`Unsupported Bridge operation "${operation}"`);
|
||||
}
|
||||
@@ -468,7 +480,7 @@ export function getSavedComponentFilters(): Array<ComponentFilter> {
|
||||
);
|
||||
if (raw != null) {
|
||||
const parsedFilters: Array<ComponentFilter> = JSON.parse(raw);
|
||||
return filterOutLocationComponentFilters(parsedFilters);
|
||||
return persistableComponentFilters(parsedFilters);
|
||||
}
|
||||
} catch (error) {}
|
||||
return getDefaultComponentFilters();
|
||||
@@ -479,16 +491,11 @@ export function setSavedComponentFilters(
|
||||
): void {
|
||||
localStorageSetItem(
|
||||
LOCAL_STORAGE_COMPONENT_FILTER_PREFERENCES_KEY,
|
||||
JSON.stringify(filterOutLocationComponentFilters(componentFilters)),
|
||||
JSON.stringify(persistableComponentFilters(componentFilters)),
|
||||
);
|
||||
}
|
||||
|
||||
// Following __debugSource removal from Fiber, the new approach for finding the source location
|
||||
// of a component, represented by the Fiber, is based on lazily generating and parsing component stack frames
|
||||
// To find the original location, React DevTools will perform symbolication, source maps are required for that.
|
||||
// In order to start filtering Fibers, we need to find location for all of them, which can't be done lazily.
|
||||
// Eager symbolication can become quite expensive for large applications.
|
||||
export function filterOutLocationComponentFilters(
|
||||
export function persistableComponentFilters(
|
||||
componentFilters: Array<ComponentFilter>,
|
||||
): Array<ComponentFilter> {
|
||||
// This is just an additional check to preserve the previous state
|
||||
@@ -497,7 +504,18 @@ export function filterOutLocationComponentFilters(
|
||||
return componentFilters;
|
||||
}
|
||||
|
||||
return componentFilters.filter(f => f.type !== ComponentFilterLocation);
|
||||
return componentFilters.filter(f => {
|
||||
return (
|
||||
// Following __debugSource removal from Fiber, the new approach for finding the source location
|
||||
// of a component, represented by the Fiber, is based on lazily generating and parsing component stack frames
|
||||
// To find the original location, React DevTools will perform symbolication, source maps are required for that.
|
||||
// In order to start filtering Fibers, we need to find location for all of them, which can't be done lazily.
|
||||
// Eager symbolication can become quite expensive for large applications.
|
||||
f.type !== ComponentFilterLocation &&
|
||||
// Activity slice filters are based on DevTools instance IDs which do not persist across sessions.
|
||||
f.type !== ComponentFilterActivitySlice
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const vscodeFilepath = 'vscode://file/{path}:{line}:{column}';
|
||||
|
||||
96
packages/react-devtools-shell/src/app/Segments/index.js
vendored
Normal file
96
packages/react-devtools-shell/src/app/Segments/index.js
vendored
Normal file
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
function deferred<T>(
|
||||
timeoutMS: number,
|
||||
resolvedValue: T,
|
||||
displayName: string,
|
||||
): Promise<T> {
|
||||
const promise = new Promise<T>(resolve => {
|
||||
setTimeout(() => resolve(resolvedValue), timeoutMS);
|
||||
});
|
||||
(promise as any).displayName = displayName;
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
const title = deferred(100, 'Segmented Page Title', 'title');
|
||||
const content = deferred(
|
||||
400,
|
||||
'This is the content of a segmented page. It loads in multiple parts.',
|
||||
'content',
|
||||
);
|
||||
function Page(): React.Node {
|
||||
return (
|
||||
<article>
|
||||
<h1>{title}</h1>
|
||||
<p>{content}</p>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
function InnerSegment({children}: {children: React.Node}): React.Node {
|
||||
return (
|
||||
<>
|
||||
<h3>Inner Segment</h3>
|
||||
<React.Suspense name="InnerSegment" fallback={<p>Loading...</p>}>
|
||||
<section>{children}</section>
|
||||
<p>After inner</p>
|
||||
</React.Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const cookies = deferred(200, 'Cookies: 🍪🍪🍪', 'cookies');
|
||||
function OuterSegment({children}: {children: React.Node}): React.Node {
|
||||
return (
|
||||
<>
|
||||
<h2>Outer Segment</h2>
|
||||
<React.Suspense name="OuterSegment" fallback={<p>Loading outer</p>}>
|
||||
<p>{cookies}</p>
|
||||
<div>{children}</div>
|
||||
<p>After outer</p>
|
||||
</React.Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Root({children}: {children: React.Node}): React.Node {
|
||||
return (
|
||||
<>
|
||||
<h1>Root Segment</h1>
|
||||
<React.Suspense name="Root" fallback={<p>Loading root</p>}>
|
||||
<main>{children}</main>
|
||||
<footer>After root</footer>
|
||||
</React.Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Segments(): React.Node {
|
||||
return (
|
||||
<React.Activity name="/" mode="visible">
|
||||
<Root>
|
||||
<React.Activity name="/outer/" mode="visible">
|
||||
<OuterSegment>
|
||||
<React.Activity name="/outer/inner" mode="visible">
|
||||
<InnerSegment>
|
||||
<React.Activity name="/outer/inner/page" mode="visible">
|
||||
<Page />
|
||||
</React.Activity>
|
||||
</InnerSegment>
|
||||
</React.Activity>
|
||||
</OuterSegment>
|
||||
</React.Activity>
|
||||
</Root>
|
||||
</React.Activity>
|
||||
);
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import ToDoList from './ToDoList';
|
||||
import Toggle from './Toggle';
|
||||
import ErrorBoundaries from './ErrorBoundaries';
|
||||
import PartiallyStrictApp from './PartiallyStrictApp';
|
||||
import Segments from './Segments';
|
||||
import SuspenseTree from './SuspenseTree';
|
||||
import TraceUpdatesTest from './TraceUpdatesTest';
|
||||
import {ignoreErrors, ignoreLogs, ignoreWarnings} from './console';
|
||||
@@ -114,6 +115,7 @@ function mountTestApp() {
|
||||
mountApp(DeeplyNestedComponents);
|
||||
mountApp(Iframe);
|
||||
mountApp(TraceUpdatesTest);
|
||||
mountApp(Segments);
|
||||
|
||||
if (shouldRenderLegacy) {
|
||||
mountLegacyApp(PartiallyStrictApp);
|
||||
|
||||
@@ -27,6 +27,6 @@
|
||||
"internal-ip": "^6.2.0",
|
||||
"minimist": "^1.2.3",
|
||||
"react-devtools-core": "7.0.1",
|
||||
"update-notifier": "^2.1.0"
|
||||
"update-notifier": "^5.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,8 @@ import {getParentHydrationBoundary} from './ReactFiberConfigDOM';
|
||||
|
||||
import {enableScopeAPI} from 'shared/ReactFeatureFlags';
|
||||
|
||||
import {enableInternalInstanceMap} from 'shared/ReactFeatureFlags';
|
||||
|
||||
const randomKey = Math.random().toString(36).slice(2);
|
||||
const internalInstanceKey = '__reactFiber$' + randomKey;
|
||||
const internalPropsKey = '__reactProps$' + randomKey;
|
||||
@@ -49,7 +51,32 @@ const internalRootNodeResourcesKey = '__reactResources$' + randomKey;
|
||||
const internalHoistableMarker = '__reactMarker$' + randomKey;
|
||||
const internalScrollTimer = '__reactScroll$' + randomKey;
|
||||
|
||||
type InstanceUnion =
|
||||
| Instance
|
||||
| TextInstance
|
||||
| SuspenseInstance
|
||||
| ActivityInstance
|
||||
| ReactScopeInstance
|
||||
| Container;
|
||||
|
||||
const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
|
||||
const internalInstanceMap:
|
||||
| WeakMap<InstanceUnion, Fiber>
|
||||
| Map<InstanceUnion, Fiber> = new PossiblyWeakMap();
|
||||
const internalPropsMap:
|
||||
| WeakMap<InstanceUnion, Props>
|
||||
| Map<InstanceUnion, Props> = new PossiblyWeakMap();
|
||||
|
||||
export function detachDeletedInstance(node: Instance): void {
|
||||
if (enableInternalInstanceMap) {
|
||||
internalInstanceMap.delete(node);
|
||||
internalPropsMap.delete(node);
|
||||
delete (node: any)[internalEventHandlersKey];
|
||||
delete (node: any)[internalEventHandlerListenersKey];
|
||||
delete (node: any)[internalEventHandlesSetKey];
|
||||
delete (node: any)[internalRootNodeResourcesKey];
|
||||
return;
|
||||
}
|
||||
// TODO: This function is only called on host components. I don't think all of
|
||||
// these fields are relevant.
|
||||
delete (node: any)[internalInstanceKey];
|
||||
@@ -68,6 +95,10 @@ export function precacheFiberNode(
|
||||
| ActivityInstance
|
||||
| ReactScopeInstance,
|
||||
): void {
|
||||
if (enableInternalInstanceMap) {
|
||||
internalInstanceMap.set(node, hostInst);
|
||||
return;
|
||||
}
|
||||
(node: any)[internalInstanceKey] = hostInst;
|
||||
}
|
||||
|
||||
@@ -95,7 +126,12 @@ export function isContainerMarkedAsRoot(node: Container): boolean {
|
||||
// HostRoot back. To get to the HostRoot, you need to pass a child of it.
|
||||
// The same thing applies to Suspense and Activity boundaries.
|
||||
export function getClosestInstanceFromNode(targetNode: Node): null | Fiber {
|
||||
let targetInst = (targetNode: any)[internalInstanceKey];
|
||||
let targetInst: void | Fiber;
|
||||
if (enableInternalInstanceMap) {
|
||||
targetInst = internalInstanceMap.get(((targetNode: any): InstanceUnion));
|
||||
} else {
|
||||
targetInst = (targetNode: any)[internalInstanceKey];
|
||||
}
|
||||
if (targetInst) {
|
||||
// Don't return HostRoot, SuspenseComponent or ActivityComponent here.
|
||||
return targetInst;
|
||||
@@ -112,9 +148,15 @@ export function getClosestInstanceFromNode(targetNode: Node): null | Fiber {
|
||||
// itself because the fibers are conceptually between the container
|
||||
// node and the first child. It isn't surrounding the container node.
|
||||
// If it's not a container, we check if it's an instance.
|
||||
targetInst =
|
||||
(parentNode: any)[internalContainerInstanceKey] ||
|
||||
(parentNode: any)[internalInstanceKey];
|
||||
if (enableInternalInstanceMap) {
|
||||
targetInst =
|
||||
(parentNode: any)[internalContainerInstanceKey] ||
|
||||
internalInstanceMap.get(((parentNode: any): InstanceUnion));
|
||||
} else {
|
||||
targetInst =
|
||||
(parentNode: any)[internalContainerInstanceKey] ||
|
||||
(parentNode: any)[internalInstanceKey];
|
||||
}
|
||||
if (targetInst) {
|
||||
// Since this wasn't the direct target of the event, we might have
|
||||
// stepped past dehydrated DOM nodes to get here. However they could
|
||||
@@ -147,8 +189,10 @@ export function getClosestInstanceFromNode(targetNode: Node): null | Fiber {
|
||||
// have had an internalInstanceKey on it.
|
||||
// Let's get the fiber associated with the SuspenseComponent
|
||||
// as the deepest instance.
|
||||
// $FlowFixMe[prop-missing]
|
||||
const targetFiber = hydrationInstance[internalInstanceKey];
|
||||
const targetFiber = enableInternalInstanceMap
|
||||
? internalInstanceMap.get(hydrationInstance)
|
||||
: // $FlowFixMe[prop-missing]
|
||||
hydrationInstance[internalInstanceKey];
|
||||
if (targetFiber) {
|
||||
return targetFiber;
|
||||
}
|
||||
@@ -175,9 +219,16 @@ export function getClosestInstanceFromNode(targetNode: Node): null | Fiber {
|
||||
* instance, or null if the node was not rendered by this React.
|
||||
*/
|
||||
export function getInstanceFromNode(node: Node): Fiber | null {
|
||||
const inst =
|
||||
(node: any)[internalInstanceKey] ||
|
||||
(node: any)[internalContainerInstanceKey];
|
||||
let inst: void | null | Fiber;
|
||||
if (enableInternalInstanceMap) {
|
||||
inst =
|
||||
internalInstanceMap.get(((node: any): InstanceUnion)) ||
|
||||
(node: any)[internalContainerInstanceKey];
|
||||
} else {
|
||||
inst =
|
||||
(node: any)[internalInstanceKey] ||
|
||||
(node: any)[internalContainerInstanceKey];
|
||||
}
|
||||
if (inst) {
|
||||
const tag = inst.tag;
|
||||
if (
|
||||
@@ -226,16 +277,25 @@ export function getFiberCurrentPropsFromNode(
|
||||
| TextInstance
|
||||
| SuspenseInstance
|
||||
| ActivityInstance,
|
||||
): Props {
|
||||
): Props | null {
|
||||
if (enableInternalInstanceMap) {
|
||||
return internalPropsMap.get(node) || null;
|
||||
}
|
||||
return (node: any)[internalPropsKey] || null;
|
||||
}
|
||||
|
||||
export function updateFiberProps(node: Instance, props: Props): void {
|
||||
if (enableInternalInstanceMap) {
|
||||
internalPropsMap.set(node, props);
|
||||
return;
|
||||
}
|
||||
(node: any)[internalPropsKey] = props;
|
||||
}
|
||||
|
||||
export function getEventListenerSet(node: EventTarget): Set<string> {
|
||||
let elementListenerSet = (node: any)[internalEventHandlersKey];
|
||||
let elementListenerSet: Set<string> | void = (node: any)[
|
||||
internalEventHandlersKey
|
||||
];
|
||||
if (elementListenerSet === undefined) {
|
||||
elementListenerSet = (node: any)[internalEventHandlersKey] = new Set();
|
||||
}
|
||||
@@ -246,6 +306,9 @@ export function getFiberFromScopeInstance(
|
||||
scope: ReactScopeInstance,
|
||||
): null | Fiber {
|
||||
if (enableScopeAPI) {
|
||||
if (enableInternalInstanceMap) {
|
||||
return internalInstanceMap.get(((scope: any): InstanceUnion)) || null;
|
||||
}
|
||||
return (scope: any)[internalInstanceKey] || null;
|
||||
}
|
||||
return null;
|
||||
@@ -318,6 +381,12 @@ export function clearScrollEndTimer(node: EventTarget): void {
|
||||
}
|
||||
|
||||
export function isOwnedInstance(node: Node): boolean {
|
||||
if (enableInternalInstanceMap) {
|
||||
return !!(
|
||||
(node: any)[internalHoistableMarker] ||
|
||||
internalInstanceMap.has((node: any))
|
||||
);
|
||||
}
|
||||
return !!(
|
||||
(node: any)[internalHoistableMarker] || (node: any)[internalInstanceKey]
|
||||
);
|
||||
|
||||
@@ -1435,8 +1435,13 @@ export function applyViewTransitionName(
|
||||
className: ?string,
|
||||
): void {
|
||||
instance = ((instance: any): HTMLElement);
|
||||
// If the name isn't valid CSS identifier, base64 encode the name instead.
|
||||
// This doesn't let you select it in custom CSS selectors but it does work in current
|
||||
// browsers.
|
||||
const escapedName =
|
||||
CSS.escape(name) !== name ? 'r-' + btoa(name).replace(/=/g, '') : name;
|
||||
// $FlowFixMe[prop-missing]
|
||||
instance.style.viewTransitionName = name;
|
||||
instance.style.viewTransitionName = escapedName;
|
||||
if (className != null) {
|
||||
// $FlowFixMe[prop-missing]
|
||||
instance.style.viewTransitionClass = className;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user