Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
107d1983fa | ||
|
|
19b769fa5f | ||
|
|
dbf2538355 | ||
|
|
21f282425c | ||
|
|
257b033fc7 | ||
|
|
de97ef9ad5 | ||
|
|
93fc57400b | ||
|
|
093b3246e1 | ||
|
|
3a495ae722 | ||
|
|
bbe3f4d322 | ||
|
|
1ea46df8ba | ||
|
|
8c15edd57c | ||
|
|
5e94655cbb | ||
|
|
db8273c12f | ||
|
|
04ee54cd12 | ||
|
|
100fc4a8cf | ||
|
|
92ac4e8b80 | ||
|
|
f76c3617e0 | ||
|
|
7296120396 |
@@ -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';
|
||||
@@ -36,13 +37,19 @@ type DerivationMetadata = {
|
||||
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 {
|
||||
@@ -173,25 +180,24 @@ function isNamedIdentifier(place: Place): place is Place & {
|
||||
*/
|
||||
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') {
|
||||
@@ -232,13 +238,11 @@ export function validateNoDerivedComputationsInEffects_exp(
|
||||
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(
|
||||
@@ -282,11 +286,61 @@ 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();
|
||||
@@ -309,25 +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) {
|
||||
isSource = true;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -525,21 +591,72 @@ function renderTree(
|
||||
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
|
||||
@@ -553,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -576,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,
|
||||
);
|
||||
@@ -583,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,
|
||||
});
|
||||
@@ -617,15 +743,17 @@ 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 propsSet = new Set<string>();
|
||||
const stateSet = new Set<string>();
|
||||
@@ -651,6 +779,12 @@ function validateEffect(
|
||||
),
|
||||
);
|
||||
|
||||
for (const dep of derivedSetStateCall.sourceIds) {
|
||||
if (cleanUpFunctionDeps !== undefined && cleanUpFunctionDeps.has(dep)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const propsArr = Array.from(propsSet);
|
||||
const stateArr = Array.from(stateSet);
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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,58 +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.
|
||||
|
||||
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:
|
||||
|
||||
Props: [value]
|
||||
|
||||
Data Flow Tree:
|
||||
└── value (Prop)
|
||||
|
||||
See: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state.
|
||||
|
||||
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,55 +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.
|
||||
|
||||
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:
|
||||
|
||||
Props: [input]
|
||||
|
||||
Data Flow Tree:
|
||||
└── input (Prop)
|
||||
|
||||
See: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state.
|
||||
|
||||
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,52 +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.
|
||||
|
||||
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:
|
||||
|
||||
State: [count]
|
||||
|
||||
Data Flow Tree:
|
||||
└── count (State)
|
||||
|
||||
See: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state.
|
||||
|
||||
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,64 +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.
|
||||
|
||||
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:
|
||||
|
||||
Props: [firstName]
|
||||
State: [lastName]
|
||||
|
||||
Data Flow Tree:
|
||||
├── firstName (Prop)
|
||||
└── lastName (State)
|
||||
|
||||
See: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state.
|
||||
|
||||
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,48 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
|
||||
function Component({value}) {
|
||||
const [checked, setChecked] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setChecked(value === '' ? [] : value.split(','));
|
||||
}, [value]);
|
||||
|
||||
return <div>{checked}</div>;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: You might not need an effect. Derive values in render, not effects.
|
||||
|
||||
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:
|
||||
|
||||
Props: [value]
|
||||
|
||||
Data Flow Tree:
|
||||
└── value (Prop)
|
||||
|
||||
See: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state.
|
||||
|
||||
error.derived-state-from-prop-setter-ternary.ts:7:4
|
||||
5 |
|
||||
6 | useEffect(() => {
|
||||
> 7 | setChecked(value === '' ? [] : value.split(','));
|
||||
| ^^^^^^^^^^ This should be computed during render, not in an effect
|
||||
8 | }, [value]);
|
||||
9 |
|
||||
10 | return <div>{checked}</div>;
|
||||
```
|
||||
|
||||
|
||||
@@ -1,55 +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.
|
||||
|
||||
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:
|
||||
|
||||
Props: [value]
|
||||
|
||||
Data Flow Tree:
|
||||
└── value (Prop)
|
||||
|
||||
See: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state.
|
||||
|
||||
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,59 +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.
|
||||
|
||||
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:
|
||||
|
||||
Props: [propValue]
|
||||
|
||||
Data Flow Tree:
|
||||
└── propValue (Prop)
|
||||
|
||||
See: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state.
|
||||
|
||||
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,57 +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.
|
||||
|
||||
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:
|
||||
|
||||
State: [firstName]
|
||||
|
||||
Data Flow Tree:
|
||||
└── firstName (State)
|
||||
|
||||
See: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state.
|
||||
|
||||
error.invalid-derived-computation-in-effect.ts:11:4
|
||||
9 | const [fullName, setFullName] = useState('');
|
||||
10 | useEffect(() => {
|
||||
> 11 | setFullName(firstName + ' ' + lastName);
|
||||
| ^^^^^^^^^^^ This should be computed during render, not in an effect
|
||||
12 | }, [firstName, lastName]);
|
||||
13 |
|
||||
14 | return <div>{fullName}</div>;
|
||||
```
|
||||
|
||||
|
||||
@@ -1,56 +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.
|
||||
|
||||
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:
|
||||
|
||||
Props: [props]
|
||||
|
||||
Data Flow Tree:
|
||||
└── computed
|
||||
└── props (Prop)
|
||||
|
||||
See: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state.
|
||||
|
||||
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,56 +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.
|
||||
|
||||
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:
|
||||
|
||||
Props: [props]
|
||||
|
||||
Data Flow Tree:
|
||||
└── props (Prop)
|
||||
|
||||
See: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state.
|
||||
|
||||
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;
|
||||
}
|
||||
59
packages/react-client/src/ReactFlightClient.js
vendored
59
packages/react-client/src/ReactFlightClient.js
vendored
@@ -4857,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" */ ||
|
||||
@@ -4916,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) {
|
||||
@@ -4936,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1576,7 +1576,6 @@ export function attach(
|
||||
currentRoot = rootInstance;
|
||||
unmountInstanceRecursively(rootInstance);
|
||||
rootToFiberInstanceMap.delete(root);
|
||||
flushPendingEvents();
|
||||
currentRoot = (null: any);
|
||||
});
|
||||
|
||||
@@ -1646,7 +1645,6 @@ export function attach(
|
||||
currentRoot = newRoot;
|
||||
setRootPseudoKey(currentRoot.id, root.current);
|
||||
mountFiberRecursively(root.current, false);
|
||||
flushPendingEvents();
|
||||
currentRoot = (null: any);
|
||||
});
|
||||
|
||||
@@ -2159,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__) {
|
||||
@@ -2187,8 +2184,7 @@ export function attach(
|
||||
pendingOperations.length === 0 &&
|
||||
pendingRealUnmountedIDs.length === 0 &&
|
||||
pendingRealUnmountedSuspenseIDs.length === 0 &&
|
||||
pendingSuspenderChanges.size === 0 &&
|
||||
pendingUnmountedRootID === null
|
||||
pendingSuspenderChanges.size === 0
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2250,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;
|
||||
|
||||
@@ -2330,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.
|
||||
@@ -2382,7 +2371,6 @@ export function attach(
|
||||
pendingRealUnmountedIDs.length = 0;
|
||||
pendingRealUnmountedSuspenseIDs.length = 0;
|
||||
pendingSuspenderChanges.clear();
|
||||
pendingUnmountedRootID = null;
|
||||
pendingStringTable.clear();
|
||||
pendingStringTableLength = 0;
|
||||
}
|
||||
@@ -2868,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.
|
||||
@@ -2878,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 {
|
||||
@@ -5772,11 +5749,12 @@ export function attach(
|
||||
|
||||
mountFiberRecursively(root.current, false);
|
||||
|
||||
flushPendingEvents();
|
||||
|
||||
needsToFlushComponentLogs = false;
|
||||
currentRoot = (null: any);
|
||||
});
|
||||
|
||||
flushPendingEvents();
|
||||
|
||||
needsToFlushComponentLogs = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -341,7 +341,6 @@ export function getEventPriority(domEventName: DOMEventName): EventPriority {
|
||||
case 'pointerup':
|
||||
case 'ratechange':
|
||||
case 'reset':
|
||||
case 'resize':
|
||||
case 'seeked':
|
||||
case 'submit':
|
||||
case 'toggle':
|
||||
@@ -380,6 +379,7 @@ export function getEventPriority(domEventName: DOMEventName): EventPriority {
|
||||
case 'pointermove':
|
||||
case 'pointerout':
|
||||
case 'pointerover':
|
||||
case 'resize':
|
||||
case 'scroll':
|
||||
case 'touchmove':
|
||||
case 'wheel':
|
||||
|
||||
@@ -10,11 +10,17 @@
|
||||
'use strict';
|
||||
|
||||
let React;
|
||||
let Activity;
|
||||
let useState;
|
||||
let ReactDOM;
|
||||
let ReactDOMClient;
|
||||
let Scheduler;
|
||||
let act;
|
||||
let Activity;
|
||||
let useState;
|
||||
let useLayoutEffect;
|
||||
let useEffect;
|
||||
let LegacyHidden;
|
||||
let assertLog;
|
||||
let Suspense;
|
||||
|
||||
describe('ReactDOMActivity', () => {
|
||||
let container;
|
||||
@@ -22,11 +28,19 @@ describe('ReactDOMActivity', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
React = require('react');
|
||||
Scheduler = require('scheduler/unstable_mock');
|
||||
Activity = React.Activity;
|
||||
useState = React.useState;
|
||||
Suspense = React.Suspense;
|
||||
useState = React.useState;
|
||||
LegacyHidden = React.unstable_LegacyHidden;
|
||||
useLayoutEffect = React.useLayoutEffect;
|
||||
useEffect = React.useEffect;
|
||||
ReactDOM = require('react-dom');
|
||||
ReactDOMClient = require('react-dom/client');
|
||||
act = require('internal-test-utils').act;
|
||||
const InternalTestUtils = require('internal-test-utils');
|
||||
act = InternalTestUtils.act;
|
||||
assertLog = InternalTestUtils.assertLog;
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
});
|
||||
@@ -35,6 +49,11 @@ describe('ReactDOMActivity', () => {
|
||||
document.body.removeChild(container);
|
||||
});
|
||||
|
||||
function Text(props) {
|
||||
Scheduler.log(props.text);
|
||||
return <span prop={props.text}>{props.children}</span>;
|
||||
}
|
||||
|
||||
// @gate enableActivity
|
||||
it(
|
||||
'hiding an Activity boundary also hides the direct children of any ' +
|
||||
@@ -53,7 +72,7 @@ describe('ReactDOMActivity', () => {
|
||||
);
|
||||
}
|
||||
|
||||
function App({portalContents}) {
|
||||
function App() {
|
||||
return (
|
||||
<Accordion>
|
||||
<div>
|
||||
@@ -99,7 +118,7 @@ describe('ReactDOMActivity', () => {
|
||||
);
|
||||
}
|
||||
|
||||
function App({portalContents}) {
|
||||
function App() {
|
||||
return (
|
||||
<Activity mode="hidden">
|
||||
<div>
|
||||
@@ -131,4 +150,416 @@ describe('ReactDOMActivity', () => {
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
// @gate enableActivity
|
||||
it('hides new portals added to an already hidden tree', async () => {
|
||||
function Child() {
|
||||
return <Text text="Child" />;
|
||||
}
|
||||
|
||||
const portalContainer = document.createElement('div');
|
||||
|
||||
function Portal({children}) {
|
||||
return <div>{ReactDOM.createPortal(children, portalContainer)}</div>;
|
||||
}
|
||||
|
||||
const root = ReactDOMClient.createRoot(container);
|
||||
// Mount hidden tree.
|
||||
await act(() => {
|
||||
root.render(
|
||||
<Activity mode="hidden">
|
||||
<Text text="Parent" />
|
||||
</Activity>,
|
||||
);
|
||||
});
|
||||
assertLog(['Parent']);
|
||||
expect(container.innerHTML).toBe(
|
||||
'<span prop="Parent" style="display: none;"></span>',
|
||||
);
|
||||
expect(portalContainer.innerHTML).toBe('');
|
||||
|
||||
// Add a portal inside the hidden tree.
|
||||
await act(() => {
|
||||
root.render(
|
||||
<Activity mode="hidden">
|
||||
<Text text="Parent" />
|
||||
<Portal>
|
||||
<Child />
|
||||
</Portal>
|
||||
</Activity>,
|
||||
);
|
||||
});
|
||||
assertLog(['Parent', 'Child']);
|
||||
expect(container.innerHTML).toBe(
|
||||
'<span prop="Parent" style="display: none;"></span><div style="display: none;"></div>',
|
||||
);
|
||||
expect(portalContainer.innerHTML).toBe(
|
||||
'<span prop="Child" style="display: none;"></span>',
|
||||
);
|
||||
|
||||
// Now reveal it.
|
||||
await act(() => {
|
||||
root.render(
|
||||
<Activity mode="visible">
|
||||
<Text text="Parent" />
|
||||
<Portal>
|
||||
<Child />
|
||||
</Portal>
|
||||
</Activity>,
|
||||
);
|
||||
});
|
||||
|
||||
assertLog(['Parent', 'Child']);
|
||||
expect(container.innerHTML).toBe(
|
||||
'<span prop="Parent" style=""></span><div style=""></div>',
|
||||
);
|
||||
expect(portalContainer.innerHTML).toBe(
|
||||
'<span prop="Child" style=""></span>',
|
||||
);
|
||||
});
|
||||
|
||||
// @gate enableActivity
|
||||
it('hides new insertions inside an already hidden portal', async () => {
|
||||
function Child({text}) {
|
||||
useLayoutEffect(() => {
|
||||
Scheduler.log(`Mount layout ${text}`);
|
||||
return () => {
|
||||
Scheduler.log(`Unmount layout ${text}`);
|
||||
};
|
||||
}, [text]);
|
||||
return <Text text={text} />;
|
||||
}
|
||||
|
||||
const portalContainer = document.createElement('div');
|
||||
|
||||
function Portal({children}) {
|
||||
return <div>{ReactDOM.createPortal(children, portalContainer)}</div>;
|
||||
}
|
||||
|
||||
const root = ReactDOMClient.createRoot(container);
|
||||
// Mount hidden tree.
|
||||
await act(() => {
|
||||
root.render(
|
||||
<Activity mode="hidden">
|
||||
<Portal>
|
||||
<Child text="A" />
|
||||
</Portal>
|
||||
</Activity>,
|
||||
);
|
||||
});
|
||||
assertLog(['A']);
|
||||
expect(container.innerHTML).toBe('<div style="display: none;"></div>');
|
||||
expect(portalContainer.innerHTML).toBe(
|
||||
'<span prop="A" style="display: none;"></span>',
|
||||
);
|
||||
|
||||
// Add a node inside the hidden portal.
|
||||
await act(() => {
|
||||
root.render(
|
||||
<Activity mode="hidden">
|
||||
<Portal>
|
||||
<Child text="A" />
|
||||
<Child text="B" />
|
||||
</Portal>
|
||||
</Activity>,
|
||||
);
|
||||
});
|
||||
assertLog(['A', 'B']);
|
||||
expect(container.innerHTML).toBe('<div style="display: none;"></div>');
|
||||
expect(portalContainer.innerHTML).toBe(
|
||||
'<span prop="A" style="display: none;"></span><span prop="B" style="display: none;"></span>',
|
||||
);
|
||||
|
||||
// Now reveal it.
|
||||
await act(() => {
|
||||
root.render(
|
||||
<Activity mode="visible">
|
||||
<Portal>
|
||||
<Child text="A" />
|
||||
<Child text="B" />
|
||||
</Portal>
|
||||
</Activity>,
|
||||
);
|
||||
});
|
||||
|
||||
assertLog(['A', 'B', 'Mount layout A', 'Mount layout B']);
|
||||
expect(container.innerHTML).toBe('<div style=""></div>');
|
||||
expect(portalContainer.innerHTML).toBe(
|
||||
'<span prop="A" style=""></span><span prop="B" style=""></span>',
|
||||
);
|
||||
});
|
||||
|
||||
// @gate enableActivity
|
||||
it('reveal an inner Suspense boundary without revealing an outer Activity on the same host child', async () => {
|
||||
const promise = new Promise(() => {});
|
||||
|
||||
function Child({showInner}) {
|
||||
useLayoutEffect(() => {
|
||||
Scheduler.log('Mount layout');
|
||||
return () => {
|
||||
Scheduler.log('Unmount layout');
|
||||
};
|
||||
}, []);
|
||||
return (
|
||||
<>
|
||||
{showInner ? null : promise}
|
||||
<Text text="Child" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const portalContainer = document.createElement('div');
|
||||
|
||||
function Portal({children}) {
|
||||
return <div>{ReactDOM.createPortal(children, portalContainer)}</div>;
|
||||
}
|
||||
|
||||
const root = ReactDOMClient.createRoot(container);
|
||||
|
||||
// Prerender the whole tree.
|
||||
await act(() => {
|
||||
root.render(
|
||||
<Activity mode="hidden">
|
||||
<Portal>
|
||||
<Suspense name="Inner" fallback={<span>Loading</span>}>
|
||||
<Child showInner={true} />
|
||||
</Suspense>
|
||||
</Portal>
|
||||
</Activity>,
|
||||
);
|
||||
});
|
||||
|
||||
assertLog(['Child']);
|
||||
expect(container.innerHTML).toBe('<div style="display: none;"></div>');
|
||||
expect(portalContainer.innerHTML).toBe(
|
||||
'<span prop="Child" style="display: none;"></span>',
|
||||
);
|
||||
|
||||
// Re-suspend the inner.
|
||||
await act(() => {
|
||||
root.render(
|
||||
<Activity mode="hidden">
|
||||
<Portal>
|
||||
<Suspense name="Inner" fallback={<span>Loading</span>}>
|
||||
<Child showInner={false} />
|
||||
</Suspense>
|
||||
</Portal>
|
||||
</Activity>,
|
||||
);
|
||||
});
|
||||
assertLog([]);
|
||||
expect(container.innerHTML).toBe('<div style="display: none;"></div>');
|
||||
expect(portalContainer.innerHTML).toBe(
|
||||
'<span prop="Child" style="display: none;"></span><span style="display: none;">Loading</span>',
|
||||
);
|
||||
|
||||
// Toggle to visible while suspended.
|
||||
await act(() => {
|
||||
root.render(
|
||||
<Activity mode="visible">
|
||||
<Portal>
|
||||
<Suspense name="Inner" fallback={<span>Loading</span>}>
|
||||
<Child showInner={false} />
|
||||
</Suspense>
|
||||
</Portal>
|
||||
</Activity>,
|
||||
);
|
||||
});
|
||||
assertLog([]);
|
||||
expect(container.innerHTML).toBe('<div style=""></div>');
|
||||
expect(portalContainer.innerHTML).toBe(
|
||||
'<span prop="Child" style="display: none;"></span><span style="">Loading</span>',
|
||||
);
|
||||
|
||||
// Now reveal.
|
||||
await act(() => {
|
||||
root.render(
|
||||
<Activity mode="visible">
|
||||
<Portal>
|
||||
<Suspense name="Inner" fallback={<span>Loading</span>}>
|
||||
<Child showInner={true} />
|
||||
</Suspense>
|
||||
</Portal>
|
||||
</Activity>,
|
||||
);
|
||||
});
|
||||
assertLog(['Child', 'Mount layout']);
|
||||
expect(container.innerHTML).toBe('<div style=""></div>');
|
||||
expect(portalContainer.innerHTML).toBe(
|
||||
'<span prop="Child" style=""></span>',
|
||||
);
|
||||
});
|
||||
|
||||
// @gate enableActivity
|
||||
it('mounts/unmounts layout effects in portal when visibility changes (starting visible)', async () => {
|
||||
function Child() {
|
||||
useLayoutEffect(() => {
|
||||
Scheduler.log('Mount layout');
|
||||
return () => {
|
||||
Scheduler.log('Unmount layout');
|
||||
};
|
||||
}, []);
|
||||
return <Text text="Child" />;
|
||||
}
|
||||
|
||||
const portalContainer = document.createElement('div');
|
||||
|
||||
function Portal({children}) {
|
||||
return <div>{ReactDOM.createPortal(children, portalContainer)}</div>;
|
||||
}
|
||||
|
||||
const root = ReactDOMClient.createRoot(container);
|
||||
// Mount visible tree.
|
||||
await act(() => {
|
||||
root.render(
|
||||
<Activity mode="visible">
|
||||
<Portal>
|
||||
<Child />
|
||||
</Portal>
|
||||
</Activity>,
|
||||
);
|
||||
});
|
||||
assertLog(['Child', 'Mount layout']);
|
||||
expect(container.innerHTML).toBe('<div></div>');
|
||||
expect(portalContainer.innerHTML).toBe('<span prop="Child"></span>');
|
||||
|
||||
// Hide the tree. The layout effect is unmounted.
|
||||
await act(() => {
|
||||
root.render(
|
||||
<Activity mode="hidden">
|
||||
<Portal>
|
||||
<Child />
|
||||
</Portal>
|
||||
</Activity>,
|
||||
);
|
||||
});
|
||||
assertLog(['Unmount layout', 'Child']);
|
||||
expect(container.innerHTML).toBe('<div style="display: none;"></div>');
|
||||
expect(portalContainer.innerHTML).toBe(
|
||||
'<span prop="Child" style="display: none;"></span>',
|
||||
);
|
||||
});
|
||||
|
||||
// @gate enableActivity
|
||||
it('mounts/unmounts layout effects in portal when visibility changes (starting hidden)', async () => {
|
||||
function Child() {
|
||||
useLayoutEffect(() => {
|
||||
Scheduler.log('Mount layout');
|
||||
return () => {
|
||||
Scheduler.log('Unmount layout');
|
||||
};
|
||||
}, []);
|
||||
return <Text text="Child" />;
|
||||
}
|
||||
|
||||
const portalContainer = document.createElement('div');
|
||||
|
||||
function Portal({children}) {
|
||||
return <div>{ReactDOM.createPortal(children, portalContainer)}</div>;
|
||||
}
|
||||
|
||||
const root = ReactDOMClient.createRoot(container);
|
||||
// Mount hidden tree.
|
||||
await act(() => {
|
||||
root.render(
|
||||
<Activity mode="hidden">
|
||||
<Portal>
|
||||
<Child />
|
||||
</Portal>
|
||||
</Activity>,
|
||||
);
|
||||
});
|
||||
// No layout effect.
|
||||
assertLog(['Child']);
|
||||
expect(container.innerHTML).toBe('<div style="display: none;"></div>');
|
||||
expect(portalContainer.innerHTML).toBe(
|
||||
'<span prop="Child" style="display: none;"></span>',
|
||||
);
|
||||
|
||||
// Unhide the tree. The layout effect is mounted.
|
||||
await act(() => {
|
||||
root.render(
|
||||
<Activity mode="visible">
|
||||
<Portal>
|
||||
<Child />
|
||||
</Portal>
|
||||
</Activity>,
|
||||
);
|
||||
});
|
||||
assertLog(['Child', 'Mount layout']);
|
||||
expect(container.innerHTML).toBe('<div style=""></div>');
|
||||
expect(portalContainer.innerHTML).toBe(
|
||||
'<span prop="Child" style=""></span>',
|
||||
);
|
||||
});
|
||||
|
||||
// @gate enableLegacyHidden
|
||||
it('does not toggle effects or hide nodes for LegacyHidden component inside portal', async () => {
|
||||
function Child() {
|
||||
useLayoutEffect(() => {
|
||||
Scheduler.log('Mount layout');
|
||||
return () => {
|
||||
Scheduler.log('Unmount layout');
|
||||
};
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
Scheduler.log('Mount passive');
|
||||
return () => {
|
||||
Scheduler.log('Unmount passive');
|
||||
};
|
||||
}, []);
|
||||
return <Text text="Child" />;
|
||||
}
|
||||
|
||||
const portalContainer = document.createElement('div');
|
||||
|
||||
function Portal({children}) {
|
||||
return <div>{ReactDOM.createPortal(children, portalContainer)}</div>;
|
||||
}
|
||||
|
||||
const root = ReactDOMClient.createRoot(container);
|
||||
// Mount visible tree.
|
||||
await act(() => {
|
||||
root.render(
|
||||
<LegacyHidden mode="visible">
|
||||
<Portal>
|
||||
<Child />
|
||||
</Portal>
|
||||
</LegacyHidden>,
|
||||
);
|
||||
});
|
||||
assertLog(['Child', 'Mount layout', 'Mount passive']);
|
||||
expect(container.innerHTML).toBe('<div></div>');
|
||||
expect(portalContainer.innerHTML).toBe('<span prop="Child"></span>');
|
||||
|
||||
// Hide the tree.
|
||||
await act(() => {
|
||||
root.render(
|
||||
<LegacyHidden mode="hidden">
|
||||
<Portal>
|
||||
<Child />
|
||||
</Portal>
|
||||
</LegacyHidden>,
|
||||
);
|
||||
});
|
||||
// Effects not unmounted.
|
||||
assertLog(['Child']);
|
||||
expect(container.innerHTML).toBe('<div></div>');
|
||||
expect(portalContainer.innerHTML).toBe('<span prop="Child"></span>');
|
||||
|
||||
// Unhide the tree.
|
||||
await act(() => {
|
||||
root.render(
|
||||
<LegacyHidden mode="visible">
|
||||
<Portal>
|
||||
<Child />
|
||||
</Portal>
|
||||
</LegacyHidden>,
|
||||
);
|
||||
});
|
||||
// Effects already mounted.
|
||||
assertLog(['Child']);
|
||||
expect(container.innerHTML).toBe('<div></div>');
|
||||
expect(portalContainer.innerHTML).toBe('<span prop="Child"></span>');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -40,7 +40,6 @@ describe('created with ReactFabric called with ReactNative', () => {
|
||||
require('react-native/Libraries/ReactPrivate/ReactNativePrivateInterface').getNativeTagFromPublicInstance;
|
||||
});
|
||||
|
||||
// @gate !disableLegacyMode
|
||||
it('find Fabric instances with the RN renderer', () => {
|
||||
const View = createReactNativeComponentClass('RCTView', () => ({
|
||||
validAttributes: {title: true},
|
||||
@@ -61,7 +60,6 @@ describe('created with ReactFabric called with ReactNative', () => {
|
||||
expect(getNativeTagFromPublicInstance(instance)).toBe(2);
|
||||
});
|
||||
|
||||
// @gate !disableLegacyMode
|
||||
it('find Fabric nodes with the RN renderer', () => {
|
||||
const View = createReactNativeComponentClass('RCTView', () => ({
|
||||
validAttributes: {title: true},
|
||||
@@ -82,7 +80,6 @@ describe('created with ReactFabric called with ReactNative', () => {
|
||||
expect(handle).toBe(2);
|
||||
});
|
||||
|
||||
// @gate !disableLegacyMode
|
||||
it('dispatches commands on Fabric nodes with the RN renderer', () => {
|
||||
nativeFabricUIManager.dispatchCommand.mockClear();
|
||||
const View = createReactNativeComponentClass('RCTView', () => ({
|
||||
@@ -104,7 +101,6 @@ describe('created with ReactFabric called with ReactNative', () => {
|
||||
expect(UIManager.dispatchViewManagerCommand).not.toBeCalled();
|
||||
});
|
||||
|
||||
// @gate !disableLegacyMode
|
||||
it('dispatches sendAccessibilityEvent on Fabric nodes with the RN renderer', () => {
|
||||
nativeFabricUIManager.sendAccessibilityEvent.mockClear();
|
||||
const View = createReactNativeComponentClass('RCTView', () => ({
|
||||
@@ -147,7 +143,6 @@ describe('created with ReactNative called with ReactFabric', () => {
|
||||
.ReactNativeViewConfigRegistry.register;
|
||||
});
|
||||
|
||||
// @gate !disableLegacyMode
|
||||
it('find Paper instances with the Fabric renderer', () => {
|
||||
const View = createReactNativeComponentClass('RCTView', () => ({
|
||||
validAttributes: {title: true},
|
||||
@@ -168,7 +163,6 @@ describe('created with ReactNative called with ReactFabric', () => {
|
||||
expect(instance._nativeTag).toBe(3);
|
||||
});
|
||||
|
||||
// @gate !disableLegacyMode
|
||||
it('find Paper nodes with the Fabric renderer', () => {
|
||||
const View = createReactNativeComponentClass('RCTView', () => ({
|
||||
validAttributes: {title: true},
|
||||
@@ -189,7 +183,6 @@ describe('created with ReactNative called with ReactFabric', () => {
|
||||
expect(handle).toBe(3);
|
||||
});
|
||||
|
||||
// @gate !disableLegacyMode
|
||||
it('dispatches commands on Paper nodes with the Fabric renderer', () => {
|
||||
UIManager.dispatchViewManagerCommand.mockReset();
|
||||
const View = createReactNativeComponentClass('RCTView', () => ({
|
||||
@@ -212,7 +205,6 @@ describe('created with ReactNative called with ReactFabric', () => {
|
||||
expect(nativeFabricUIManager.dispatchCommand).not.toBeCalled();
|
||||
});
|
||||
|
||||
// @gate !disableLegacyMode
|
||||
it('dispatches sendAccessibilityEvent on Paper nodes with the Fabric renderer', () => {
|
||||
ReactNativePrivateInterface.legacySendAccessibilityEvent.mockReset();
|
||||
const View = createReactNativeComponentClass('RCTView', () => ({
|
||||
|
||||
@@ -5,18 +5,11 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @emails react-core
|
||||
* @jest-environment ./scripts/jest/ReactDOMServerIntegrationEnvironment
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
// Polyfills for test environment
|
||||
global.ReadableStream =
|
||||
require('web-streams-polyfill/ponyfill/es6').ReadableStream;
|
||||
global.WritableStream =
|
||||
require('web-streams-polyfill/ponyfill/es6').WritableStream;
|
||||
global.TextEncoder = require('util').TextEncoder;
|
||||
global.TextDecoder = require('util').TextDecoder;
|
||||
|
||||
let clientExports;
|
||||
let turbopackMap;
|
||||
let turbopackModules;
|
||||
|
||||
@@ -5,17 +5,11 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @emails react-core
|
||||
* @jest-environment ./scripts/jest/ReactDOMServerIntegrationEnvironment
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
// Polyfills for test environment
|
||||
global.ReadableStream =
|
||||
require('web-streams-polyfill/ponyfill/es6').ReadableStream;
|
||||
global.TextEncoder = require('util').TextEncoder;
|
||||
global.TextDecoder = require('util').TextDecoder;
|
||||
|
||||
// let serverExports;
|
||||
let turbopackServerMap;
|
||||
let ReactServerDOMServer;
|
||||
let ReactServerDOMClient;
|
||||
@@ -29,7 +23,6 @@ describe('ReactFlightDOMTurbopackReply', () => {
|
||||
require('react-server-dom-turbopack/server.edge'),
|
||||
);
|
||||
const TurbopackMock = require('./utils/TurbopackMock');
|
||||
// serverExports = TurbopackMock.serverExports;
|
||||
turbopackServerMap = TurbopackMock.turbopackServerMap;
|
||||
ReactServerDOMServer = require('react-server-dom-turbopack/server.edge');
|
||||
jest.resetModules();
|
||||
|
||||
@@ -10,18 +10,6 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
// Polyfills for test environment
|
||||
global.ReadableStream =
|
||||
require('web-streams-polyfill/ponyfill/es6').ReadableStream;
|
||||
global.WritableStream =
|
||||
require('web-streams-polyfill/ponyfill/es6').WritableStream;
|
||||
global.TextEncoder = require('util').TextEncoder;
|
||||
global.TextDecoder = require('util').TextDecoder;
|
||||
global.Blob = require('buffer').Blob;
|
||||
if (typeof File === 'undefined' || typeof FormData === 'undefined') {
|
||||
global.File = require('buffer').File || require('undici').File;
|
||||
global.FormData = require('undici').FormData;
|
||||
}
|
||||
// Patch for Edge environments for global scope
|
||||
global.AsyncLocalStorage = require('async_hooks').AsyncLocalStorage;
|
||||
|
||||
@@ -127,8 +115,16 @@ describe('ReactFlightDOMEdge', () => {
|
||||
chunk.set(prevChunk, 0);
|
||||
chunk.set(value, prevChunk.length);
|
||||
if (chunk.length > 50) {
|
||||
// Copy the part we're keeping (prevChunk) to avoid buffer
|
||||
// transfer. When we enqueue the partial chunk below, downstream
|
||||
// consumers (like byte streams in the Flight Client) may detach
|
||||
// the underlying buffer. Since prevChunk would share the same
|
||||
// buffer, we copy it first so it has its own independent buffer.
|
||||
// TODO: Should we just use {type: 'bytes'} for this stream to
|
||||
// always transfer ownership, and not only "accidentally" when we
|
||||
// enqueue in the Flight Client?
|
||||
prevChunk = chunk.slice(chunk.length - 50);
|
||||
controller.enqueue(chunk.subarray(0, chunk.length - 50));
|
||||
prevChunk = chunk.subarray(chunk.length - 50);
|
||||
} else {
|
||||
// Wait to see if we get some more bytes to join in.
|
||||
prevChunk = chunk;
|
||||
@@ -1118,25 +1114,121 @@ describe('ReactFlightDOMEdge', () => {
|
||||
expect(streamedBuffers).toEqual(buffers);
|
||||
});
|
||||
|
||||
it('should support binary ReadableStreams', async () => {
|
||||
const encoder = new TextEncoder();
|
||||
const words = ['Hello', 'streaming', 'world'];
|
||||
|
||||
const stream = new ReadableStream({
|
||||
type: 'bytes',
|
||||
async start(controller) {
|
||||
for (let i = 0; i < words.length; i++) {
|
||||
const chunk = encoder.encode(words[i] + ' ');
|
||||
controller.enqueue(chunk);
|
||||
}
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
|
||||
const rscStream = await serverAct(() =>
|
||||
ReactServerDOMServer.renderToReadableStream(stream, {}),
|
||||
);
|
||||
|
||||
const result = await ReactServerDOMClient.createFromReadableStream(
|
||||
rscStream,
|
||||
{
|
||||
serverConsumerManifest: {
|
||||
moduleMap: null,
|
||||
moduleLoading: null,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const reader = result.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
let text = '';
|
||||
let entry;
|
||||
while (!(entry = await reader.read()).done) {
|
||||
text += decoder.decode(entry.value);
|
||||
}
|
||||
|
||||
expect(text).toBe('Hello streaming world ');
|
||||
});
|
||||
|
||||
it('should support large binary ReadableStreams', async () => {
|
||||
const chunkCount = 100;
|
||||
const chunkSize = 1024;
|
||||
const expectedBytes = [];
|
||||
|
||||
const stream = new ReadableStream({
|
||||
type: 'bytes',
|
||||
start(controller) {
|
||||
for (let i = 0; i < chunkCount; i++) {
|
||||
const chunk = new Uint8Array(chunkSize);
|
||||
for (let j = 0; j < chunkSize; j++) {
|
||||
chunk[j] = (i + j) % 256;
|
||||
}
|
||||
expectedBytes.push(...Array.from(chunk));
|
||||
controller.enqueue(chunk);
|
||||
}
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
|
||||
const rscStream = await serverAct(() =>
|
||||
ReactServerDOMServer.renderToReadableStream(stream, {}),
|
||||
);
|
||||
|
||||
const result = await ReactServerDOMClient.createFromReadableStream(
|
||||
// Use passThrough to split and rejoin chunks at arbitrary boundaries.
|
||||
passThrough(rscStream),
|
||||
{
|
||||
serverConsumerManifest: {
|
||||
moduleMap: null,
|
||||
moduleLoading: null,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const reader = result.getReader();
|
||||
const receivedBytes = [];
|
||||
let entry;
|
||||
while (!(entry = await reader.read()).done) {
|
||||
expect(entry.value instanceof Uint8Array).toBe(true);
|
||||
receivedBytes.push(...Array.from(entry.value));
|
||||
}
|
||||
|
||||
expect(receivedBytes).toEqual(expectedBytes);
|
||||
});
|
||||
|
||||
it('should support BYOB binary ReadableStreams', async () => {
|
||||
const buffer = new Uint8Array([
|
||||
const sourceBytes = [
|
||||
123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20,
|
||||
]).buffer;
|
||||
const buffers = [
|
||||
new Int8Array(buffer, 1),
|
||||
new Uint8Array(buffer, 2),
|
||||
new Uint8ClampedArray(buffer, 2),
|
||||
new Int16Array(buffer, 2),
|
||||
new Uint16Array(buffer, 2),
|
||||
new Int32Array(buffer, 4),
|
||||
new Uint32Array(buffer, 4),
|
||||
new Float32Array(buffer, 4),
|
||||
new Float64Array(buffer, 0),
|
||||
new BigInt64Array(buffer, 0),
|
||||
new BigUint64Array(buffer, 0),
|
||||
new DataView(buffer, 3),
|
||||
];
|
||||
|
||||
// Create separate buffers for each typed array to avoid ArrayBuffer
|
||||
// transfer issues. Each view needs its own buffer because enqueue()
|
||||
// transfers ownership.
|
||||
const buffers = [
|
||||
new Int8Array(sourceBytes.slice(1)),
|
||||
new Uint8Array(sourceBytes.slice(2)),
|
||||
new Uint8ClampedArray(sourceBytes.slice(2)),
|
||||
new Int16Array(new Uint8Array(sourceBytes.slice(2)).buffer),
|
||||
new Uint16Array(new Uint8Array(sourceBytes.slice(2)).buffer),
|
||||
new Int32Array(new Uint8Array(sourceBytes.slice(4)).buffer),
|
||||
new Uint32Array(new Uint8Array(sourceBytes.slice(4)).buffer),
|
||||
new Float32Array(new Uint8Array(sourceBytes.slice(4)).buffer),
|
||||
new Float64Array(new Uint8Array(sourceBytes.slice(0)).buffer),
|
||||
new BigInt64Array(new Uint8Array(sourceBytes.slice(0)).buffer),
|
||||
new BigUint64Array(new Uint8Array(sourceBytes.slice(0)).buffer),
|
||||
new DataView(new Uint8Array(sourceBytes.slice(3)).buffer),
|
||||
];
|
||||
|
||||
// Save expected bytes before enqueueing (which will detach the buffers).
|
||||
const expectedBytes = buffers.flatMap(c =>
|
||||
Array.from(new Uint8Array(c.buffer, c.byteOffset, c.byteLength)),
|
||||
);
|
||||
|
||||
// This a binary stream where each chunk ends up as Uint8Array.
|
||||
const s = new ReadableStream({
|
||||
type: 'bytes',
|
||||
@@ -1176,11 +1268,7 @@ describe('ReactFlightDOMEdge', () => {
|
||||
|
||||
// The streamed buffers might be in different chunks and in Uint8Array form but
|
||||
// the concatenated bytes should be the same.
|
||||
expect(streamedBuffers.flatMap(t => Array.from(t))).toEqual(
|
||||
buffers.flatMap(c =>
|
||||
Array.from(new Uint8Array(c.buffer, c.byteOffset, c.byteLength)),
|
||||
),
|
||||
);
|
||||
expect(streamedBuffers.flatMap(t => Array.from(t))).toEqual(expectedBytes);
|
||||
});
|
||||
|
||||
// @gate !__DEV__ || enableComponentPerformanceTrack
|
||||
|
||||
@@ -10,18 +10,6 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
// Polyfills for test environment
|
||||
global.ReadableStream =
|
||||
require('web-streams-polyfill/ponyfill/es6').ReadableStream;
|
||||
global.TextEncoder = require('util').TextEncoder;
|
||||
global.TextDecoder = require('util').TextDecoder;
|
||||
|
||||
global.Blob = require('buffer').Blob;
|
||||
if (typeof File === 'undefined' || typeof FormData === 'undefined') {
|
||||
global.File = require('buffer').File || require('undici').File;
|
||||
global.FormData = require('undici').FormData;
|
||||
}
|
||||
|
||||
let serverExports;
|
||||
let webpackServerMap;
|
||||
let ReactServerDOMServer;
|
||||
@@ -194,24 +182,33 @@ describe('ReactFlightDOMReplyEdge', () => {
|
||||
});
|
||||
|
||||
it('should support BYOB binary ReadableStreams', async () => {
|
||||
const buffer = new Uint8Array([
|
||||
const sourceBytes = [
|
||||
123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20,
|
||||
]).buffer;
|
||||
const buffers = [
|
||||
new Int8Array(buffer, 1),
|
||||
new Uint8Array(buffer, 2),
|
||||
new Uint8ClampedArray(buffer, 2),
|
||||
new Int16Array(buffer, 2),
|
||||
new Uint16Array(buffer, 2),
|
||||
new Int32Array(buffer, 4),
|
||||
new Uint32Array(buffer, 4),
|
||||
new Float32Array(buffer, 4),
|
||||
new Float64Array(buffer, 0),
|
||||
new BigInt64Array(buffer, 0),
|
||||
new BigUint64Array(buffer, 0),
|
||||
new DataView(buffer, 3),
|
||||
];
|
||||
|
||||
// Create separate buffers for each typed array to avoid ArrayBuffer
|
||||
// transfer issues. Each view needs its own buffer because enqueue()
|
||||
// transfers ownership.
|
||||
const buffers = [
|
||||
new Int8Array(sourceBytes.slice(1)),
|
||||
new Uint8Array(sourceBytes.slice(2)),
|
||||
new Uint8ClampedArray(sourceBytes.slice(2)),
|
||||
new Int16Array(new Uint8Array(sourceBytes.slice(2)).buffer),
|
||||
new Uint16Array(new Uint8Array(sourceBytes.slice(2)).buffer),
|
||||
new Int32Array(new Uint8Array(sourceBytes.slice(4)).buffer),
|
||||
new Uint32Array(new Uint8Array(sourceBytes.slice(4)).buffer),
|
||||
new Float32Array(new Uint8Array(sourceBytes.slice(4)).buffer),
|
||||
new Float64Array(new Uint8Array(sourceBytes.slice(0)).buffer),
|
||||
new BigInt64Array(new Uint8Array(sourceBytes.slice(0)).buffer),
|
||||
new BigUint64Array(new Uint8Array(sourceBytes.slice(0)).buffer),
|
||||
new DataView(new Uint8Array(sourceBytes.slice(3)).buffer),
|
||||
];
|
||||
|
||||
// Save expected bytes before enqueueing (which will detach the buffers).
|
||||
const expectedBytes = buffers.flatMap(c =>
|
||||
Array.from(new Uint8Array(c.buffer, c.byteOffset, c.byteLength)),
|
||||
);
|
||||
|
||||
// This a binary stream where each chunk ends up as Uint8Array.
|
||||
const s = new ReadableStream({
|
||||
type: 'bytes',
|
||||
@@ -239,11 +236,7 @@ describe('ReactFlightDOMReplyEdge', () => {
|
||||
|
||||
// The streamed buffers might be in different chunks and in Uint8Array form but
|
||||
// the concatenated bytes should be the same.
|
||||
expect(streamedBuffers.flatMap(t => Array.from(t))).toEqual(
|
||||
buffers.flatMap(c =>
|
||||
Array.from(new Uint8Array(c.buffer, c.byteOffset, c.byteLength)),
|
||||
),
|
||||
);
|
||||
expect(streamedBuffers.flatMap(t => Array.from(t))).toEqual(expectedBytes);
|
||||
});
|
||||
|
||||
it('should abort when parsing an incomplete payload', async () => {
|
||||
|
||||
14
packages/react-server/src/ReactFlightServer.js
vendored
14
packages/react-server/src/ReactFlightServer.js
vendored
@@ -1149,6 +1149,8 @@ function serializeReadableStream(
|
||||
supportsBYOB = false;
|
||||
}
|
||||
}
|
||||
// At this point supportsBYOB is guaranteed to be a boolean.
|
||||
const isByteStream: boolean = supportsBYOB;
|
||||
|
||||
const reader = stream.getReader();
|
||||
|
||||
@@ -1172,7 +1174,7 @@ function serializeReadableStream(
|
||||
// The task represents the Stop row. This adds a Start row.
|
||||
request.pendingChunks++;
|
||||
const startStreamRow =
|
||||
streamTask.id.toString(16) + ':' + (supportsBYOB ? 'r' : 'R') + '\n';
|
||||
streamTask.id.toString(16) + ':' + (isByteStream ? 'r' : 'R') + '\n';
|
||||
request.completedRegularChunks.push(stringToChunk(startStreamRow));
|
||||
|
||||
function progress(entry: {done: boolean, value: ReactClientValue, ...}) {
|
||||
@@ -1190,9 +1192,15 @@ function serializeReadableStream(
|
||||
callOnAllReadyIfReady(request);
|
||||
} else {
|
||||
try {
|
||||
streamTask.model = entry.value;
|
||||
request.pendingChunks++;
|
||||
tryStreamTask(request, streamTask);
|
||||
streamTask.model = entry.value;
|
||||
if (isByteStream) {
|
||||
// Chunks of byte streams are always Uint8Array instances.
|
||||
const chunk: Uint8Array = (streamTask.model: any);
|
||||
emitTypedArrayChunk(request, streamTask.id, 'b', chunk, false);
|
||||
} else {
|
||||
tryStreamTask(request, streamTask);
|
||||
}
|
||||
enqueueFlush(request);
|
||||
reader.read().then(progress, error);
|
||||
} catch (x) {
|
||||
|
||||
@@ -36,7 +36,7 @@ export const disableCommentsAsDOMContainers: boolean = true;
|
||||
export const disableInputAttributeSyncing: boolean = false;
|
||||
export const disableLegacyContext: boolean = false;
|
||||
export const disableLegacyContextForFunctionComponents: boolean = false;
|
||||
export const disableLegacyMode: boolean = true;
|
||||
export const disableLegacyMode: boolean = false;
|
||||
export const disableSchedulerTimeoutInWorkLoop: boolean = false;
|
||||
export const disableTextareaChildren: boolean = false;
|
||||
export const enableAsyncDebugInfo: boolean = false;
|
||||
|
||||
@@ -21,7 +21,7 @@ export const disableCommentsAsDOMContainers: boolean = true;
|
||||
export const disableInputAttributeSyncing: boolean = false;
|
||||
export const disableLegacyContext: boolean = true;
|
||||
export const disableLegacyContextForFunctionComponents: boolean = true;
|
||||
export const disableLegacyMode: boolean = true;
|
||||
export const disableLegacyMode: boolean = false;
|
||||
export const disableSchedulerTimeoutInWorkLoop: boolean = false;
|
||||
export const disableTextareaChildren: boolean = false;
|
||||
export const enableAsyncDebugInfo: boolean = false;
|
||||
|
||||
Reference in New Issue
Block a user