Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8219b34b60 |
@@ -520,8 +520,7 @@ function printErrorSummary(category: ErrorCategory, message: string): string {
|
||||
case ErrorCategory.AutomaticEffectDependencies:
|
||||
case ErrorCategory.CapitalizedCalls:
|
||||
case ErrorCategory.Config:
|
||||
case ErrorCategory.EffectDerivationDeriveInRender:
|
||||
case ErrorCategory.EffectDerivationShadowingParentState:
|
||||
case ErrorCategory.EffectDerivationsOfState:
|
||||
case ErrorCategory.EffectSetState:
|
||||
case ErrorCategory.ErrorBoundaries:
|
||||
case ErrorCategory.Factories:
|
||||
@@ -615,14 +614,7 @@ export enum ErrorCategory {
|
||||
* Checks for no setState in effect bodies
|
||||
*/
|
||||
EffectSetState = 'EffectSetState',
|
||||
/**
|
||||
* Checks for derived state in effects that could be calculated in render
|
||||
*/
|
||||
EffectDerivationDeriveInRender = 'EffectDerivationDeriveInRender',
|
||||
/**
|
||||
* Checks for derived state in effects that could be hoisted to parent
|
||||
*/
|
||||
EffectDerivationShadowingParentState = 'EffectDerivationShadowingParentState',
|
||||
EffectDerivationsOfState = 'EffectDerivationsOfState',
|
||||
/**
|
||||
* Validates against try/catch in place of error boundaries
|
||||
*/
|
||||
@@ -759,23 +751,13 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.EffectDerivationDeriveInRender: {
|
||||
case ErrorCategory.EffectDerivationsOfState: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'effect-derive-in-render',
|
||||
name: 'no-deriving-state-in-effects',
|
||||
description:
|
||||
'Validates if a useEffect is deriving state from props and/or local state that could be calculated in render.',
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
case ErrorCategory.EffectDerivationShadowingParentState: {
|
||||
return {
|
||||
category,
|
||||
severity: ErrorSeverity.Error,
|
||||
name: 'effect-shadow-parent-state',
|
||||
description:
|
||||
'Validates if a useEffect is deriving state from parent state and if the component is updating the shadowed state locally.',
|
||||
'Validates against deriving values from state in an effect',
|
||||
recommended: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -325,15 +325,6 @@ function runWithEnvironment(
|
||||
outlineJSX(hir);
|
||||
}
|
||||
|
||||
if (env.config.enableNameAnonymousFunctions) {
|
||||
nameAnonymousFunctions(hir);
|
||||
log({
|
||||
kind: 'hir',
|
||||
name: 'NameAnonymousFunctions',
|
||||
value: hir,
|
||||
});
|
||||
}
|
||||
|
||||
if (env.config.enableFunctionOutlining) {
|
||||
outlineFunctions(hir, fbtOperands);
|
||||
log({kind: 'hir', name: 'OutlineFunctions', value: hir});
|
||||
@@ -424,6 +415,15 @@ function runWithEnvironment(
|
||||
});
|
||||
}
|
||||
|
||||
if (env.config.enableNameAnonymousFunctions) {
|
||||
nameAnonymousFunctions(hir);
|
||||
log({
|
||||
kind: 'hir',
|
||||
name: 'NameAnonymougFunctions',
|
||||
value: hir,
|
||||
});
|
||||
}
|
||||
|
||||
const reactiveFunction = buildReactiveFunction(hir);
|
||||
log({
|
||||
kind: 'reactive',
|
||||
|
||||
@@ -47,7 +47,6 @@ import {
|
||||
makePropertyLiteral,
|
||||
makeType,
|
||||
promoteTemporary,
|
||||
validateIdentifierName,
|
||||
} from './HIR';
|
||||
import HIRBuilder, {Bindings, createTemporaryPlace} from './HIRBuilder';
|
||||
import {BuiltInArrayId} from './ObjectShape';
|
||||
@@ -214,16 +213,6 @@ export function lower(
|
||||
);
|
||||
}
|
||||
|
||||
let validatedId: HIRFunction['id'] = null;
|
||||
if (id != null) {
|
||||
const idResult = validateIdentifierName(id);
|
||||
if (idResult.isErr()) {
|
||||
builder.errors.merge(idResult.unwrapErr());
|
||||
} else {
|
||||
validatedId = idResult.unwrap().value;
|
||||
}
|
||||
}
|
||||
|
||||
if (builder.errors.hasAnyErrors()) {
|
||||
return Err(builder.errors);
|
||||
}
|
||||
@@ -245,8 +234,7 @@ export function lower(
|
||||
);
|
||||
|
||||
return Ok({
|
||||
id: validatedId,
|
||||
nameHint: null,
|
||||
id,
|
||||
params,
|
||||
fnType: bindings == null ? env.fnType : 'Other',
|
||||
returnTypeAnnotation: null, // TODO: extract the actual return type node if present
|
||||
@@ -3575,14 +3563,19 @@ function lowerFunctionToValue(
|
||||
): InstructionValue {
|
||||
const exprNode = expr.node;
|
||||
const exprLoc = exprNode.loc ?? GeneratedSource;
|
||||
let name: string | null = null;
|
||||
if (expr.isFunctionExpression()) {
|
||||
name = expr.get('id')?.node?.name ?? null;
|
||||
} else if (expr.isFunctionDeclaration()) {
|
||||
name = expr.get('id')?.node?.name ?? null;
|
||||
}
|
||||
const loweredFunc = lowerFunction(builder, expr);
|
||||
if (!loweredFunc) {
|
||||
return {kind: 'UnsupportedNode', node: exprNode, loc: exprLoc};
|
||||
}
|
||||
return {
|
||||
kind: 'FunctionExpression',
|
||||
name: loweredFunc.func.id,
|
||||
nameHint: null,
|
||||
name,
|
||||
type: expr.node.type,
|
||||
loc: exprLoc,
|
||||
loweredFunc,
|
||||
|
||||
@@ -7,11 +7,7 @@
|
||||
|
||||
import {BindingKind} from '@babel/traverse';
|
||||
import * as t from '@babel/types';
|
||||
import {
|
||||
CompilerDiagnostic,
|
||||
CompilerError,
|
||||
ErrorCategory,
|
||||
} from '../CompilerError';
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
import {Environment, ReactFunctionType} from './Environment';
|
||||
import type {HookKind} from './ObjectShape';
|
||||
@@ -58,8 +54,7 @@ export type SourceLocation = t.SourceLocation | typeof GeneratedSource;
|
||||
*/
|
||||
export type ReactiveFunction = {
|
||||
loc: SourceLocation;
|
||||
id: ValidIdentifierName | null;
|
||||
nameHint: string | null;
|
||||
id: string | null;
|
||||
params: Array<Place | SpreadPattern>;
|
||||
generator: boolean;
|
||||
async: boolean;
|
||||
@@ -281,8 +276,7 @@ export type ReactiveTryTerminal = {
|
||||
// A function lowered to HIR form, ie where its body is lowered to an HIR control-flow graph
|
||||
export type HIRFunction = {
|
||||
loc: SourceLocation;
|
||||
id: ValidIdentifierName | null;
|
||||
nameHint: string | null;
|
||||
id: string | null;
|
||||
fnType: ReactFunctionType;
|
||||
env: Environment;
|
||||
params: Array<Place | SpreadPattern>;
|
||||
@@ -1130,8 +1124,7 @@ export type JsxAttribute =
|
||||
|
||||
export type FunctionExpression = {
|
||||
kind: 'FunctionExpression';
|
||||
name: ValidIdentifierName | null;
|
||||
nameHint: string | null;
|
||||
name: string | null;
|
||||
loweredFunc: LoweredFunction;
|
||||
type:
|
||||
| 'ArrowFunctionExpression'
|
||||
@@ -1308,41 +1301,11 @@ export function forkTemporaryIdentifier(
|
||||
|
||||
export function validateIdentifierName(
|
||||
name: string,
|
||||
): Result<ValidatedIdentifier, CompilerError> {
|
||||
if (isReservedWord(name)) {
|
||||
const error = new CompilerError();
|
||||
error.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Syntax,
|
||||
reason: 'Expected a non-reserved identifier name',
|
||||
description: `\`${name}\` is a reserved word in JavaScript and cannot be used as an identifier name`,
|
||||
suggestions: null,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: 'reserved word',
|
||||
}),
|
||||
);
|
||||
return Err(error);
|
||||
} else if (!t.isValidIdentifier(name)) {
|
||||
const error = new CompilerError();
|
||||
error.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Syntax,
|
||||
reason: `Expected a valid identifier name`,
|
||||
description: `\`${name}\` is not a valid JavaScript identifier`,
|
||||
suggestions: null,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: 'reserved word',
|
||||
}),
|
||||
);
|
||||
): Result<ValidIdentifierName, null> {
|
||||
if (isReservedWord(name) || !t.isValidIdentifier(name)) {
|
||||
return Err(null);
|
||||
}
|
||||
return Ok({
|
||||
kind: 'named',
|
||||
value: name as ValidIdentifierName,
|
||||
});
|
||||
return Ok(makeIdentifierName(name).value);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1351,7 +1314,31 @@ export function validateIdentifierName(
|
||||
* original source code.
|
||||
*/
|
||||
export function makeIdentifierName(name: string): ValidatedIdentifier {
|
||||
return validateIdentifierName(name).unwrap();
|
||||
if (isReservedWord(name)) {
|
||||
CompilerError.throwInvalidJS({
|
||||
reason: 'Expected a non-reserved identifier name',
|
||||
loc: GeneratedSource,
|
||||
description: `\`${name}\` is a reserved word in JavaScript and cannot be used as an identifier name`,
|
||||
suggestions: null,
|
||||
});
|
||||
} else {
|
||||
CompilerError.invariant(t.isValidIdentifier(name), {
|
||||
reason: `Expected a valid identifier name`,
|
||||
description: `\`${name}\` is not a valid JavaScript identifier`,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: GeneratedSource,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
return {
|
||||
kind: 'named',
|
||||
value: name as ValidIdentifierName,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -56,9 +56,6 @@ export function printFunction(fn: HIRFunction): string {
|
||||
} else {
|
||||
definition += '<<anonymous>>';
|
||||
}
|
||||
if (fn.nameHint != null) {
|
||||
definition += ` ${fn.nameHint}`;
|
||||
}
|
||||
if (fn.params.length !== 0) {
|
||||
definition +=
|
||||
'(' +
|
||||
|
||||
@@ -748,10 +748,14 @@ function applyEffect(
|
||||
case 'Alias':
|
||||
case 'Capture': {
|
||||
CompilerError.invariant(
|
||||
effect.kind === 'Capture' || initialized.has(effect.into.identifier.id),
|
||||
effect.kind === 'Capture' ||
|
||||
effect.kind === 'MaybeAlias' ||
|
||||
initialized.has(effect.into.identifier.id),
|
||||
{
|
||||
reason: `Expected destination value to already be initialized within this instruction for Alias effect`,
|
||||
description: `Destination ${printPlace(effect.into)} is not initialized in this instruction`,
|
||||
reason: `Expected destination to already be initialized within this instruction`,
|
||||
description:
|
||||
`Destination ${printPlace(effect.into)} is not initialized in this ` +
|
||||
`instruction for effect ${printAliasingEffect(effect)}`,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
@@ -767,49 +771,67 @@ function applyEffect(
|
||||
* copy-on-write semantics, then we can prune the effect
|
||||
*/
|
||||
const intoKind = state.kind(effect.into).kind;
|
||||
let isMutableDesination: boolean;
|
||||
let destinationType: 'context' | 'mutable' | null = null;
|
||||
switch (intoKind) {
|
||||
case ValueKind.Context:
|
||||
case ValueKind.Mutable:
|
||||
case ValueKind.MaybeFrozen: {
|
||||
isMutableDesination = true;
|
||||
case ValueKind.Context: {
|
||||
destinationType = 'context';
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
isMutableDesination = false;
|
||||
case ValueKind.Mutable:
|
||||
case ValueKind.MaybeFrozen: {
|
||||
destinationType = 'mutable';
|
||||
break;
|
||||
}
|
||||
}
|
||||
const fromKind = state.kind(effect.from).kind;
|
||||
let isMutableReferenceType: boolean;
|
||||
let sourceType: 'context' | 'mutable' | 'frozen' | null = null;
|
||||
switch (fromKind) {
|
||||
case ValueKind.Context: {
|
||||
sourceType = 'context';
|
||||
break;
|
||||
}
|
||||
case ValueKind.Global:
|
||||
case ValueKind.Primitive: {
|
||||
isMutableReferenceType = false;
|
||||
break;
|
||||
}
|
||||
case ValueKind.Frozen: {
|
||||
isMutableReferenceType = false;
|
||||
applyEffect(
|
||||
context,
|
||||
state,
|
||||
{
|
||||
kind: 'ImmutableCapture',
|
||||
from: effect.from,
|
||||
into: effect.into,
|
||||
},
|
||||
initialized,
|
||||
effects,
|
||||
);
|
||||
sourceType = 'frozen';
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
isMutableReferenceType = true;
|
||||
sourceType = 'mutable';
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isMutableDesination && isMutableReferenceType) {
|
||||
|
||||
if (sourceType === 'frozen') {
|
||||
applyEffect(
|
||||
context,
|
||||
state,
|
||||
{
|
||||
kind: 'ImmutableCapture',
|
||||
from: effect.from,
|
||||
into: effect.into,
|
||||
},
|
||||
initialized,
|
||||
effects,
|
||||
);
|
||||
} else if (
|
||||
(sourceType === 'mutable' && destinationType === 'mutable') ||
|
||||
effect.kind === 'MaybeAlias'
|
||||
) {
|
||||
effects.push(effect);
|
||||
} else if (
|
||||
(sourceType === 'context' && destinationType != null) ||
|
||||
(sourceType === 'mutable' && destinationType === 'context')
|
||||
) {
|
||||
applyEffect(
|
||||
context,
|
||||
state,
|
||||
{kind: 'MaybeAlias', from: effect.from, into: effect.into},
|
||||
initialized,
|
||||
effects,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -779,7 +779,13 @@ class AliasingState {
|
||||
if (edge.index >= index) {
|
||||
break;
|
||||
}
|
||||
queue.push({place: edge.node, transitive, direction: 'forwards', kind});
|
||||
queue.push({
|
||||
place: edge.node,
|
||||
transitive,
|
||||
direction: 'forwards',
|
||||
// Traversing a maybeAlias edge always downgrades to conditional mutation
|
||||
kind: edge.kind === 'maybeAlias' ? MutationKind.Conditional : kind,
|
||||
});
|
||||
}
|
||||
for (const [alias, when] of node.createdFrom) {
|
||||
if (when >= index) {
|
||||
@@ -807,7 +813,12 @@ class AliasingState {
|
||||
if (when >= index) {
|
||||
continue;
|
||||
}
|
||||
queue.push({place: alias, transitive, direction: 'backwards', kind});
|
||||
queue.push({
|
||||
place: alias,
|
||||
transitive,
|
||||
direction: 'backwards',
|
||||
kind,
|
||||
});
|
||||
}
|
||||
/**
|
||||
* MaybeAlias indicates potential data flow from unknown function calls,
|
||||
|
||||
@@ -249,7 +249,6 @@ function emitSelectorFn(env: Environment, keys: Array<string>): Instruction {
|
||||
const fn: HIRFunction = {
|
||||
loc: GeneratedSource,
|
||||
id: null,
|
||||
nameHint: null,
|
||||
fnType: 'Other',
|
||||
env,
|
||||
params: [obj],
|
||||
@@ -276,7 +275,6 @@ function emitSelectorFn(env: Environment, keys: Array<string>): Instruction {
|
||||
value: {
|
||||
kind: 'FunctionExpression',
|
||||
name: null,
|
||||
nameHint: null,
|
||||
loweredFunc: {
|
||||
func: fn,
|
||||
},
|
||||
|
||||
@@ -31,9 +31,7 @@ export function outlineFunctions(
|
||||
) {
|
||||
const loweredFunc = value.loweredFunc.func;
|
||||
|
||||
const id = fn.env.generateGloballyUniqueIdentifierName(
|
||||
loweredFunc.id ?? loweredFunc.nameHint,
|
||||
);
|
||||
const id = fn.env.generateGloballyUniqueIdentifierName(loweredFunc.id);
|
||||
loweredFunc.id = id.value;
|
||||
|
||||
fn.env.outlineFunction(loweredFunc, null);
|
||||
|
||||
@@ -364,7 +364,6 @@ function emitOutlinedFn(
|
||||
const fn: HIRFunction = {
|
||||
loc: GeneratedSource,
|
||||
id: null,
|
||||
nameHint: null,
|
||||
fnType: 'Other',
|
||||
env,
|
||||
params: [propsObj],
|
||||
|
||||
@@ -44,7 +44,6 @@ export function buildReactiveFunction(fn: HIRFunction): ReactiveFunction {
|
||||
return {
|
||||
loc: fn.loc,
|
||||
id: fn.id,
|
||||
nameHint: fn.nameHint,
|
||||
params: fn.params,
|
||||
generator: fn.generator,
|
||||
async: fn.async,
|
||||
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
ValidIdentifierName,
|
||||
getHookKind,
|
||||
makeIdentifierName,
|
||||
validateIdentifierName,
|
||||
} from '../HIR/HIR';
|
||||
import {printIdentifier, printInstruction, printPlace} from '../HIR/PrintHIR';
|
||||
import {eachPatternOperand} from '../HIR/visitors';
|
||||
@@ -61,7 +62,6 @@ export const EARLY_RETURN_SENTINEL = 'react.early_return_sentinel';
|
||||
export type CodegenFunction = {
|
||||
type: 'CodegenFunction';
|
||||
id: t.Identifier | null;
|
||||
nameHint: string | null;
|
||||
params: t.FunctionDeclaration['params'];
|
||||
body: t.BlockStatement;
|
||||
generator: boolean;
|
||||
@@ -384,7 +384,6 @@ function codegenReactiveFunction(
|
||||
type: 'CodegenFunction',
|
||||
loc: fn.loc,
|
||||
id: fn.id !== null ? t.identifier(fn.id) : null,
|
||||
nameHint: fn.nameHint,
|
||||
params,
|
||||
body,
|
||||
generator: fn.generator,
|
||||
@@ -2329,6 +2328,10 @@ function codegenInstructionValue(
|
||||
reactiveFunction,
|
||||
).unwrap();
|
||||
|
||||
const validatedName =
|
||||
instrValue.name != null
|
||||
? validateIdentifierName(instrValue.name)
|
||||
: Err(null);
|
||||
if (instrValue.type === 'ArrowFunctionExpression') {
|
||||
let body: t.BlockStatement | t.Expression = fn.body;
|
||||
if (body.body.length === 1 && loweredFunc.directives.length == 0) {
|
||||
@@ -2340,7 +2343,9 @@ function codegenInstructionValue(
|
||||
value = t.arrowFunctionExpression(fn.params, body, fn.async);
|
||||
} else {
|
||||
value = t.functionExpression(
|
||||
instrValue.name != null ? t.identifier(instrValue.name) : null,
|
||||
validatedName
|
||||
.map<t.Identifier | null>(name => t.identifier(name))
|
||||
.unwrapOr(null),
|
||||
fn.params,
|
||||
fn.body,
|
||||
fn.generator,
|
||||
@@ -2349,10 +2354,10 @@ function codegenInstructionValue(
|
||||
}
|
||||
if (
|
||||
cx.env.config.enableNameAnonymousFunctions &&
|
||||
instrValue.name == null &&
|
||||
instrValue.nameHint != null
|
||||
validatedName.isErr() &&
|
||||
instrValue.name != null
|
||||
) {
|
||||
const name = instrValue.nameHint;
|
||||
const name = instrValue.name;
|
||||
value = t.memberExpression(
|
||||
t.objectExpression([t.objectProperty(t.stringLiteral(name), value)]),
|
||||
t.stringLiteral(name),
|
||||
|
||||
@@ -26,8 +26,7 @@ export function nameAnonymousFunctions(fn: HIRFunction): void {
|
||||
* nesting depth.
|
||||
*/
|
||||
const name = `${prefix}${node.generatedName}]`;
|
||||
node.fn.nameHint = name;
|
||||
node.fn.loweredFunc.func.nameHint = name;
|
||||
node.fn.name = name;
|
||||
}
|
||||
/**
|
||||
* Whether or not we generated a name for the function at this node,
|
||||
|
||||
@@ -5,48 +5,21 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {CompilerDiagnostic, CompilerError, Effect, SourceLocation} from '..';
|
||||
import {CompilerError, SourceLocation} from '..';
|
||||
import {ErrorCategory} from '../CompilerError';
|
||||
import {
|
||||
ArrayExpression,
|
||||
BasicBlock,
|
||||
BlockId,
|
||||
FunctionExpression,
|
||||
HIRFunction,
|
||||
IdentifierId,
|
||||
Instruction,
|
||||
Place,
|
||||
isSetStateType,
|
||||
isUseEffectHookType,
|
||||
isUseStateType,
|
||||
isUseRefType,
|
||||
GeneratedSource,
|
||||
} from '../HIR';
|
||||
import {eachInstructionOperand, eachInstructionLValue} from '../HIR/visitors';
|
||||
import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
|
||||
type SetStateCall = {
|
||||
loc: SourceLocation;
|
||||
derivedDep: DerivationMetadata;
|
||||
setStateId: IdentifierId;
|
||||
};
|
||||
|
||||
type TypeOfValue = 'ignored' | 'fromProps' | 'fromState' | 'fromPropsOrState';
|
||||
|
||||
type DerivationMetadata = {
|
||||
typeOfValue: TypeOfValue;
|
||||
place: Place;
|
||||
sources: Array<Place>;
|
||||
};
|
||||
|
||||
type ErrorMetadata = {
|
||||
type: TypeOfValue;
|
||||
description: string | undefined;
|
||||
loc: SourceLocation;
|
||||
setStateName: string | undefined | null;
|
||||
derivedDepsNames: Array<string>;
|
||||
};
|
||||
import {
|
||||
eachInstructionValueOperand,
|
||||
eachTerminalOperand,
|
||||
} from '../HIR/visitors';
|
||||
|
||||
/**
|
||||
* Validates that useEffect is not used for derived computations which could/should
|
||||
@@ -75,46 +48,12 @@ export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
|
||||
const candidateDependencies: Map<IdentifierId, ArrayExpression> = new Map();
|
||||
const functions: Map<IdentifierId, FunctionExpression> = new Map();
|
||||
const locals: Map<IdentifierId, IdentifierId> = new Map();
|
||||
const derivationCache: Map<IdentifierId, DerivationMetadata> = new Map();
|
||||
const shadowingUseState: Map<string, Array<SourceLocation>> = new Map();
|
||||
|
||||
const effectSetStates: Map<
|
||||
string | undefined | null,
|
||||
Array<Place>
|
||||
> = new Map();
|
||||
const setStateCalls: Map<string | undefined | null, Array<Place>> = new Map();
|
||||
|
||||
const errors: Array<ErrorMetadata> = [];
|
||||
|
||||
if (fn.fnType === 'Hook') {
|
||||
for (const param of fn.params) {
|
||||
if (param.kind === 'Identifier') {
|
||||
derivationCache.set(param.identifier.id, {
|
||||
place: param,
|
||||
sources: [param],
|
||||
typeOfValue: 'fromProps',
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (fn.fnType === 'Component') {
|
||||
const props = fn.params[0];
|
||||
if (props != null && props.kind === 'Identifier') {
|
||||
derivationCache.set(props.identifier.id, {
|
||||
place: props,
|
||||
sources: [props],
|
||||
typeOfValue: 'fromProps',
|
||||
});
|
||||
}
|
||||
}
|
||||
const errors = new CompilerError();
|
||||
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
parseBlockPhi(block, derivationCache);
|
||||
|
||||
for (const instr of block.instructions) {
|
||||
const {lvalue, value} = instr;
|
||||
|
||||
parseInstr(instr, derivationCache, setStateCalls, shadowingUseState);
|
||||
|
||||
if (value.kind === 'LoadLocal') {
|
||||
locals.set(lvalue.identifier.id, value.place.identifier.id);
|
||||
} else if (value.kind === 'ArrayExpression') {
|
||||
@@ -127,7 +66,6 @@ export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
|
||||
) {
|
||||
const callee =
|
||||
value.kind === 'CallExpression' ? value.callee : value.property;
|
||||
|
||||
if (
|
||||
isUseEffectHookType(callee.identifier) &&
|
||||
value.args.length === 2 &&
|
||||
@@ -159,8 +97,6 @@ export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
|
||||
validateEffect(
|
||||
effectFunction.loweredFunc.func,
|
||||
dependencies,
|
||||
derivationCache,
|
||||
effectSetStates,
|
||||
errors,
|
||||
);
|
||||
}
|
||||
@@ -168,333 +104,43 @@ export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const compilerError = generateCompilerError(
|
||||
setStateCalls,
|
||||
effectSetStates,
|
||||
shadowingUseState,
|
||||
errors,
|
||||
);
|
||||
|
||||
if (compilerError.hasErrors()) {
|
||||
throw compilerError;
|
||||
}
|
||||
}
|
||||
|
||||
function generateCompilerError(
|
||||
setStateCalls: Map<string | undefined | null, Array<Place>>,
|
||||
effectSetStates: Map<string | undefined | null, Array<Place>>,
|
||||
shadowingUseState: Map<string, Array<SourceLocation>>,
|
||||
errors: Array<ErrorMetadata>,
|
||||
): CompilerError {
|
||||
const throwableErrors = new CompilerError();
|
||||
for (const error of errors) {
|
||||
let compilerDiagnostic: CompilerDiagnostic | undefined = undefined;
|
||||
|
||||
/*
|
||||
* If we use a setState from an invalid useEffect elsewhere then we probably have to
|
||||
* hoist state up, else we should calculate in render
|
||||
*/
|
||||
if (
|
||||
setStateCalls.get(error.setStateName)?.length !=
|
||||
effectSetStates.get(error.setStateName)?.length &&
|
||||
error.type !== 'fromState'
|
||||
) {
|
||||
compilerDiagnostic = CompilerDiagnostic.create({
|
||||
description: `The setState within a useEffect is deriving from ${error.description}. Instead of shadowing the prop with local state, hoist the state to the parent component and update it there. If you are purposefully initializing state with a prop, and want to update it when a prop changes, do so conditionally in render`,
|
||||
category: ErrorCategory.EffectDerivationShadowingParentState,
|
||||
reason:
|
||||
'You might not need an effect. Local state shadows parent state.',
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: error.loc,
|
||||
message: `this derives values from props ${error.type === 'fromPropsOrState' ? 'and local state ' : ''}to synchronize state`,
|
||||
});
|
||||
|
||||
for (const derivedDep of error.derivedDepsNames) {
|
||||
if (shadowingUseState.has(derivedDep)) {
|
||||
for (const loc of shadowingUseState.get(derivedDep)!) {
|
||||
compilerDiagnostic.withDetails({
|
||||
kind: 'error',
|
||||
loc: loc,
|
||||
message: `this useState shadows ${derivedDep}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const [key, setStateCallArray] of effectSetStates) {
|
||||
if (setStateCallArray.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const nonUseEffectSetStateCalls = setStateCalls.get(key);
|
||||
if (nonUseEffectSetStateCalls) {
|
||||
for (const place of nonUseEffectSetStateCalls) {
|
||||
if (!setStateCallArray.includes(place)) {
|
||||
compilerDiagnostic.withDetails({
|
||||
kind: 'error',
|
||||
loc: place.loc,
|
||||
message:
|
||||
'this setState updates the shadowed state, but should call an onChange event from the parent',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
compilerDiagnostic = CompilerDiagnostic.create({
|
||||
description: `${error.description ? error.description.charAt(0).toUpperCase() + error.description.slice(1) : ''}. Derived values should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user`,
|
||||
category: ErrorCategory.EffectDerivationDeriveInRender,
|
||||
reason:
|
||||
'You might not need an effect. Derive values in render, not effects.',
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: error.loc,
|
||||
message: 'This should be computed during render, not in an effect',
|
||||
});
|
||||
}
|
||||
|
||||
if (compilerDiagnostic) {
|
||||
throwableErrors.pushDiagnostic(compilerDiagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
return throwableErrors;
|
||||
}
|
||||
|
||||
function joinValue(
|
||||
lvalueType: TypeOfValue,
|
||||
valueType: TypeOfValue,
|
||||
): TypeOfValue {
|
||||
if (lvalueType === 'ignored') return valueType;
|
||||
if (valueType === 'ignored') return lvalueType;
|
||||
if (lvalueType === valueType) return lvalueType;
|
||||
return 'fromPropsOrState';
|
||||
}
|
||||
|
||||
function updateDerivationMetadata(
|
||||
target: Place,
|
||||
sources: Array<DerivationMetadata> | undefined,
|
||||
typeOfValue: TypeOfValue | undefined,
|
||||
derivationCache: Map<IdentifierId, DerivationMetadata>,
|
||||
): void {
|
||||
let newValue: DerivationMetadata = {
|
||||
place: target,
|
||||
sources: [],
|
||||
typeOfValue: typeOfValue ?? 'ignored',
|
||||
};
|
||||
|
||||
if (sources !== undefined) {
|
||||
for (const source of sources) {
|
||||
/*
|
||||
* If the identifier of the source is a promoted identifier, then
|
||||
* we should set the target as the source.
|
||||
*/
|
||||
for (const place of source.sources) {
|
||||
if (
|
||||
place.identifier.name === null ||
|
||||
place.identifier.name?.kind === 'promoted'
|
||||
) {
|
||||
newValue.sources.push(target);
|
||||
} else {
|
||||
newValue.sources.push(place);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
derivationCache.set(target.identifier.id, newValue);
|
||||
}
|
||||
|
||||
function parseInstr(
|
||||
instr: Instruction,
|
||||
derivationCache: Map<IdentifierId, DerivationMetadata>,
|
||||
setStateCalls: Map<string | undefined | null, Array<Place>>,
|
||||
shadowingUseState: Map<string, Array<SourceLocation>>,
|
||||
): void {
|
||||
// Recursively parse function expressions
|
||||
let typeOfValue: TypeOfValue = 'ignored';
|
||||
|
||||
let sources: Array<DerivationMetadata> = [];
|
||||
if (instr.value.kind === 'FunctionExpression') {
|
||||
for (const [, block] of instr.value.loweredFunc.func.body.blocks) {
|
||||
for (const instr of block.instructions) {
|
||||
parseInstr(instr, derivationCache, setStateCalls, shadowingUseState);
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
instr.value.kind === 'CallExpression' &&
|
||||
isSetStateType(instr.value.callee.identifier) &&
|
||||
instr.value.args.length === 1 &&
|
||||
instr.value.args[0].kind === 'Identifier' &&
|
||||
instr.value.callee.loc !== GeneratedSource
|
||||
) {
|
||||
if (setStateCalls.has(instr.value.callee.loc.identifierName)) {
|
||||
setStateCalls
|
||||
.get(instr.value.callee.loc.identifierName)!
|
||||
.push(instr.value.callee);
|
||||
} else {
|
||||
setStateCalls.set(instr.value.callee.loc.identifierName, [
|
||||
instr.value.callee,
|
||||
]);
|
||||
}
|
||||
} else if (
|
||||
(instr.value.kind === 'CallExpression' ||
|
||||
instr.value.kind === 'MethodCall') &&
|
||||
isUseStateType(instr.lvalue.identifier) &&
|
||||
instr.value.args.length > 0
|
||||
) {
|
||||
const stateValueSource = instr.value.args[0];
|
||||
if (stateValueSource.kind === 'Identifier') {
|
||||
sources.push({
|
||||
place: stateValueSource,
|
||||
typeOfValue: typeOfValue,
|
||||
sources: [stateValueSource],
|
||||
});
|
||||
}
|
||||
|
||||
typeOfValue = joinValue(typeOfValue, 'fromState');
|
||||
}
|
||||
|
||||
for (const operand of eachInstructionOperand(instr)) {
|
||||
const opSource = derivationCache.get(operand.identifier.id);
|
||||
if (opSource === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
typeOfValue = joinValue(typeOfValue, opSource.typeOfValue);
|
||||
sources.push(opSource);
|
||||
|
||||
if (
|
||||
(instr.value.kind === 'CallExpression' ||
|
||||
instr.value.kind === 'MethodCall') &&
|
||||
opSource.typeOfValue === 'fromProps' &&
|
||||
isUseStateType(instr.lvalue.identifier)
|
||||
) {
|
||||
opSource.sources.forEach(source => {
|
||||
if (source.identifier.name !== null) {
|
||||
if (shadowingUseState.has(source.identifier.name.value)) {
|
||||
shadowingUseState
|
||||
.get(source.identifier.name.value)
|
||||
?.push(instr.lvalue.loc);
|
||||
} else {
|
||||
shadowingUseState.set(source.identifier.name.value, [
|
||||
instr.lvalue.loc,
|
||||
]);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (typeOfValue !== 'ignored') {
|
||||
for (const lvalue of eachInstructionLValue(instr)) {
|
||||
updateDerivationMetadata(lvalue, sources, typeOfValue, derivationCache);
|
||||
}
|
||||
|
||||
for (const operand of eachInstructionOperand(instr)) {
|
||||
switch (operand.effect) {
|
||||
case Effect.Capture:
|
||||
case Effect.Store:
|
||||
case Effect.ConditionallyMutate:
|
||||
case Effect.ConditionallyMutateIterator:
|
||||
case Effect.Mutate: {
|
||||
if (isMutable(instr, operand)) {
|
||||
updateDerivationMetadata(
|
||||
operand,
|
||||
sources,
|
||||
typeOfValue,
|
||||
derivationCache,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Effect.Freeze:
|
||||
case Effect.Read: {
|
||||
// no-op
|
||||
break;
|
||||
}
|
||||
case Effect.Unknown: {
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Unexpected unknown effect',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: operand.loc,
|
||||
message: 'Unexpected unknown effect',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(
|
||||
operand.effect,
|
||||
`Unexpected effect kind \`${operand.effect}\``,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function parseBlockPhi(
|
||||
block: BasicBlock,
|
||||
derivationCache: Map<IdentifierId, DerivationMetadata>,
|
||||
): void {
|
||||
for (const phi of block.phis) {
|
||||
let typeOfValue: TypeOfValue = 'ignored';
|
||||
let sources: Array<DerivationMetadata> = [];
|
||||
for (const operand of phi.operands.values()) {
|
||||
const opSource = derivationCache.get(operand.identifier.id);
|
||||
|
||||
if (opSource === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
typeOfValue = joinValue(typeOfValue, opSource?.typeOfValue ?? 'ignored');
|
||||
sources.push(opSource);
|
||||
}
|
||||
|
||||
if (typeOfValue !== 'ignored') {
|
||||
updateDerivationMetadata(
|
||||
phi.place,
|
||||
sources,
|
||||
typeOfValue,
|
||||
derivationCache,
|
||||
);
|
||||
}
|
||||
if (errors.hasAnyErrors()) {
|
||||
throw errors;
|
||||
}
|
||||
}
|
||||
|
||||
function validateEffect(
|
||||
effectFunction: HIRFunction,
|
||||
effectDeps: Array<IdentifierId>,
|
||||
derivationCache: Map<IdentifierId, DerivationMetadata>,
|
||||
effectSetStates: Map<string | undefined | null, Array<Place>>,
|
||||
errors: Array<ErrorMetadata>,
|
||||
errors: CompilerError,
|
||||
): void {
|
||||
let isUsingDerivedDeps = false;
|
||||
for (const operand of effectFunction.context) {
|
||||
if (isSetStateType(operand.identifier)) {
|
||||
continue;
|
||||
} else if (effectDeps.find(dep => dep === operand.identifier.id) != null) {
|
||||
continue;
|
||||
} else {
|
||||
// Captured something other than the effect dep or setState
|
||||
return;
|
||||
}
|
||||
}
|
||||
for (const dep of effectDeps) {
|
||||
const depMetadata = derivationCache.get(dep);
|
||||
if (
|
||||
effectFunction.context.find(operand => operand.identifier.id === dep) !=
|
||||
null ||
|
||||
(depMetadata !== undefined && depMetadata.typeOfValue !== 'ignored')
|
||||
effectFunction.context.find(operand => operand.identifier.id === dep) ==
|
||||
null
|
||||
) {
|
||||
isUsingDerivedDeps = true;
|
||||
// effect dep wasn't actually used in the function
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isUsingDerivedDeps) {
|
||||
// no prop/state derived deps were used in the body of the effect
|
||||
return;
|
||||
const seenBlocks: Set<BlockId> = new Set();
|
||||
const values: Map<IdentifierId, Array<IdentifierId>> = new Map();
|
||||
for (const dep of effectDeps) {
|
||||
values.set(dep, [dep]);
|
||||
}
|
||||
|
||||
const seenBlocks: Set<BlockId> = new Set();
|
||||
|
||||
const derivedSetStateCall: Array<SetStateCall> = [];
|
||||
const setStateLocations: Array<SourceLocation> = [];
|
||||
for (const block of effectFunction.body.blocks.values()) {
|
||||
for (const pred of block.preds) {
|
||||
if (!seenBlocks.has(pred)) {
|
||||
@@ -502,34 +148,21 @@ function validateEffect(
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
parseBlockPhi(block, derivationCache);
|
||||
|
||||
for (const instr of block.instructions) {
|
||||
// Early return if any instruction is deriving a value from a ref
|
||||
if (isUseRefType(instr.lvalue.identifier)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
instr.value.kind === 'CallExpression' &&
|
||||
isSetStateType(instr.value.callee.identifier) &&
|
||||
instr.value.args.length === 1 &&
|
||||
instr.value.args[0].kind === 'Identifier' &&
|
||||
instr.value.callee.loc !== GeneratedSource &&
|
||||
instr.value.callee.loc.identifierName !== undefined &&
|
||||
instr.value.callee.loc.identifierName !== null
|
||||
) {
|
||||
if (effectSetStates.has(instr.value.callee.loc.identifierName)) {
|
||||
effectSetStates
|
||||
.get(instr.value.callee.loc.identifierName)!
|
||||
.push(instr.value.callee);
|
||||
} else {
|
||||
effectSetStates.set(instr.value.callee.loc.identifierName, [
|
||||
instr.value.callee,
|
||||
]);
|
||||
for (const phi of block.phis) {
|
||||
const aggregateDeps: Set<IdentifierId> = new Set();
|
||||
for (const operand of phi.operands.values()) {
|
||||
const deps = values.get(operand.identifier.id);
|
||||
if (deps != null) {
|
||||
for (const dep of deps) {
|
||||
aggregateDeps.add(dep);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (aggregateDeps.size !== 0) {
|
||||
values.set(phi.place.identifier.id, Array.from(aggregateDeps));
|
||||
}
|
||||
}
|
||||
for (const instr of block.instructions) {
|
||||
switch (instr.value.kind) {
|
||||
case 'Primitive':
|
||||
case 'JSXText':
|
||||
@@ -537,6 +170,10 @@ function validateEffect(
|
||||
break;
|
||||
}
|
||||
case 'LoadLocal': {
|
||||
const deps = values.get(instr.value.place.identifier.id);
|
||||
if (deps != null) {
|
||||
values.set(instr.lvalue.identifier.id, deps);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'ComputedLoad':
|
||||
@@ -545,61 +182,57 @@ function validateEffect(
|
||||
case 'TemplateLiteral':
|
||||
case 'CallExpression':
|
||||
case 'MethodCall': {
|
||||
const aggregateDeps: Set<IdentifierId> = new Set();
|
||||
for (const operand of eachInstructionValueOperand(instr.value)) {
|
||||
const deps = values.get(operand.identifier.id);
|
||||
if (deps != null) {
|
||||
for (const dep of deps) {
|
||||
aggregateDeps.add(dep);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (aggregateDeps.size !== 0) {
|
||||
values.set(instr.lvalue.identifier.id, Array.from(aggregateDeps));
|
||||
}
|
||||
|
||||
if (
|
||||
instr.value.kind === 'CallExpression' &&
|
||||
isSetStateType(instr.value.callee.identifier) &&
|
||||
instr.value.args.length === 1 &&
|
||||
instr.value.args[0].kind === 'Identifier'
|
||||
) {
|
||||
const derivedDep = derivationCache.get(
|
||||
instr.value.args[0].identifier.id,
|
||||
);
|
||||
|
||||
if (derivedDep !== undefined) {
|
||||
derivedSetStateCall.push({
|
||||
loc: instr.value.callee.loc,
|
||||
setStateId: instr.value.callee.identifier.id,
|
||||
derivedDep: derivedDep,
|
||||
});
|
||||
const deps = values.get(instr.value.args[0].identifier.id);
|
||||
if (deps != null && new Set(deps).size === effectDeps.length) {
|
||||
setStateLocations.push(instr.value.callee.loc);
|
||||
} else {
|
||||
// doesn't depend on any deps
|
||||
return;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const operand of eachTerminalOperand(block.terminal)) {
|
||||
if (values.has(operand.identifier.id)) {
|
||||
//
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
seenBlocks.add(block.id);
|
||||
}
|
||||
|
||||
for (const call of derivedSetStateCall) {
|
||||
const derivedDepsStr = Array.from(call.derivedDep.sources)
|
||||
.map(place => {
|
||||
return place.identifier.name?.value;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join(', ');
|
||||
|
||||
let errorDescription = '';
|
||||
|
||||
if (call.derivedDep.typeOfValue === 'fromProps') {
|
||||
errorDescription = `props [${derivedDepsStr}]`;
|
||||
} else if (call.derivedDep.typeOfValue === 'fromState') {
|
||||
errorDescription = `local state [${derivedDepsStr}]`;
|
||||
} else {
|
||||
errorDescription = `both props and local state [${derivedDepsStr}]`;
|
||||
}
|
||||
|
||||
for (const loc of setStateLocations) {
|
||||
errors.push({
|
||||
type: call.derivedDep.typeOfValue,
|
||||
description: `${errorDescription}`,
|
||||
loc: call.loc,
|
||||
setStateName:
|
||||
call.loc !== GeneratedSource ? call.loc.identifierName : undefined,
|
||||
derivedDepsNames: Array.from(call.derivedDep.sources)
|
||||
.map(place => {
|
||||
return place.identifier.name?.value ?? '';
|
||||
})
|
||||
.filter(Boolean),
|
||||
category: ErrorCategory.EffectDerivationsOfState,
|
||||
reason:
|
||||
'Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)',
|
||||
description: null,
|
||||
loc,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
IdentifierId,
|
||||
InstructionValue,
|
||||
ManualMemoDependency,
|
||||
Place,
|
||||
PrunedReactiveScopeBlock,
|
||||
ReactiveFunction,
|
||||
ReactiveInstruction,
|
||||
@@ -28,10 +29,7 @@ import {
|
||||
SourceLocation,
|
||||
} from '../HIR';
|
||||
import {printIdentifier, printManualMemoDependency} from '../HIR/PrintHIR';
|
||||
import {
|
||||
eachInstructionValueLValue,
|
||||
eachInstructionValueOperand,
|
||||
} from '../HIR/visitors';
|
||||
import {eachInstructionValueOperand} from '../HIR/visitors';
|
||||
import {collectMaybeMemoDependencies} from '../Inference/DropManualMemoization';
|
||||
import {
|
||||
ReactiveFunctionVisitor,
|
||||
@@ -339,53 +337,56 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
|
||||
* @returns a @{ManualMemoDependency} representing the variable +
|
||||
* property reads represented by @value
|
||||
*/
|
||||
recordDepsInValue(value: ReactiveValue, state: VisitorState): void {
|
||||
recordDepsInValue(
|
||||
value: ReactiveValue,
|
||||
state: VisitorState,
|
||||
): ManualMemoDependency | null {
|
||||
switch (value.kind) {
|
||||
case 'SequenceExpression': {
|
||||
for (const instr of value.instructions) {
|
||||
this.visitInstruction(instr, state);
|
||||
}
|
||||
this.recordDepsInValue(value.value, state);
|
||||
break;
|
||||
const result = this.recordDepsInValue(value.value, state);
|
||||
return result;
|
||||
}
|
||||
case 'OptionalExpression': {
|
||||
this.recordDepsInValue(value.value, state);
|
||||
break;
|
||||
return this.recordDepsInValue(value.value, state);
|
||||
}
|
||||
case 'ConditionalExpression': {
|
||||
this.recordDepsInValue(value.test, state);
|
||||
this.recordDepsInValue(value.consequent, state);
|
||||
this.recordDepsInValue(value.alternate, state);
|
||||
break;
|
||||
return null;
|
||||
}
|
||||
case 'LogicalExpression': {
|
||||
this.recordDepsInValue(value.left, state);
|
||||
this.recordDepsInValue(value.right, state);
|
||||
break;
|
||||
return null;
|
||||
}
|
||||
default: {
|
||||
collectMaybeMemoDependencies(value, this.temporaries, false);
|
||||
if (
|
||||
value.kind === 'StoreLocal' ||
|
||||
value.kind === 'StoreContext' ||
|
||||
value.kind === 'Destructure'
|
||||
) {
|
||||
for (const storeTarget of eachInstructionValueLValue(value)) {
|
||||
state.manualMemoState?.decls.add(
|
||||
storeTarget.identifier.declarationId,
|
||||
);
|
||||
if (storeTarget.identifier.name?.kind === 'named') {
|
||||
this.temporaries.set(storeTarget.identifier.id, {
|
||||
root: {
|
||||
kind: 'NamedLocal',
|
||||
value: storeTarget,
|
||||
},
|
||||
path: [],
|
||||
});
|
||||
}
|
||||
const dep = collectMaybeMemoDependencies(
|
||||
value,
|
||||
this.temporaries,
|
||||
false,
|
||||
);
|
||||
if (value.kind === 'StoreLocal' || value.kind === 'StoreContext') {
|
||||
const storeTarget = value.lvalue.place;
|
||||
state.manualMemoState?.decls.add(
|
||||
storeTarget.identifier.declarationId,
|
||||
);
|
||||
if (storeTarget.identifier.name?.kind === 'named' && dep == null) {
|
||||
const dep: ManualMemoDependency = {
|
||||
root: {
|
||||
kind: 'NamedLocal',
|
||||
value: storeTarget,
|
||||
},
|
||||
path: [],
|
||||
};
|
||||
this.temporaries.set(storeTarget.identifier.id, dep);
|
||||
return dep;
|
||||
}
|
||||
}
|
||||
break;
|
||||
return dep;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -402,15 +403,19 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
|
||||
state.manualMemoState.decls.add(lvalue.identifier.declarationId);
|
||||
}
|
||||
|
||||
this.recordDepsInValue(value, state);
|
||||
if (lvalue != null) {
|
||||
temporaries.set(lvalue.identifier.id, {
|
||||
root: {
|
||||
kind: 'NamedLocal',
|
||||
value: {...lvalue},
|
||||
},
|
||||
path: [],
|
||||
});
|
||||
const maybeDep = this.recordDepsInValue(value, state);
|
||||
if (lvalId != null) {
|
||||
if (maybeDep != null) {
|
||||
temporaries.set(lvalId, maybeDep);
|
||||
} else if (isNamedLocal) {
|
||||
temporaries.set(lvalId, {
|
||||
root: {
|
||||
kind: 'NamedLocal',
|
||||
value: {...(instr.lvalue as Place)},
|
||||
},
|
||||
path: [],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,15 +24,13 @@ function BadExample() {
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: You might not need an effect. Derive values in render, not effects.
|
||||
|
||||
Local state [firstName, lastName]. Derived values should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
|
||||
Error: Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
|
||||
|
||||
error.invalid-derived-computation-in-effect.ts:9:4
|
||||
7 | const [fullName, setFullName] = useState('');
|
||||
8 | useEffect(() => {
|
||||
> 9 | setFullName(capitalize(firstName + ' ' + lastName));
|
||||
| ^^^^^^^^^^^ This should be computed during render, not in an effect
|
||||
| ^^^^^^^^^^^ Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
|
||||
10 | }, [firstName, lastName]);
|
||||
11 |
|
||||
12 | return <div>{fullName}</div>;
|
||||
@@ -1,59 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validatePreserveExistingMemoizationGuarantees
|
||||
|
||||
/**
|
||||
* Repro from https://github.com/facebook/react/issues/34262
|
||||
*
|
||||
* The compiler memoizes more precisely than the original code, with two reactive scopes:
|
||||
* - One for `transform(input)` with `input` as dep
|
||||
* - One for `{value}` with `value` as dep
|
||||
*
|
||||
* When we validate preserving manual memoization we incorrectly reject this, because
|
||||
* the original memoization had `object` depending on `input` but our scope depends on
|
||||
* `value`.
|
||||
*
|
||||
* This fixture adds a later potential mutation, which extends the scope and should
|
||||
* fail validation. This confirms that even though we allow the dependency to diverge,
|
||||
* we still check that the output value is memoized.
|
||||
*/
|
||||
function useInputValue(input) {
|
||||
const object = React.useMemo(() => {
|
||||
const {value} = transform(input);
|
||||
return {value};
|
||||
}, [input]);
|
||||
mutate(object);
|
||||
return object;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Compilation Skipped: Existing memoization could not be preserved
|
||||
|
||||
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This value was memoized in source but not in compilation output.
|
||||
|
||||
error.repro-preserve-memoization-inner-destructured-value-mistaken-as-dependency-later-mutation.ts:19:17
|
||||
17 | */
|
||||
18 | function useInputValue(input) {
|
||||
> 19 | const object = React.useMemo(() => {
|
||||
| ^^^^^^^^^^^^^^^^^^^^^
|
||||
> 20 | const {value} = transform(input);
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 21 | return {value};
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 22 | }, [input]);
|
||||
| ^^^^^^^^^^^^^^ Could not preserve existing memoization
|
||||
23 | mutate(object);
|
||||
24 | return object;
|
||||
25 | }
|
||||
```
|
||||
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
// @validatePreserveExistingMemoizationGuarantees
|
||||
|
||||
/**
|
||||
* Repro from https://github.com/facebook/react/issues/34262
|
||||
*
|
||||
* The compiler memoizes more precisely than the original code, with two reactive scopes:
|
||||
* - One for `transform(input)` with `input` as dep
|
||||
* - One for `{value}` with `value` as dep
|
||||
*
|
||||
* When we validate preserving manual memoization we incorrectly reject this, because
|
||||
* the original memoization had `object` depending on `input` but our scope depends on
|
||||
* `value`.
|
||||
*
|
||||
* This fixture adds a later potential mutation, which extends the scope and should
|
||||
* fail validation. This confirms that even though we allow the dependency to diverge,
|
||||
* we still check that the output value is memoized.
|
||||
*/
|
||||
function useInputValue(input) {
|
||||
const object = React.useMemo(() => {
|
||||
const {value} = transform(input);
|
||||
return {value};
|
||||
}, [input]);
|
||||
mutate(object);
|
||||
return object;
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validatePreserveExistingMemoizationGuarantees
|
||||
|
||||
import {identity, Stringify, useHook} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* Repro from https://github.com/facebook/react/issues/34262
|
||||
*
|
||||
* The compiler memoizes more precisely than the original code, with two reactive scopes:
|
||||
* - One for `transform(input)` with `input` as dep
|
||||
* - One for `{value}` with `value` as dep
|
||||
*
|
||||
* When we validate preserving manual memoization we incorrectly reject this, because
|
||||
* the original memoization had `object` depending on `input` but our scope depends on
|
||||
* `value`.
|
||||
*/
|
||||
function useInputValue(input) {
|
||||
// Conflate the `identity(input, x)` call with something outside the useMemo,
|
||||
// to try and break memoization of `value`. This gets correctly flagged since
|
||||
// the dependency is being mutated
|
||||
let x = {};
|
||||
useHook();
|
||||
const object = React.useMemo(() => {
|
||||
const {value} = identity(input, x);
|
||||
return {value};
|
||||
}, [input, x]);
|
||||
return object;
|
||||
}
|
||||
|
||||
function Component() {
|
||||
return <Stringify value={useInputValue({value: 42}).value} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Compilation Skipped: Existing memoization could not be preserved
|
||||
|
||||
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This dependency may be mutated later, which could cause the value to change unexpectedly.
|
||||
|
||||
error.repro-preserve-memoization-inner-destructured-value-mistaken-as-dependency-mutated-dep.ts:25:13
|
||||
23 | const {value} = identity(input, x);
|
||||
24 | return {value};
|
||||
> 25 | }, [input, x]);
|
||||
| ^ This dependency may be modified later
|
||||
26 | return object;
|
||||
27 | }
|
||||
28 |
|
||||
```
|
||||
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
// @validatePreserveExistingMemoizationGuarantees
|
||||
|
||||
import {identity, Stringify, useHook} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* Repro from https://github.com/facebook/react/issues/34262
|
||||
*
|
||||
* The compiler memoizes more precisely than the original code, with two reactive scopes:
|
||||
* - One for `transform(input)` with `input` as dep
|
||||
* - One for `{value}` with `value` as dep
|
||||
*
|
||||
* When we validate preserving manual memoization we incorrectly reject this, because
|
||||
* the original memoization had `object` depending on `input` but our scope depends on
|
||||
* `value`.
|
||||
*/
|
||||
function useInputValue(input) {
|
||||
// Conflate the `identity(input, x)` call with something outside the useMemo,
|
||||
// to try and break memoization of `value`. This gets correctly flagged since
|
||||
// the dependency is being mutated
|
||||
let x = {};
|
||||
useHook();
|
||||
const object = React.useMemo(() => {
|
||||
const {value} = identity(input, x);
|
||||
return {value};
|
||||
}, [input, x]);
|
||||
return object;
|
||||
}
|
||||
|
||||
function Component() {
|
||||
return <Stringify value={useInputValue({value: 42}).value} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{}],
|
||||
};
|
||||
@@ -1,52 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableNameAnonymousFunctions
|
||||
import {Stringify} from 'shared-runtime';
|
||||
|
||||
function Component(props) {
|
||||
const onClick = () => {
|
||||
console.log('hello!');
|
||||
};
|
||||
return <div onClick={onClick} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 42}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableNameAnonymousFunctions
|
||||
import { Stringify } from "shared-runtime";
|
||||
|
||||
function Component(props) {
|
||||
const $ = _c(1);
|
||||
const onClick = _ComponentOnClick;
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = <div onClick={onClick} />;
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
function _ComponentOnClick() {
|
||||
console.log("hello!");
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ value: 42 }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div></div>
|
||||
@@ -1,14 +0,0 @@
|
||||
// @enableNameAnonymousFunctions
|
||||
import {Stringify} from 'shared-runtime';
|
||||
|
||||
function Component(props) {
|
||||
const onClick = () => {
|
||||
console.log('hello!');
|
||||
};
|
||||
return <div onClick={onClick} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 42}],
|
||||
};
|
||||
@@ -0,0 +1,82 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @compilationMode:"infer"
|
||||
function Component() {
|
||||
const dispatch = useDispatch();
|
||||
// const [state, setState] = useState(0);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input
|
||||
type="file"
|
||||
onChange={event => {
|
||||
dispatch(...event.target);
|
||||
event.target.value = '';
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function useDispatch() {
|
||||
'use no memo';
|
||||
// skip compilation to make it easier to debug the above function
|
||||
return (...values) => {
|
||||
console.log(...values);
|
||||
};
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @compilationMode:"infer"
|
||||
function Component() {
|
||||
const $ = _c(2);
|
||||
const dispatch = useDispatch();
|
||||
let t0;
|
||||
if ($[0] !== dispatch) {
|
||||
t0 = (
|
||||
<div>
|
||||
<input
|
||||
type="file"
|
||||
onChange={(event) => {
|
||||
dispatch(...event.target);
|
||||
event.target.value = "";
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
$[0] = dispatch;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
|
||||
function useDispatch() {
|
||||
"use no memo";
|
||||
// skip compilation to make it easier to debug the above function
|
||||
return (...values) => {
|
||||
console.log(...values);
|
||||
};
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div><input type="file"></div>
|
||||
@@ -0,0 +1,30 @@
|
||||
// @compilationMode:"infer"
|
||||
function Component() {
|
||||
const dispatch = useDispatch();
|
||||
// const [state, setState] = useState(0);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input
|
||||
type="file"
|
||||
onChange={event => {
|
||||
dispatch(...event.target);
|
||||
event.target.value = '';
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function useDispatch() {
|
||||
'use no memo';
|
||||
// skip compilation to make it easier to debug the above function
|
||||
return (...values) => {
|
||||
console.log(...values);
|
||||
};
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{}],
|
||||
};
|
||||
@@ -1,109 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validatePreserveExistingMemoizationGuarantees
|
||||
|
||||
import {identity, Stringify} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* Repro from https://github.com/facebook/react/issues/34262
|
||||
*
|
||||
* The compiler memoizes more precisely than the original code, with two reactive scopes:
|
||||
* - One for `transform(input)` with `input` as dep
|
||||
* - One for `{value}` with `value` as dep
|
||||
*
|
||||
* Previously ValidatePreservedManualMemoization rejected this input, because
|
||||
* the original memoization had `object` depending on `input` but we split the scope per above,
|
||||
* and the scope for the FinishMemoize instruction is the second scope which depends on `value`
|
||||
*/
|
||||
function useInputValue(input) {
|
||||
const object = React.useMemo(() => {
|
||||
const {value} = identity(input);
|
||||
return {value};
|
||||
}, [input]);
|
||||
return object;
|
||||
}
|
||||
|
||||
function Component() {
|
||||
return <Stringify value={useInputValue({value: 42}).value} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees
|
||||
|
||||
import { identity, Stringify } from "shared-runtime";
|
||||
|
||||
/**
|
||||
* Repro from https://github.com/facebook/react/issues/34262
|
||||
*
|
||||
* The compiler memoizes more precisely than the original code, with two reactive scopes:
|
||||
* - One for `transform(input)` with `input` as dep
|
||||
* - One for `{value}` with `value` as dep
|
||||
*
|
||||
* Previously ValidatePreservedManualMemoization rejected this input, because
|
||||
* the original memoization had `object` depending on `input` but we split the scope per above,
|
||||
* and the scope for the FinishMemoize instruction is the second scope which depends on `value`
|
||||
*/
|
||||
function useInputValue(input) {
|
||||
const $ = _c(4);
|
||||
let t0;
|
||||
if ($[0] !== input) {
|
||||
t0 = identity(input);
|
||||
$[0] = input;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
const { value } = t0;
|
||||
let t1;
|
||||
if ($[2] !== value) {
|
||||
t1 = { value };
|
||||
$[2] = value;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
}
|
||||
const object = t1;
|
||||
return object;
|
||||
}
|
||||
|
||||
function Component() {
|
||||
const $ = _c(3);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = { value: 42 };
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
const t1 = useInputValue(t0);
|
||||
let t2;
|
||||
if ($[1] !== t1.value) {
|
||||
t2 = <Stringify value={t1.value} />;
|
||||
$[1] = t1.value;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>{"value":42}</div>
|
||||
@@ -1,31 +0,0 @@
|
||||
// @validatePreserveExistingMemoizationGuarantees
|
||||
|
||||
import {identity, Stringify} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* Repro from https://github.com/facebook/react/issues/34262
|
||||
*
|
||||
* The compiler memoizes more precisely than the original code, with two reactive scopes:
|
||||
* - One for `transform(input)` with `input` as dep
|
||||
* - One for `{value}` with `value` as dep
|
||||
*
|
||||
* Previously ValidatePreservedManualMemoization rejected this input, because
|
||||
* the original memoization had `object` depending on `input` but we split the scope per above,
|
||||
* and the scope for the FinishMemoize instruction is the second scope which depends on `value`
|
||||
*/
|
||||
function useInputValue(input) {
|
||||
const object = React.useMemo(() => {
|
||||
const {value} = identity(input);
|
||||
return {value};
|
||||
}, [input]);
|
||||
return object;
|
||||
}
|
||||
|
||||
function Component() {
|
||||
return <Stringify value={useInputValue({value: 42}).value} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{}],
|
||||
};
|
||||
@@ -1,73 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState, useRef} from 'react';
|
||||
|
||||
export default function Component({test}) {
|
||||
const [local, setLocal] = useState('');
|
||||
|
||||
const myRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
setLocal(myRef.current + test);
|
||||
}, [test]);
|
||||
|
||||
return <>{local}</>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{test: 'testString'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
|
||||
export default function Component(t0) {
|
||||
const $ = _c(5);
|
||||
const { test } = t0;
|
||||
const [local, setLocal] = useState("");
|
||||
|
||||
const myRef = useRef(null);
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== test) {
|
||||
t1 = () => {
|
||||
setLocal(myRef.current + test);
|
||||
};
|
||||
t2 = [test];
|
||||
$[0] = test;
|
||||
$[1] = t1;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
t2 = $[2];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[3] !== local) {
|
||||
t3 = <>{local}</>;
|
||||
$[3] = local;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ test: "testString" }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) nulltestString
|
||||
@@ -1,19 +0,0 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState, useRef} from 'react';
|
||||
|
||||
export default function Component({test}) {
|
||||
const [local, setLocal] = useState('');
|
||||
|
||||
const myRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
setLocal(myRef.current + test);
|
||||
}, [test]);
|
||||
|
||||
return <>{local}</>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{test: 'testString'}],
|
||||
};
|
||||
@@ -1,87 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({initialName}) {
|
||||
const [name, setName] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setName(initialName);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input value={name} onChange={e => setName(e.target.value)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{initialName: 'John'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(6);
|
||||
const { initialName } = t0;
|
||||
const [name, setName] = useState("");
|
||||
let t1;
|
||||
if ($[0] !== initialName) {
|
||||
t1 = () => {
|
||||
setName(initialName);
|
||||
};
|
||||
$[0] = initialName;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
let t2;
|
||||
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = [];
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t3 = (e) => setName(e.target.value);
|
||||
$[3] = t3;
|
||||
} else {
|
||||
t3 = $[3];
|
||||
}
|
||||
let t4;
|
||||
if ($[4] !== name) {
|
||||
t4 = (
|
||||
<div>
|
||||
<input value={name} onChange={t3} />
|
||||
</div>
|
||||
);
|
||||
$[4] = name;
|
||||
$[5] = t4;
|
||||
} else {
|
||||
t4 = $[5];
|
||||
}
|
||||
return t4;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ initialName: "John" }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div><input value="John"></div>
|
||||
@@ -1,21 +0,0 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({initialName}) {
|
||||
const [name, setName] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setName(initialName);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input value={name} onChange={e => setName(e.target.value)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{initialName: 'John'}],
|
||||
};
|
||||
@@ -1,51 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({prefix}) {
|
||||
const [name, setName] = useState('');
|
||||
const [displayName, setDisplayName] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setDisplayName(prefix + name);
|
||||
}, [prefix, name]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input value={name} onChange={e => setName(e.target.value)} />
|
||||
<div>{displayName}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{prefix: 'Hello, '}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: You might not need an effect. Derive values in render, not effects.
|
||||
|
||||
Both props and local state [prefix, name]. Derived values should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
|
||||
|
||||
error.bug-derived-state-from-mixed-deps.ts:9:4
|
||||
7 |
|
||||
8 | useEffect(() => {
|
||||
> 9 | setDisplayName(prefix + name);
|
||||
| ^^^^^^^^^^^^^^ This should be computed during render, not in an effect
|
||||
10 | }, [prefix, name]);
|
||||
11 |
|
||||
12 | return (
|
||||
```
|
||||
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({prefix}) {
|
||||
const [name, setName] = useState('');
|
||||
const [displayName, setDisplayName] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setDisplayName(prefix + name);
|
||||
}, [prefix, name]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input value={name} onChange={e => setName(e.target.value)} />
|
||||
<div>{displayName}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{prefix: 'Hello, '}],
|
||||
};
|
||||
@@ -1,86 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useState, useEffect} from 'react';
|
||||
|
||||
function Component({props, number}) {
|
||||
const nothing = 0;
|
||||
const missDirection = number;
|
||||
const [displayValue, setDisplayValue] = useState(
|
||||
props.prefix + missDirection + nothing
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setDisplayValue(props.prefix + missDirection + nothing);
|
||||
}, [props.prefix, missDirection, nothing]);
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={() => {
|
||||
setDisplayValue('clicked');
|
||||
}}>
|
||||
{displayValue}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: You might not need an effect. Local state shadows parent state.
|
||||
|
||||
The setState within a useEffect is deriving from props [props, number]. Instead of shadowing the prop with local state, hoist the state to the parent component and update it there. If you are purposefully initializing state with a prop, and want to update it when a prop changes, do so conditionally in render.
|
||||
|
||||
error.derived-state-from-shadowed-props.ts:12:4
|
||||
10 |
|
||||
11 | useEffect(() => {
|
||||
> 12 | setDisplayValue(props.prefix + missDirection + nothing);
|
||||
| ^^^^^^^^^^^^^^^ this derives values from props to synchronize state
|
||||
13 | }, [props.prefix, missDirection, nothing]);
|
||||
14 |
|
||||
15 | return (
|
||||
|
||||
error.derived-state-from-shadowed-props.ts:7:42
|
||||
5 | const nothing = 0;
|
||||
6 | const missDirection = number;
|
||||
> 7 | const [displayValue, setDisplayValue] = useState(
|
||||
| ^^^^^^^^^
|
||||
> 8 | props.prefix + missDirection + nothing
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 9 | );
|
||||
| ^^^^ this useState shadows props
|
||||
10 |
|
||||
11 | useEffect(() => {
|
||||
12 | setDisplayValue(props.prefix + missDirection + nothing);
|
||||
|
||||
error.derived-state-from-shadowed-props.ts:7:42
|
||||
5 | const nothing = 0;
|
||||
6 | const missDirection = number;
|
||||
> 7 | const [displayValue, setDisplayValue] = useState(
|
||||
| ^^^^^^^^^
|
||||
> 8 | props.prefix + missDirection + nothing
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 9 | );
|
||||
| ^^^^ this useState shadows number
|
||||
10 |
|
||||
11 | useEffect(() => {
|
||||
12 | setDisplayValue(props.prefix + missDirection + nothing);
|
||||
|
||||
error.derived-state-from-shadowed-props.ts:18:8
|
||||
16 | <div
|
||||
17 | onClick={() => {
|
||||
> 18 | setDisplayValue('clicked');
|
||||
| ^^^^^^^^^^^^^^^ this setState updates the shadowed state, but should call an onChange event from the parent
|
||||
19 | }}>
|
||||
20 | {displayValue}
|
||||
21 | </div>
|
||||
```
|
||||
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useState, useEffect} from 'react';
|
||||
|
||||
function Component({props, number}) {
|
||||
const nothing = 0;
|
||||
const missDirection = number;
|
||||
const [displayValue, setDisplayValue] = useState(
|
||||
props.prefix + missDirection + nothing
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setDisplayValue(props.prefix + missDirection + nothing);
|
||||
}, [props.prefix, missDirection, nothing]);
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={() => {
|
||||
setDisplayValue('clicked');
|
||||
}}>
|
||||
{displayValue}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({value, enabled}) {
|
||||
const [localValue, setLocalValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (enabled) {
|
||||
setLocalValue(value);
|
||||
} else {
|
||||
setLocalValue('disabled');
|
||||
}
|
||||
}, [value, enabled]);
|
||||
|
||||
return <div>{localValue}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 'test', enabled: true}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: You might not need an effect. Derive values in render, not effects.
|
||||
|
||||
Props [value]. Derived values should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
|
||||
|
||||
error.derived-state-with-conditional.ts:9:6
|
||||
7 | useEffect(() => {
|
||||
8 | if (enabled) {
|
||||
> 9 | setLocalValue(value);
|
||||
| ^^^^^^^^^^^^^ This should be computed during render, not in an effect
|
||||
10 | } else {
|
||||
11 | setLocalValue('disabled');
|
||||
12 | }
|
||||
```
|
||||
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({value, enabled}) {
|
||||
const [localValue, setLocalValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (enabled) {
|
||||
setLocalValue(value);
|
||||
} else {
|
||||
setLocalValue('disabled');
|
||||
}
|
||||
}, [value, enabled]);
|
||||
|
||||
return <div>{localValue}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 'test', enabled: true}],
|
||||
};
|
||||
@@ -1,47 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({value}) {
|
||||
const [localValue, setLocalValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
console.log('Value changed:', value);
|
||||
setLocalValue(value);
|
||||
document.title = `Value: ${value}`;
|
||||
}, [value]);
|
||||
|
||||
return <div>{localValue}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 'test'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: You might not need an effect. Derive values in render, not effects.
|
||||
|
||||
Props [value]. Derived values should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
|
||||
|
||||
error.derived-state-with-side-effects.ts:9:4
|
||||
7 | useEffect(() => {
|
||||
8 | console.log('Value changed:', value);
|
||||
> 9 | setLocalValue(value);
|
||||
| ^^^^^^^^^^^^^ This should be computed during render, not in an effect
|
||||
10 | document.title = `Value: ${value}`;
|
||||
11 | }, [value]);
|
||||
12 |
|
||||
```
|
||||
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({value}) {
|
||||
const [localValue, setLocalValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
console.log('Value changed:', value);
|
||||
setLocalValue(value);
|
||||
document.title = `Value: ${value}`;
|
||||
}, [value]);
|
||||
|
||||
return <div>{localValue}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 'test'}],
|
||||
};
|
||||
@@ -1,46 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component(props) {
|
||||
const [displayValue, setDisplayValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const computed = props.prefix + props.value + props.suffix;
|
||||
setDisplayValue(computed);
|
||||
}, [props.prefix, props.value, props.suffix]);
|
||||
|
||||
return <div>{displayValue}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{prefix: '[', value: 'test', suffix: ']'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: You might not need an effect. Derive values in render, not effects.
|
||||
|
||||
Props [props, props, props]. Derived values should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
|
||||
|
||||
error.invalid-derived-state-from-props-computed.ts:9:4
|
||||
7 | useEffect(() => {
|
||||
8 | const computed = props.prefix + props.value + props.suffix;
|
||||
> 9 | setDisplayValue(computed);
|
||||
| ^^^^^^^^^^^^^^^ This should be computed during render, not in an effect
|
||||
10 | }, [props.prefix, props.value, props.suffix]);
|
||||
11 |
|
||||
12 | return <div>{displayValue}</div>;
|
||||
```
|
||||
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component(props) {
|
||||
const [displayValue, setDisplayValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const computed = props.prefix + props.value + props.suffix;
|
||||
setDisplayValue(computed);
|
||||
}, [props.prefix, props.value, props.suffix]);
|
||||
|
||||
return <div>{displayValue}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{prefix: '[', value: 'test', suffix: ']'}],
|
||||
};
|
||||
@@ -1,47 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({props}) {
|
||||
const [fullName, setFullName] = useState(
|
||||
props.firstName + ' ' + props.lastName
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setFullName(props.firstName + ' ' + props.lastName);
|
||||
}, [props.firstName, props.lastName]);
|
||||
|
||||
return <div>{fullName}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{firstName: 'John', lastName: 'Doe'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: You might not need an effect. Derive values in render, not effects.
|
||||
|
||||
Props [props, props]. Derived values should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
|
||||
|
||||
error.invalid-derived-state-from-props-destructured.ts:10:4
|
||||
8 |
|
||||
9 | useEffect(() => {
|
||||
> 10 | setFullName(props.firstName + ' ' + props.lastName);
|
||||
| ^^^^^^^^^^^ This should be computed during render, not in an effect
|
||||
11 | }, [props.firstName, props.lastName]);
|
||||
12 |
|
||||
13 | return <div>{fullName}</div>;
|
||||
```
|
||||
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({props}) {
|
||||
const [fullName, setFullName] = useState(
|
||||
props.firstName + ' ' + props.lastName
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setFullName(props.firstName + ' ' + props.lastName);
|
||||
}, [props.firstName, props.lastName]);
|
||||
|
||||
return <div>{fullName}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{firstName: 'John', lastName: 'Doe'}],
|
||||
};
|
||||
@@ -1,45 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({firstName, lastName}) {
|
||||
const [fullName, setFullName] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setFullName(firstName + ' ' + lastName);
|
||||
}, [firstName, lastName]);
|
||||
|
||||
return <div>{fullName}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{firstName: 'John', lastName: 'Doe'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: You might not need an effect. Derive values in render, not effects.
|
||||
|
||||
Props [firstName, lastName]. Derived values should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
|
||||
|
||||
error.invalid-derived-state-from-props-in-effect.ts:8:4
|
||||
6 |
|
||||
7 | useEffect(() => {
|
||||
> 8 | setFullName(firstName + ' ' + lastName);
|
||||
| ^^^^^^^^^^^ This should be computed during render, not in an effect
|
||||
9 | }, [firstName, lastName]);
|
||||
10 |
|
||||
11 | return <div>{fullName}</div>;
|
||||
```
|
||||
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({firstName, lastName}) {
|
||||
const [fullName, setFullName] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setFullName(firstName + ' ' + lastName);
|
||||
}, [firstName, lastName]);
|
||||
|
||||
return <div>{fullName}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{firstName: 'John', lastName: 'Doe'}],
|
||||
};
|
||||
@@ -1,39 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
|
||||
export default function InProductLobbyGeminiCard(input = 'empty') {
|
||||
const [currInput, setCurrInput] = useState(input);
|
||||
|
||||
useEffect(() => {
|
||||
setCurrInput(input);
|
||||
}, [input]);
|
||||
|
||||
return <div>{currInput}</div>;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: You might not need an effect. Derive values in render, not effects.
|
||||
|
||||
Props [input]. Derived values should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
|
||||
|
||||
error.invalid-derived-state-from-props-with-default-value.ts:7:4
|
||||
5 |
|
||||
6 | useEffect(() => {
|
||||
> 7 | setCurrInput(input);
|
||||
| ^^^^^^^^^^^^ This should be computed during render, not in an effect
|
||||
8 | }, [input]);
|
||||
9 |
|
||||
10 | return <div>{currInput}</div>;
|
||||
```
|
||||
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
|
||||
export default function InProductLobbyGeminiCard(input = 'empty') {
|
||||
const [currInput, setCurrInput] = useState(input);
|
||||
|
||||
useEffect(() => {
|
||||
setCurrInput(input);
|
||||
}, [input]);
|
||||
|
||||
return <div>{currInput}</div>;
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component() {
|
||||
const [firstName, setFirstName] = useState('John');
|
||||
const [lastName, setLastName] = useState('Doe');
|
||||
const [fullName, setFullName] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setFullName(firstName + ' ' + lastName);
|
||||
}, [firstName, lastName]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input value={firstName} onChange={e => setFirstName(e.target.value)} />
|
||||
<input value={lastName} onChange={e => setLastName(e.target.value)} />
|
||||
<div>{fullName}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: You might not need an effect. Derive values in render, not effects.
|
||||
|
||||
Local state [firstName, lastName]. Derived values should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
|
||||
|
||||
error.invalid-derived-state-from-state-in-effect.ts:10:4
|
||||
8 |
|
||||
9 | useEffect(() => {
|
||||
> 10 | setFullName(firstName + ' ' + lastName);
|
||||
| ^^^^^^^^^^^ This should be computed during render, not in an effect
|
||||
11 | }, [firstName, lastName]);
|
||||
12 |
|
||||
13 | return (
|
||||
```
|
||||
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component() {
|
||||
const [firstName, setFirstName] = useState('John');
|
||||
const [lastName, setLastName] = useState('Doe');
|
||||
const [fullName, setFullName] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setFullName(firstName + ' ' + lastName);
|
||||
}, [firstName, lastName]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input value={firstName} onChange={e => setFirstName(e.target.value)} />
|
||||
<input value={lastName} onChange={e => setLastName(e.target.value)} />
|
||||
<div>{fullName}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [],
|
||||
};
|
||||
@@ -1,63 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
|
||||
function EndDate({startDate, endDate, onStartDateChange}) {
|
||||
const [localStartDate, setLocalStartDate] = useState(startDate);
|
||||
|
||||
useEffect(() => {
|
||||
setLocalStartDate(startDate);
|
||||
}, [startDate]);
|
||||
|
||||
const onChange = date => {
|
||||
setLocalStartDate(date);
|
||||
onStartDateChange(date);
|
||||
};
|
||||
return (
|
||||
<DateInput value={localStartDate} second={endDate} onChange={onChange} />
|
||||
);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: You might not need an effect. Local state shadows parent state.
|
||||
|
||||
The setState within a useEffect is deriving from props [startDate]. Instead of shadowing the prop with local state, hoist the state to the parent component and update it there. If you are purposefully initializing state with a prop, and want to update it when a prop changes, do so conditionally in render.
|
||||
|
||||
error.shadowed-props-with-onchange.ts:7:4
|
||||
5 |
|
||||
6 | useEffect(() => {
|
||||
> 7 | setLocalStartDate(startDate);
|
||||
| ^^^^^^^^^^^^^^^^^ this derives values from props to synchronize state
|
||||
8 | }, [startDate]);
|
||||
9 |
|
||||
10 | const onChange = date => {
|
||||
|
||||
error.shadowed-props-with-onchange.ts:4:46
|
||||
2 |
|
||||
3 | function EndDate({startDate, endDate, onStartDateChange}) {
|
||||
> 4 | const [localStartDate, setLocalStartDate] = useState(startDate);
|
||||
| ^^^^^^^^^^^^^^^^^^^ this useState shadows startDate
|
||||
5 |
|
||||
6 | useEffect(() => {
|
||||
7 | setLocalStartDate(startDate);
|
||||
|
||||
error.shadowed-props-with-onchange.ts:11:4
|
||||
9 |
|
||||
10 | const onChange = date => {
|
||||
> 11 | setLocalStartDate(date);
|
||||
| ^^^^^^^^^^^^^^^^^ this setState updates the shadowed state, but should call an onChange event from the parent
|
||||
12 | onStartDateChange(date);
|
||||
13 | };
|
||||
14 | return (
|
||||
```
|
||||
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
|
||||
function EndDate({startDate, endDate, onStartDateChange}) {
|
||||
const [localStartDate, setLocalStartDate] = useState(startDate);
|
||||
|
||||
useEffect(() => {
|
||||
setLocalStartDate(startDate);
|
||||
}, [startDate]);
|
||||
|
||||
const onChange = date => {
|
||||
setLocalStartDate(date);
|
||||
onStartDateChange(date);
|
||||
};
|
||||
return (
|
||||
<DateInput value={localStartDate} second={endDate} onChange={onChange} />
|
||||
);
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState, useRef} from 'react';
|
||||
|
||||
export default function Component({test}) {
|
||||
const [local, setLocal] = useState('');
|
||||
|
||||
const myRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (myRef.current) {
|
||||
setLocal(test + 'Available');
|
||||
} else {
|
||||
setLocal(test + 'NotAvailable');
|
||||
}
|
||||
}, [test]);
|
||||
|
||||
return <>{local}</>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{test: 'testString'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
|
||||
export default function Component(t0) {
|
||||
const $ = _c(5);
|
||||
const { test } = t0;
|
||||
const [local, setLocal] = useState("");
|
||||
|
||||
const myRef = useRef(null);
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== test) {
|
||||
t1 = () => {
|
||||
if (myRef.current) {
|
||||
setLocal(test + "Available");
|
||||
} else {
|
||||
setLocal(test + "NotAvailable");
|
||||
}
|
||||
};
|
||||
|
||||
t2 = [test];
|
||||
$[0] = test;
|
||||
$[1] = t1;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
t2 = $[2];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[3] !== local) {
|
||||
t3 = <>{local}</>;
|
||||
$[3] = local;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ test: "testString" }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) testStringNotAvailable
|
||||
@@ -1,23 +0,0 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState, useRef} from 'react';
|
||||
|
||||
export default function Component({test}) {
|
||||
const [local, setLocal] = useState('');
|
||||
|
||||
const myRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (myRef.current) {
|
||||
setLocal(test + 'Available');
|
||||
} else {
|
||||
setLocal(test + 'NotAvailable');
|
||||
}
|
||||
}, [test]);
|
||||
|
||||
return <>{local}</>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{test: 'testString'}],
|
||||
};
|
||||
@@ -34,8 +34,4 @@ const configs = {
|
||||
},
|
||||
};
|
||||
|
||||
const rules = Object.fromEntries(
|
||||
Object.entries(allRules).map(([name, {rule}]) => [name, rule]),
|
||||
);
|
||||
|
||||
export {configs, rules, meta};
|
||||
export {configs, allRules as rules, meta};
|
||||
|
||||
@@ -10494,7 +10494,16 @@ string-length@^4.0.1:
|
||||
char-regex "^1.0.2"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
|
||||
"string-width-cjs@npm:string-width@^4.2.0":
|
||||
version "4.2.3"
|
||||
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
dependencies:
|
||||
emoji-regex "^8.0.0"
|
||||
is-fullwidth-code-point "^3.0.0"
|
||||
strip-ansi "^6.0.1"
|
||||
|
||||
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
@@ -10567,7 +10576,14 @@ string_decoder@~1.1.1:
|
||||
dependencies:
|
||||
safe-buffer "~5.1.0"
|
||||
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
|
||||
version "6.0.1"
|
||||
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
dependencies:
|
||||
ansi-regex "^5.0.1"
|
||||
|
||||
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
@@ -11344,7 +11360,7 @@ workerpool@^6.5.1:
|
||||
resolved "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz"
|
||||
integrity sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==
|
||||
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
|
||||
version "7.0.0"
|
||||
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
@@ -11362,6 +11378,15 @@ wrap-ansi@^6.2.0:
|
||||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
wrap-ansi@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
dependencies:
|
||||
ansi-styles "^4.0.0"
|
||||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
wrap-ansi@^8.1.0:
|
||||
version "8.1.0"
|
||||
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz"
|
||||
|
||||
@@ -21,8 +21,6 @@ import {Note} from './cjs/Note.js';
|
||||
|
||||
import {GenerateImage} from './GenerateImage.js';
|
||||
|
||||
import LargeContent from './LargeContent.js';
|
||||
|
||||
import {like, greet, increment} from './actions.js';
|
||||
|
||||
import {getServerState} from './ServerState.js';
|
||||
@@ -235,11 +233,6 @@ export default async function App({prerender, noCache}) {
|
||||
<Foo>{dedupedChild}</Foo>
|
||||
<Bar>{Promise.resolve([dedupedChild])}</Bar>
|
||||
<Navigate />
|
||||
{prerender ? null : ( // TODO: prerender is broken for large content for some reason.
|
||||
<React.Suspense fallback={null}>
|
||||
<LargeContent />
|
||||
</React.Suspense>
|
||||
)}
|
||||
</Container>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
391
packages/react-client/src/ReactFlightClient.js
vendored
391
packages/react-client/src/ReactFlightClient.js
vendored
@@ -366,7 +366,6 @@ type Response = {
|
||||
_debugRootOwner?: null | ReactComponentInfo, // DEV-only
|
||||
_debugRootStack?: null | Error, // DEV-only
|
||||
_debugRootTask?: null | ConsoleTask, // DEV-only
|
||||
_debugStartTime: number, // DEV-only
|
||||
_debugFindSourceMapURL?: void | FindSourceMapURLCallback, // DEV-only
|
||||
_debugChannel?: void | DebugChannel, // DEV-only
|
||||
_blockedConsole?: null | SomeChunk<ConsoleEntry>, // DEV-only
|
||||
@@ -823,7 +822,6 @@ type InitializationReference = {
|
||||
key: string,
|
||||
) => any,
|
||||
path: Array<string>,
|
||||
isDebug?: boolean, // DEV-only
|
||||
};
|
||||
type InitializationHandler = {
|
||||
parent: null | InitializationHandler,
|
||||
@@ -874,7 +872,6 @@ function initializeDebugChunk(
|
||||
response,
|
||||
initializeDebugInfo,
|
||||
[''], // path
|
||||
true,
|
||||
);
|
||||
break;
|
||||
}
|
||||
@@ -897,7 +894,6 @@ function initializeDebugChunk(
|
||||
response,
|
||||
initializeDebugInfo,
|
||||
[''], // path
|
||||
true,
|
||||
);
|
||||
break;
|
||||
}
|
||||
@@ -1411,6 +1407,8 @@ function fulfillReference(
|
||||
const mappedValue = map(response, value, parentObject, key);
|
||||
parentObject[key] = mappedValue;
|
||||
|
||||
transferReferencedDebugInfo(handler.chunk, fulfilledChunk, mappedValue);
|
||||
|
||||
// If this is the root object for a model reference, where `handler.value`
|
||||
// is a stale `null`, the resolved value can be used directly.
|
||||
if (key === '' && handler.value === null) {
|
||||
@@ -1429,27 +1427,19 @@ function fulfillReference(
|
||||
const element: any = handler.value;
|
||||
switch (key) {
|
||||
case '3':
|
||||
transferReferencedDebugInfo(handler.chunk, fulfilledChunk, mappedValue);
|
||||
element.props = mappedValue;
|
||||
break;
|
||||
case '4':
|
||||
// This path doesn't call transferReferencedDebugInfo because this reference is to a debug chunk.
|
||||
if (__DEV__) {
|
||||
element._owner = mappedValue;
|
||||
}
|
||||
break;
|
||||
case '5':
|
||||
// This path doesn't call transferReferencedDebugInfo because this reference is to a debug chunk.
|
||||
if (__DEV__) {
|
||||
element._debugStack = mappedValue;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
transferReferencedDebugInfo(handler.chunk, fulfilledChunk, mappedValue);
|
||||
break;
|
||||
}
|
||||
} else if (__DEV__ && !reference.isDebug) {
|
||||
transferReferencedDebugInfo(handler.chunk, fulfilledChunk, mappedValue);
|
||||
}
|
||||
|
||||
handler.deps--;
|
||||
@@ -1528,7 +1518,6 @@ function waitForReference<T>(
|
||||
response: Response,
|
||||
map: (response: Response, model: any, parentObject: Object, key: string) => T,
|
||||
path: Array<string>,
|
||||
isAwaitingDebugInfo: boolean, // DEV-only
|
||||
): T {
|
||||
if (
|
||||
__DEV__ &&
|
||||
@@ -1573,9 +1562,6 @@ function waitForReference<T>(
|
||||
map,
|
||||
path,
|
||||
};
|
||||
if (__DEV__) {
|
||||
reference.isDebug = isAwaitingDebugInfo;
|
||||
}
|
||||
|
||||
// Add "listener".
|
||||
if (referencedChunk.value === null) {
|
||||
@@ -1808,21 +1794,13 @@ function transferReferencedDebugInfo(
|
||||
existingDebugInfo.push.apply(existingDebugInfo, referencedDebugInfo);
|
||||
}
|
||||
}
|
||||
// We also add the debug info to the initializing chunk since the resolution of that promise is
|
||||
// also blocked by the referenced debug info. By adding it to both we can track it even if the array/element
|
||||
// We also add it to the initializing chunk since the resolution of that promise is
|
||||
// also blocked by these. By adding it to both we can track it even if the array/element
|
||||
// is extracted, or if the root is rendered as is.
|
||||
if (parentChunk !== null) {
|
||||
const parentDebugInfo = parentChunk._debugInfo;
|
||||
for (let i = 0; i < referencedDebugInfo.length; ++i) {
|
||||
const debugInfoEntry = referencedDebugInfo[i];
|
||||
if (debugInfoEntry.name != null) {
|
||||
(debugInfoEntry: ReactComponentInfo);
|
||||
// We're not transferring Component info since we use Component info
|
||||
// in Debug info to fill in gaps between Fibers for the parent stack.
|
||||
} else {
|
||||
parentDebugInfo.push(debugInfoEntry);
|
||||
}
|
||||
}
|
||||
// $FlowFixMe[method-unbinding]
|
||||
parentDebugInfo.push.apply(parentDebugInfo, referencedDebugInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1879,7 +1857,6 @@ function getOutlinedModel<T>(
|
||||
response,
|
||||
map,
|
||||
path.slice(i - 1),
|
||||
false,
|
||||
);
|
||||
}
|
||||
case HALTED: {
|
||||
@@ -1925,27 +1902,11 @@ function getOutlinedModel<T>(
|
||||
value = value[path[i]];
|
||||
}
|
||||
const chunkValue = map(response, value, parentObject, key);
|
||||
if (
|
||||
parentObject[0] === REACT_ELEMENT_TYPE &&
|
||||
(key === '4' || key === '5')
|
||||
) {
|
||||
// If we're resolving the "owner" or "stack" slot of an Element array, we don't call
|
||||
// transferReferencedDebugInfo because this reference is to a debug chunk.
|
||||
} else {
|
||||
transferReferencedDebugInfo(initializingChunk, chunk, chunkValue);
|
||||
}
|
||||
transferReferencedDebugInfo(initializingChunk, chunk, chunkValue);
|
||||
return chunkValue;
|
||||
case PENDING:
|
||||
case BLOCKED:
|
||||
return waitForReference(
|
||||
chunk,
|
||||
parentObject,
|
||||
key,
|
||||
response,
|
||||
map,
|
||||
path,
|
||||
false,
|
||||
);
|
||||
return waitForReference(chunk, parentObject, key, response, map, path);
|
||||
case HALTED: {
|
||||
// Add a dependency that will never resolve.
|
||||
// TODO: Mark downstreams as halted too.
|
||||
@@ -2483,13 +2444,6 @@ function ResponseInstance(
|
||||
'"use ' + rootEnv.toLowerCase() + '"',
|
||||
);
|
||||
}
|
||||
if (enableAsyncDebugInfo) {
|
||||
// Track the start of the fetch to the best of our knowledge.
|
||||
// Note: createFromFetch allows this to be marked at the start of the fetch
|
||||
// where as if you use createFromReadableStream from the body of the fetch
|
||||
// then the start time is when the headers resolved.
|
||||
this._debugStartTime = performance.now();
|
||||
}
|
||||
this._debugFindSourceMapURL = findSourceMapURL;
|
||||
this._debugChannel = debugChannel;
|
||||
this._blockedConsole = null;
|
||||
@@ -2558,99 +2512,16 @@ export type StreamState = {
|
||||
_rowTag: number, // 0 indicates that we're currently parsing the row ID
|
||||
_rowLength: number, // remaining bytes in the row. 0 indicates that we're looking for a newline.
|
||||
_buffer: Array<Uint8Array>, // chunks received so far as part of this row
|
||||
_debugInfo: ReactIOInfo, // DEV-only
|
||||
_debugTargetChunkSize: number, // DEV-only
|
||||
};
|
||||
|
||||
export function createStreamState(
|
||||
weakResponse: WeakResponse, // DEV-only
|
||||
streamDebugValue: mixed, // DEV-only
|
||||
): StreamState {
|
||||
const streamState: StreamState = (({
|
||||
export function createStreamState(): StreamState {
|
||||
return {
|
||||
_rowState: 0,
|
||||
_rowID: 0,
|
||||
_rowTag: 0,
|
||||
_rowLength: 0,
|
||||
_buffer: [],
|
||||
}: Omit<StreamState, '_debugInfo' | '_debugTargetChunkSize'>): any);
|
||||
if (__DEV__ && enableAsyncDebugInfo) {
|
||||
const response = unwrapWeakResponse(weakResponse);
|
||||
// Create an entry for the I/O to load the stream itself.
|
||||
const debugValuePromise = Promise.resolve(streamDebugValue);
|
||||
(debugValuePromise: any).status = 'fulfilled';
|
||||
(debugValuePromise: any).value = streamDebugValue;
|
||||
streamState._debugInfo = {
|
||||
name: 'RSC stream',
|
||||
start: response._debugStartTime,
|
||||
end: response._debugStartTime, // will be updated once we finish a chunk
|
||||
byteSize: 0, // will be updated as we resolve a data chunk
|
||||
value: debugValuePromise,
|
||||
owner: response._debugRootOwner,
|
||||
debugStack: response._debugRootStack,
|
||||
debugTask: response._debugRootTask,
|
||||
};
|
||||
streamState._debugTargetChunkSize = MIN_CHUNK_SIZE;
|
||||
}
|
||||
return streamState;
|
||||
}
|
||||
|
||||
// Depending on set up the chunks of a TLS connection can vary in size. However in practice it's often
|
||||
// at 64kb or even multiples of 64kb. It can also be smaller but in practice it also happens that 64kb
|
||||
// is around what you can download on fast 4G connection in 300ms which is what we throttle reveals at
|
||||
// anyway. The net effect is that in practice, you won't really reveal anything in smaller units than
|
||||
// 64kb if they're revealing at maximum speed in production. Therefore we group smaller chunks into
|
||||
// these larger chunks since in production that's more realistic.
|
||||
// TODO: If the stream is compressed, then you could fit much more in a single 300ms so maybe it should
|
||||
// actually be larger.
|
||||
const MIN_CHUNK_SIZE = 65536;
|
||||
|
||||
function incrementChunkDebugInfo(
|
||||
streamState: StreamState,
|
||||
chunkLength: number,
|
||||
): void {
|
||||
if (__DEV__ && enableAsyncDebugInfo) {
|
||||
const debugInfo: ReactIOInfo = streamState._debugInfo;
|
||||
const endTime = performance.now();
|
||||
const previousEndTime = debugInfo.end;
|
||||
const newByteLength = ((debugInfo.byteSize: any): number) + chunkLength;
|
||||
if (
|
||||
newByteLength > streamState._debugTargetChunkSize ||
|
||||
endTime > previousEndTime + 10
|
||||
) {
|
||||
// This new chunk would overshoot the chunk size so therefore we treat it as its own new chunk
|
||||
// by cloning the old one. Similarly, if some time has passed we assume that it was actually
|
||||
// due to the server being unable to flush chunks faster e.g. due to I/O so it would be a
|
||||
// new chunk in production even if the buffer hasn't been reached.
|
||||
streamState._debugInfo = {
|
||||
name: debugInfo.name,
|
||||
start: debugInfo.start,
|
||||
end: endTime,
|
||||
byteSize: newByteLength,
|
||||
value: debugInfo.value,
|
||||
owner: debugInfo.owner,
|
||||
debugStack: debugInfo.debugStack,
|
||||
debugTask: debugInfo.debugTask,
|
||||
};
|
||||
streamState._debugTargetChunkSize = newByteLength + MIN_CHUNK_SIZE;
|
||||
} else {
|
||||
// Otherwise we reuse the old chunk but update the end time and byteSize to the latest.
|
||||
// $FlowFixMe[cannot-write]
|
||||
debugInfo.end = endTime;
|
||||
// $FlowFixMe[cannot-write]
|
||||
debugInfo.byteSize = newByteLength;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function resolveChunkDebugInfo(
|
||||
streamState: StreamState,
|
||||
chunk: SomeChunk<any>,
|
||||
): void {
|
||||
if (__DEV__ && enableAsyncDebugInfo) {
|
||||
// Push the currently resolving chunk's debug info representing the stream on the Promise
|
||||
// that was waiting on the stream.
|
||||
chunk._debugInfo.push({awaited: streamState._debugInfo});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function resolveDebugHalt(response: Response, id: number): void {
|
||||
@@ -2674,33 +2545,17 @@ function resolveModel(
|
||||
response: Response,
|
||||
id: number,
|
||||
model: UninitializedModel,
|
||||
streamState: StreamState,
|
||||
): void {
|
||||
const chunks = response._chunks;
|
||||
const chunk = chunks.get(id);
|
||||
if (!chunk) {
|
||||
const newChunk: ResolvedModelChunk<any> = createResolvedModelChunk(
|
||||
response,
|
||||
model,
|
||||
);
|
||||
if (__DEV__) {
|
||||
resolveChunkDebugInfo(streamState, newChunk);
|
||||
}
|
||||
chunks.set(id, newChunk);
|
||||
chunks.set(id, createResolvedModelChunk(response, model));
|
||||
} else {
|
||||
if (__DEV__) {
|
||||
resolveChunkDebugInfo(streamState, chunk);
|
||||
}
|
||||
resolveModelChunk(response, chunk, model);
|
||||
}
|
||||
}
|
||||
|
||||
function resolveText(
|
||||
response: Response,
|
||||
id: number,
|
||||
text: string,
|
||||
streamState: StreamState,
|
||||
): void {
|
||||
function resolveText(response: Response, id: number, text: string): void {
|
||||
const chunks = response._chunks;
|
||||
const chunk = chunks.get(id);
|
||||
if (chunk && chunk.status !== PENDING) {
|
||||
@@ -2714,18 +2569,13 @@ function resolveText(
|
||||
if (chunk) {
|
||||
releasePendingChunk(response, chunk);
|
||||
}
|
||||
const newChunk = createInitializedTextChunk(response, text);
|
||||
if (__DEV__) {
|
||||
resolveChunkDebugInfo(streamState, newChunk);
|
||||
}
|
||||
chunks.set(id, newChunk);
|
||||
chunks.set(id, createInitializedTextChunk(response, text));
|
||||
}
|
||||
|
||||
function resolveBuffer(
|
||||
response: Response,
|
||||
id: number,
|
||||
buffer: $ArrayBufferView | ArrayBuffer,
|
||||
streamState: StreamState,
|
||||
): void {
|
||||
const chunks = response._chunks;
|
||||
const chunk = chunks.get(id);
|
||||
@@ -2740,18 +2590,13 @@ function resolveBuffer(
|
||||
if (chunk) {
|
||||
releasePendingChunk(response, chunk);
|
||||
}
|
||||
const newChunk = createInitializedBufferChunk(response, buffer);
|
||||
if (__DEV__) {
|
||||
resolveChunkDebugInfo(streamState, newChunk);
|
||||
}
|
||||
chunks.set(id, newChunk);
|
||||
chunks.set(id, createInitializedBufferChunk(response, buffer));
|
||||
}
|
||||
|
||||
function resolveModule(
|
||||
response: Response,
|
||||
id: number,
|
||||
model: UninitializedModel,
|
||||
streamState: StreamState,
|
||||
): void {
|
||||
const chunks = response._chunks;
|
||||
const chunk = chunks.get(id);
|
||||
@@ -2788,24 +2633,14 @@ function resolveModule(
|
||||
blockedChunk = (chunk: any);
|
||||
blockedChunk.status = BLOCKED;
|
||||
}
|
||||
if (__DEV__) {
|
||||
resolveChunkDebugInfo(streamState, blockedChunk);
|
||||
}
|
||||
promise.then(
|
||||
() => resolveModuleChunk(response, blockedChunk, clientReference),
|
||||
error => triggerErrorOnChunk(response, blockedChunk, error),
|
||||
);
|
||||
} else {
|
||||
if (!chunk) {
|
||||
const newChunk = createResolvedModuleChunk(response, clientReference);
|
||||
if (__DEV__) {
|
||||
resolveChunkDebugInfo(streamState, newChunk);
|
||||
}
|
||||
chunks.set(id, newChunk);
|
||||
chunks.set(id, createResolvedModuleChunk(response, clientReference));
|
||||
} else {
|
||||
if (__DEV__) {
|
||||
resolveChunkDebugInfo(streamState, chunk);
|
||||
}
|
||||
// This can't actually happen because we don't have any forward
|
||||
// references to modules.
|
||||
resolveModuleChunk(response, chunk, clientReference);
|
||||
@@ -2818,21 +2653,13 @@ function resolveStream<T: ReadableStream | $AsyncIterable<any, any, void>>(
|
||||
id: number,
|
||||
stream: T,
|
||||
controller: FlightStreamController,
|
||||
streamState: StreamState,
|
||||
): void {
|
||||
const chunks = response._chunks;
|
||||
const chunk = chunks.get(id);
|
||||
if (!chunk) {
|
||||
const newChunk = createInitializedStreamChunk(response, stream, controller);
|
||||
if (__DEV__) {
|
||||
resolveChunkDebugInfo(streamState, newChunk);
|
||||
}
|
||||
chunks.set(id, newChunk);
|
||||
chunks.set(id, createInitializedStreamChunk(response, stream, controller));
|
||||
return;
|
||||
}
|
||||
if (__DEV__) {
|
||||
resolveChunkDebugInfo(streamState, chunk);
|
||||
}
|
||||
if (chunk.status !== PENDING) {
|
||||
// We already resolved. We didn't expect to see this.
|
||||
return;
|
||||
@@ -2888,7 +2715,6 @@ function startReadableStream<T>(
|
||||
response: Response,
|
||||
id: number,
|
||||
type: void | 'bytes',
|
||||
streamState: StreamState,
|
||||
): void {
|
||||
let controller: ReadableStreamController = (null: any);
|
||||
const stream = new ReadableStream({
|
||||
@@ -2969,7 +2795,7 @@ function startReadableStream<T>(
|
||||
}
|
||||
},
|
||||
};
|
||||
resolveStream(response, id, stream, flightController, streamState);
|
||||
resolveStream(response, id, stream, flightController);
|
||||
}
|
||||
|
||||
function asyncIterator(this: $AsyncIterator<any, any, void>) {
|
||||
@@ -2995,7 +2821,6 @@ function startAsyncIterable<T>(
|
||||
response: Response,
|
||||
id: number,
|
||||
iterator: boolean,
|
||||
streamState: StreamState,
|
||||
): void {
|
||||
const buffer: Array<SomeChunk<IteratorResult<T, T>>> = [];
|
||||
let closed = false;
|
||||
@@ -3113,7 +2938,6 @@ function startAsyncIterable<T>(
|
||||
id,
|
||||
iterator ? iterable[ASYNC_ITERATOR]() : iterable,
|
||||
flightController,
|
||||
streamState,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3193,11 +3017,7 @@ function resolveErrorDev(
|
||||
return error;
|
||||
}
|
||||
|
||||
function resolvePostponeProd(
|
||||
response: Response,
|
||||
id: number,
|
||||
streamState: StreamState,
|
||||
): void {
|
||||
function resolvePostponeProd(response: Response, id: number): void {
|
||||
if (__DEV__) {
|
||||
// These errors should never make it into a build so we don't need to encode them in codes.json
|
||||
// eslint-disable-next-line react-internal/prod-error-codes
|
||||
@@ -3215,11 +3035,7 @@ function resolvePostponeProd(
|
||||
const chunks = response._chunks;
|
||||
const chunk = chunks.get(id);
|
||||
if (!chunk) {
|
||||
const newChunk: ErroredChunk<any> = createErrorChunk(
|
||||
response,
|
||||
postponeInstance,
|
||||
);
|
||||
chunks.set(id, newChunk);
|
||||
chunks.set(id, createErrorChunk(response, postponeInstance));
|
||||
} else {
|
||||
triggerErrorOnChunk(response, chunk, postponeInstance);
|
||||
}
|
||||
@@ -3231,7 +3047,6 @@ function resolvePostponeDev(
|
||||
reason: string,
|
||||
stack: ReactStackTrace,
|
||||
env: string,
|
||||
streamState: StreamState,
|
||||
): void {
|
||||
if (!__DEV__) {
|
||||
// These errors should never make it into a build so we don't need to encode them in codes.json
|
||||
@@ -3259,18 +3074,8 @@ function resolvePostponeDev(
|
||||
const chunks = response._chunks;
|
||||
const chunk = chunks.get(id);
|
||||
if (!chunk) {
|
||||
const newChunk: ErroredChunk<any> = createErrorChunk(
|
||||
response,
|
||||
postponeInstance,
|
||||
);
|
||||
if (__DEV__) {
|
||||
resolveChunkDebugInfo(streamState, newChunk);
|
||||
}
|
||||
chunks.set(id, newChunk);
|
||||
chunks.set(id, createErrorChunk(response, postponeInstance));
|
||||
} else {
|
||||
if (__DEV__) {
|
||||
resolveChunkDebugInfo(streamState, chunk);
|
||||
}
|
||||
triggerErrorOnChunk(response, chunk, postponeInstance);
|
||||
}
|
||||
}
|
||||
@@ -3279,7 +3084,6 @@ function resolveErrorModel(
|
||||
response: Response,
|
||||
id: number,
|
||||
row: UninitializedModel,
|
||||
streamState: StreamState,
|
||||
): void {
|
||||
const chunks = response._chunks;
|
||||
const chunk = chunks.get(id);
|
||||
@@ -3293,18 +3097,8 @@ function resolveErrorModel(
|
||||
(error: any).digest = errorInfo.digest;
|
||||
const errorWithDigest: ErrorWithDigest = (error: any);
|
||||
if (!chunk) {
|
||||
const newChunk: ErroredChunk<any> = createErrorChunk(
|
||||
response,
|
||||
errorWithDigest,
|
||||
);
|
||||
if (__DEV__) {
|
||||
resolveChunkDebugInfo(streamState, newChunk);
|
||||
}
|
||||
chunks.set(id, newChunk);
|
||||
chunks.set(id, createErrorChunk(response, errorWithDigest));
|
||||
} else {
|
||||
if (__DEV__) {
|
||||
resolveChunkDebugInfo(streamState, chunk);
|
||||
}
|
||||
triggerErrorOnChunk(response, chunk, errorWithDigest);
|
||||
}
|
||||
}
|
||||
@@ -4039,7 +3833,6 @@ function resolveTypedArray(
|
||||
lastChunk: Uint8Array,
|
||||
constructor: any,
|
||||
bytesPerElement: number,
|
||||
streamState: StreamState,
|
||||
): void {
|
||||
// If the view fits into one original buffer, we just reuse that buffer instead of
|
||||
// copying it out to a separate copy. This means that it's not always possible to
|
||||
@@ -4059,7 +3852,7 @@ function resolveTypedArray(
|
||||
chunk.byteOffset,
|
||||
chunk.byteLength / bytesPerElement,
|
||||
);
|
||||
resolveBuffer(response, id, view, streamState);
|
||||
resolveBuffer(response, id, view);
|
||||
}
|
||||
|
||||
function logComponentInfo(
|
||||
@@ -4376,7 +4169,6 @@ function flushInitialRenderPerformance(response: Response): void {
|
||||
|
||||
function processFullBinaryRow(
|
||||
response: Response,
|
||||
streamState: StreamState,
|
||||
id: number,
|
||||
tag: number,
|
||||
buffer: Array<Uint8Array>,
|
||||
@@ -4385,125 +4177,47 @@ function processFullBinaryRow(
|
||||
switch (tag) {
|
||||
case 65 /* "A" */:
|
||||
// We must always clone to extract it into a separate buffer instead of just a view.
|
||||
resolveBuffer(
|
||||
response,
|
||||
id,
|
||||
mergeBuffer(buffer, chunk).buffer,
|
||||
streamState,
|
||||
);
|
||||
resolveBuffer(response, id, mergeBuffer(buffer, chunk).buffer);
|
||||
return;
|
||||
case 79 /* "O" */:
|
||||
resolveTypedArray(response, id, buffer, chunk, Int8Array, 1, streamState);
|
||||
resolveTypedArray(response, id, buffer, chunk, Int8Array, 1);
|
||||
return;
|
||||
case 111 /* "o" */:
|
||||
resolveBuffer(
|
||||
response,
|
||||
id,
|
||||
buffer.length === 0 ? chunk : mergeBuffer(buffer, chunk),
|
||||
streamState,
|
||||
);
|
||||
return;
|
||||
case 85 /* "U" */:
|
||||
resolveTypedArray(
|
||||
response,
|
||||
id,
|
||||
buffer,
|
||||
chunk,
|
||||
Uint8ClampedArray,
|
||||
1,
|
||||
streamState,
|
||||
);
|
||||
resolveTypedArray(response, id, buffer, chunk, Uint8ClampedArray, 1);
|
||||
return;
|
||||
case 83 /* "S" */:
|
||||
resolveTypedArray(
|
||||
response,
|
||||
id,
|
||||
buffer,
|
||||
chunk,
|
||||
Int16Array,
|
||||
2,
|
||||
streamState,
|
||||
);
|
||||
resolveTypedArray(response, id, buffer, chunk, Int16Array, 2);
|
||||
return;
|
||||
case 115 /* "s" */:
|
||||
resolveTypedArray(
|
||||
response,
|
||||
id,
|
||||
buffer,
|
||||
chunk,
|
||||
Uint16Array,
|
||||
2,
|
||||
streamState,
|
||||
);
|
||||
resolveTypedArray(response, id, buffer, chunk, Uint16Array, 2);
|
||||
return;
|
||||
case 76 /* "L" */:
|
||||
resolveTypedArray(
|
||||
response,
|
||||
id,
|
||||
buffer,
|
||||
chunk,
|
||||
Int32Array,
|
||||
4,
|
||||
streamState,
|
||||
);
|
||||
resolveTypedArray(response, id, buffer, chunk, Int32Array, 4);
|
||||
return;
|
||||
case 108 /* "l" */:
|
||||
resolveTypedArray(
|
||||
response,
|
||||
id,
|
||||
buffer,
|
||||
chunk,
|
||||
Uint32Array,
|
||||
4,
|
||||
streamState,
|
||||
);
|
||||
resolveTypedArray(response, id, buffer, chunk, Uint32Array, 4);
|
||||
return;
|
||||
case 71 /* "G" */:
|
||||
resolveTypedArray(
|
||||
response,
|
||||
id,
|
||||
buffer,
|
||||
chunk,
|
||||
Float32Array,
|
||||
4,
|
||||
streamState,
|
||||
);
|
||||
resolveTypedArray(response, id, buffer, chunk, Float32Array, 4);
|
||||
return;
|
||||
case 103 /* "g" */:
|
||||
resolveTypedArray(
|
||||
response,
|
||||
id,
|
||||
buffer,
|
||||
chunk,
|
||||
Float64Array,
|
||||
8,
|
||||
streamState,
|
||||
);
|
||||
resolveTypedArray(response, id, buffer, chunk, Float64Array, 8);
|
||||
return;
|
||||
case 77 /* "M" */:
|
||||
resolveTypedArray(
|
||||
response,
|
||||
id,
|
||||
buffer,
|
||||
chunk,
|
||||
BigInt64Array,
|
||||
8,
|
||||
streamState,
|
||||
);
|
||||
resolveTypedArray(response, id, buffer, chunk, BigInt64Array, 8);
|
||||
return;
|
||||
case 109 /* "m" */:
|
||||
resolveTypedArray(
|
||||
response,
|
||||
id,
|
||||
buffer,
|
||||
chunk,
|
||||
BigUint64Array,
|
||||
8,
|
||||
streamState,
|
||||
);
|
||||
resolveTypedArray(response, id, buffer, chunk, BigUint64Array, 8);
|
||||
return;
|
||||
case 86 /* "V" */:
|
||||
resolveTypedArray(response, id, buffer, chunk, DataView, 1, streamState);
|
||||
resolveTypedArray(response, id, buffer, chunk, DataView, 1);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -4513,19 +4227,18 @@ function processFullBinaryRow(
|
||||
row += readPartialStringChunk(stringDecoder, buffer[i]);
|
||||
}
|
||||
row += readFinalStringChunk(stringDecoder, chunk);
|
||||
processFullStringRow(response, streamState, id, tag, row);
|
||||
processFullStringRow(response, id, tag, row);
|
||||
}
|
||||
|
||||
function processFullStringRow(
|
||||
response: Response,
|
||||
streamState: StreamState,
|
||||
id: number,
|
||||
tag: number,
|
||||
row: string,
|
||||
): void {
|
||||
switch (tag) {
|
||||
case 73 /* "I" */: {
|
||||
resolveModule(response, id, row, streamState);
|
||||
resolveModule(response, id, row);
|
||||
return;
|
||||
}
|
||||
case 72 /* "H" */: {
|
||||
@@ -4534,11 +4247,11 @@ function processFullStringRow(
|
||||
return;
|
||||
}
|
||||
case 69 /* "E" */: {
|
||||
resolveErrorModel(response, id, row, streamState);
|
||||
resolveErrorModel(response, id, row);
|
||||
return;
|
||||
}
|
||||
case 84 /* "T" */: {
|
||||
resolveText(response, id, row, streamState);
|
||||
resolveText(response, id, row);
|
||||
return;
|
||||
}
|
||||
case 78 /* "N" */: {
|
||||
@@ -4583,22 +4296,22 @@ function processFullStringRow(
|
||||
);
|
||||
}
|
||||
case 82 /* "R" */: {
|
||||
startReadableStream(response, id, undefined, streamState);
|
||||
startReadableStream(response, id, undefined);
|
||||
return;
|
||||
}
|
||||
// Fallthrough
|
||||
case 114 /* "r" */: {
|
||||
startReadableStream(response, id, 'bytes', streamState);
|
||||
startReadableStream(response, id, 'bytes');
|
||||
return;
|
||||
}
|
||||
// Fallthrough
|
||||
case 88 /* "X" */: {
|
||||
startAsyncIterable(response, id, false, streamState);
|
||||
startAsyncIterable(response, id, false);
|
||||
return;
|
||||
}
|
||||
// Fallthrough
|
||||
case 120 /* "x" */: {
|
||||
startAsyncIterable(response, id, true, streamState);
|
||||
startAsyncIterable(response, id, true);
|
||||
return;
|
||||
}
|
||||
// Fallthrough
|
||||
@@ -4617,10 +4330,9 @@ function processFullStringRow(
|
||||
postponeInfo.reason,
|
||||
postponeInfo.stack,
|
||||
postponeInfo.env,
|
||||
streamState,
|
||||
);
|
||||
} else {
|
||||
resolvePostponeProd(response, id, streamState);
|
||||
resolvePostponeProd(response, id);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -4632,7 +4344,7 @@ function processFullStringRow(
|
||||
return;
|
||||
}
|
||||
// We assume anything else is JSON.
|
||||
resolveModel(response, id, row, streamState);
|
||||
resolveModel(response, id, row);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -4655,7 +4367,6 @@ export function processBinaryChunk(
|
||||
let rowLength = streamState._rowLength;
|
||||
const buffer = streamState._buffer;
|
||||
const chunkLength = chunk.length;
|
||||
incrementChunkDebugInfo(streamState, chunkLength);
|
||||
while (i < chunkLength) {
|
||||
let lastIdx = -1;
|
||||
switch (rowState) {
|
||||
@@ -4735,14 +4446,7 @@ 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,
|
||||
);
|
||||
processFullBinaryRow(response, rowID, rowTag, buffer, lastChunk);
|
||||
// Reset state machine for a new row
|
||||
i = lastIdx;
|
||||
if (rowState === ROW_CHUNK_BY_NEWLINE) {
|
||||
@@ -4797,7 +4501,6 @@ export function processStringChunk(
|
||||
let rowLength = streamState._rowLength;
|
||||
const buffer = streamState._buffer;
|
||||
const chunkLength = chunk.length;
|
||||
incrementChunkDebugInfo(streamState, chunkLength);
|
||||
while (i < chunkLength) {
|
||||
let lastIdx = -1;
|
||||
switch (rowState) {
|
||||
@@ -4896,7 +4599,7 @@ export function processStringChunk(
|
||||
);
|
||||
}
|
||||
const lastChunk = chunk.slice(i, lastIdx);
|
||||
processFullStringRow(response, streamState, rowID, rowTag, lastChunk);
|
||||
processFullStringRow(response, rowID, rowTag, lastChunk);
|
||||
// Reset state machine for a new row
|
||||
i = lastIdx;
|
||||
if (rowState === ROW_CHUNK_BY_NEWLINE) {
|
||||
|
||||
@@ -85,11 +85,7 @@ function getDebugInfo(obj) {
|
||||
if (debugInfo) {
|
||||
const copy = [];
|
||||
for (let i = 0; i < debugInfo.length; i++) {
|
||||
if (debugInfo[i].awaited && debugInfo[i].awaited.name === 'RSC stream') {
|
||||
// Ignore RSC stream I/O info.
|
||||
} else {
|
||||
copy.push(normalizeComponentInfo(debugInfo[i]));
|
||||
}
|
||||
copy.push(normalizeComponentInfo(debugInfo[i]));
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
@@ -2836,7 +2832,7 @@ describe('ReactFlight', () => {
|
||||
transport: expect.arrayContaining([]),
|
||||
},
|
||||
},
|
||||
{time: gate(flags => flags.enableAsyncDebugInfo) ? 53 : 21},
|
||||
{time: gate(flags => flags.enableAsyncDebugInfo) ? 23 : 21},
|
||||
]
|
||||
: undefined,
|
||||
);
|
||||
@@ -2847,7 +2843,7 @@ describe('ReactFlight', () => {
|
||||
expect(getDebugInfo(thirdPartyChildren[0])).toEqual(
|
||||
__DEV__
|
||||
? [
|
||||
{time: gate(flags => flags.enableAsyncDebugInfo) ? 54 : 22}, // Clamped to the start
|
||||
{time: gate(flags => flags.enableAsyncDebugInfo) ? 24 : 22}, // Clamped to the start
|
||||
{
|
||||
name: 'ThirdPartyComponent',
|
||||
env: 'third-party',
|
||||
@@ -2855,15 +2851,15 @@ describe('ReactFlight', () => {
|
||||
stack: ' in Object.<anonymous> (at **)',
|
||||
props: {},
|
||||
},
|
||||
{time: gate(flags => flags.enableAsyncDebugInfo) ? 54 : 22},
|
||||
{time: gate(flags => flags.enableAsyncDebugInfo) ? 55 : 23}, // This last one is when the promise resolved into the first party.
|
||||
{time: gate(flags => flags.enableAsyncDebugInfo) ? 24 : 22},
|
||||
{time: gate(flags => flags.enableAsyncDebugInfo) ? 25 : 23}, // This last one is when the promise resolved into the first party.
|
||||
]
|
||||
: undefined,
|
||||
);
|
||||
expect(getDebugInfo(thirdPartyChildren[1])).toEqual(
|
||||
__DEV__
|
||||
? [
|
||||
{time: gate(flags => flags.enableAsyncDebugInfo) ? 54 : 22}, // Clamped to the start
|
||||
{time: gate(flags => flags.enableAsyncDebugInfo) ? 24 : 22}, // Clamped to the start
|
||||
{
|
||||
name: 'ThirdPartyLazyComponent',
|
||||
env: 'third-party',
|
||||
@@ -2871,14 +2867,14 @@ describe('ReactFlight', () => {
|
||||
stack: ' in myLazy (at **)\n in lazyInitializer (at **)',
|
||||
props: {},
|
||||
},
|
||||
{time: gate(flags => flags.enableAsyncDebugInfo) ? 54 : 22},
|
||||
{time: gate(flags => flags.enableAsyncDebugInfo) ? 24 : 22},
|
||||
]
|
||||
: undefined,
|
||||
);
|
||||
expect(getDebugInfo(thirdPartyChildren[2])).toEqual(
|
||||
__DEV__
|
||||
? [
|
||||
{time: gate(flags => flags.enableAsyncDebugInfo) ? 54 : 22},
|
||||
{time: gate(flags => flags.enableAsyncDebugInfo) ? 24 : 22},
|
||||
{
|
||||
name: 'ThirdPartyFragmentComponent',
|
||||
env: 'third-party',
|
||||
@@ -2886,7 +2882,7 @@ describe('ReactFlight', () => {
|
||||
stack: ' in Object.<anonymous> (at **)',
|
||||
props: {},
|
||||
},
|
||||
{time: gate(flags => flags.enableAsyncDebugInfo) ? 54 : 22},
|
||||
{time: gate(flags => flags.enableAsyncDebugInfo) ? 24 : 22},
|
||||
]
|
||||
: undefined,
|
||||
);
|
||||
@@ -2964,10 +2960,17 @@ describe('ReactFlight', () => {
|
||||
{
|
||||
time: 16,
|
||||
},
|
||||
{
|
||||
env: 'third-party',
|
||||
key: null,
|
||||
name: 'ThirdPartyAsyncIterableComponent',
|
||||
props: {},
|
||||
stack: ' in Object.<anonymous> (at **)',
|
||||
},
|
||||
{
|
||||
time: 16,
|
||||
},
|
||||
{time: 31},
|
||||
{time: 17},
|
||||
]
|
||||
: undefined,
|
||||
);
|
||||
@@ -2976,7 +2979,7 @@ describe('ReactFlight', () => {
|
||||
expect(getDebugInfo(thirdPartyFragment)).toEqual(
|
||||
__DEV__
|
||||
? [
|
||||
{time: 32},
|
||||
{time: 18},
|
||||
{
|
||||
name: 'Keyed',
|
||||
env: 'Server',
|
||||
@@ -2987,14 +2990,19 @@ describe('ReactFlight', () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
time: 33,
|
||||
time: 19,
|
||||
},
|
||||
{
|
||||
time: 33,
|
||||
time: 19,
|
||||
},
|
||||
{
|
||||
time: 33,
|
||||
env: 'third-party',
|
||||
key: null,
|
||||
name: 'ThirdPartyAsyncIterableComponent',
|
||||
props: {},
|
||||
stack: ' in Object.<anonymous> (at **)',
|
||||
},
|
||||
{time: 19},
|
||||
]
|
||||
: undefined,
|
||||
);
|
||||
@@ -3002,7 +3010,7 @@ describe('ReactFlight', () => {
|
||||
expect(getDebugInfo(thirdPartyFragment.props.children)).toEqual(
|
||||
__DEV__
|
||||
? [
|
||||
{time: 33}, // Clamp to the start
|
||||
{time: 19}, // Clamp to the start
|
||||
{
|
||||
name: 'ThirdPartyAsyncIterableComponent',
|
||||
env: 'third-party',
|
||||
@@ -3010,7 +3018,7 @@ describe('ReactFlight', () => {
|
||||
stack: ' in Object.<anonymous> (at **)',
|
||||
props: {},
|
||||
},
|
||||
{time: 33},
|
||||
{time: 19},
|
||||
]
|
||||
: undefined,
|
||||
);
|
||||
@@ -3073,7 +3081,7 @@ describe('ReactFlight', () => {
|
||||
props: {},
|
||||
},
|
||||
{time: 16},
|
||||
{time: gate(flags => flags.enableAsyncDebugInfo) ? 24 : 17},
|
||||
{time: 17},
|
||||
]
|
||||
: undefined,
|
||||
);
|
||||
@@ -3839,115 +3847,4 @@ describe('ReactFlight', () => {
|
||||
|
||||
expect(ReactNoop).toMatchRenderedOutput(<div>not using props</div>);
|
||||
});
|
||||
|
||||
// @gate !__DEV__ || enableComponentPerformanceTrack
|
||||
it('produces correct parent stacks', async () => {
|
||||
function Container() {
|
||||
return ReactServer.createElement('div', null);
|
||||
}
|
||||
function ContainerParent() {
|
||||
return ReactServer.createElement(Container, null);
|
||||
}
|
||||
function App() {
|
||||
return ReactServer.createElement(
|
||||
'main',
|
||||
null,
|
||||
ReactServer.createElement(ContainerParent, null),
|
||||
);
|
||||
}
|
||||
|
||||
const transport = ReactNoopFlightServer.render({
|
||||
root: ReactServer.createElement(App, null),
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
const {root} = await ReactNoopFlightClient.read(transport);
|
||||
|
||||
ReactNoop.render(root);
|
||||
|
||||
expect(root.type).toBe('main');
|
||||
if (__DEV__) {
|
||||
const div = root.props.children;
|
||||
expect(getDebugInfo(div)).toEqual([
|
||||
{
|
||||
time: 14,
|
||||
},
|
||||
{
|
||||
env: 'Server',
|
||||
key: null,
|
||||
name: 'ContainerParent',
|
||||
owner: {
|
||||
env: 'Server',
|
||||
key: null,
|
||||
name: 'App',
|
||||
props: {},
|
||||
stack: ' in Object.<anonymous> (at **)',
|
||||
},
|
||||
props: {},
|
||||
stack: ' in App (at **)',
|
||||
},
|
||||
{
|
||||
time: 15,
|
||||
},
|
||||
{
|
||||
env: 'Server',
|
||||
key: null,
|
||||
name: 'Container',
|
||||
owner: {
|
||||
env: 'Server',
|
||||
key: null,
|
||||
name: 'ContainerParent',
|
||||
owner: {
|
||||
env: 'Server',
|
||||
key: null,
|
||||
name: 'App',
|
||||
props: {},
|
||||
stack: ' in Object.<anonymous> (at **)',
|
||||
},
|
||||
props: {},
|
||||
stack: ' in App (at **)',
|
||||
},
|
||||
props: {},
|
||||
stack: ' in ContainerParent (at **)',
|
||||
},
|
||||
{
|
||||
time: 16,
|
||||
},
|
||||
]);
|
||||
expect(getDebugInfo(root)).toEqual([
|
||||
{
|
||||
time: 12,
|
||||
},
|
||||
{
|
||||
env: 'Server',
|
||||
key: null,
|
||||
name: 'App',
|
||||
props: {},
|
||||
stack: ' in Object.<anonymous> (at **)',
|
||||
},
|
||||
{
|
||||
time: 13,
|
||||
},
|
||||
{
|
||||
time: 14,
|
||||
},
|
||||
{
|
||||
time: 15,
|
||||
},
|
||||
{
|
||||
time: 16,
|
||||
},
|
||||
]);
|
||||
} else {
|
||||
expect(root._debugInfo).toBe(undefined);
|
||||
expect(root._owner).toBe(undefined);
|
||||
}
|
||||
});
|
||||
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<main>
|
||||
<div />
|
||||
</main>,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
24
packages/react-markup/src/ReactMarkupServer.js
vendored
24
packages/react-markup/src/ReactMarkupServer.js
vendored
@@ -81,18 +81,7 @@ export function experimental_renderToHTML(
|
||||
options?: MarkupOptions,
|
||||
): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const flightResponse = createFlightResponse(
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
noServerCallOrFormAction,
|
||||
noServerCallOrFormAction,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
false,
|
||||
);
|
||||
const streamState = createFlightStreamState(flightResponse, null);
|
||||
const streamState = createFlightStreamState();
|
||||
const flightDestination = {
|
||||
push(chunk: string | null): boolean {
|
||||
if (chunk !== null) {
|
||||
@@ -186,6 +175,17 @@ export function experimental_renderToHTML(
|
||||
undefined,
|
||||
false,
|
||||
);
|
||||
const flightResponse = createFlightResponse(
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
noServerCallOrFormAction,
|
||||
noServerCallOrFormAction,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
false,
|
||||
);
|
||||
const resumableState = createResumableState(
|
||||
options ? options.identifierPrefix : undefined,
|
||||
undefined,
|
||||
|
||||
@@ -77,7 +77,7 @@ function read<T>(source: Source, options: ReadOptions): Thenable<T> {
|
||||
? options.debugChannel.onMessage
|
||||
: undefined,
|
||||
);
|
||||
const streamState = createStreamState(response, source);
|
||||
const streamState = createStreamState();
|
||||
for (let i = 0; i < source.length; i++) {
|
||||
processBinaryChunk(response, streamState, source[i], 0);
|
||||
}
|
||||
|
||||
@@ -115,7 +115,7 @@ function startReadingFromUniversalStream(
|
||||
// This is the same as startReadingFromStream except this allows WebSocketStreams which
|
||||
// return ArrayBuffer and string chunks instead of Uint8Array chunks. We could potentially
|
||||
// always allow streams with variable chunk types.
|
||||
const streamState = createStreamState(response, stream);
|
||||
const streamState = createStreamState();
|
||||
const reader = stream.getReader();
|
||||
function progress({
|
||||
done,
|
||||
@@ -149,9 +149,8 @@ function startReadingFromStream(
|
||||
response: FlightResponse,
|
||||
stream: ReadableStream,
|
||||
onDone: () => void,
|
||||
debugValue: mixed,
|
||||
): void {
|
||||
const streamState = createStreamState(response, debugValue);
|
||||
const streamState = createStreamState();
|
||||
const reader = stream.getReader();
|
||||
function progress({
|
||||
done,
|
||||
@@ -195,14 +194,9 @@ function createFromReadableStream<T>(
|
||||
options.debugChannel.readable,
|
||||
handleDone,
|
||||
);
|
||||
startReadingFromStream(response, stream, handleDone, stream);
|
||||
startReadingFromStream(response, stream, handleDone);
|
||||
} else {
|
||||
startReadingFromStream(
|
||||
response,
|
||||
stream,
|
||||
close.bind(null, response),
|
||||
stream,
|
||||
);
|
||||
startReadingFromStream(response, stream, close.bind(null, response));
|
||||
}
|
||||
return getRoot(response);
|
||||
}
|
||||
@@ -231,13 +225,12 @@ function createFromFetch<T>(
|
||||
options.debugChannel.readable,
|
||||
handleDone,
|
||||
);
|
||||
startReadingFromStream(response, (r.body: any), handleDone, r);
|
||||
startReadingFromStream(response, (r.body: any), handleDone);
|
||||
} else {
|
||||
startReadingFromStream(
|
||||
response,
|
||||
(r.body: any),
|
||||
close.bind(null, response),
|
||||
r,
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -66,7 +66,7 @@ function startReadingFromStream(
|
||||
stream: Readable,
|
||||
onEnd: () => void,
|
||||
): void {
|
||||
const streamState = createStreamState(response, stream);
|
||||
const streamState = createStreamState();
|
||||
|
||||
stream.on('data', chunk => {
|
||||
if (typeof chunk === 'string') {
|
||||
|
||||
@@ -141,7 +141,7 @@ function startReadingFromUniversalStream(
|
||||
// This is the same as startReadingFromStream except this allows WebSocketStreams which
|
||||
// return ArrayBuffer and string chunks instead of Uint8Array chunks. We could potentially
|
||||
// always allow streams with variable chunk types.
|
||||
const streamState = createStreamState(response, stream);
|
||||
const streamState = createStreamState();
|
||||
const reader = stream.getReader();
|
||||
function progress({
|
||||
done,
|
||||
@@ -175,9 +175,8 @@ function startReadingFromStream(
|
||||
response: FlightResponse,
|
||||
stream: ReadableStream,
|
||||
onDone: () => void,
|
||||
debugValue: mixed,
|
||||
): void {
|
||||
const streamState = createStreamState(response, debugValue);
|
||||
const streamState = createStreamState();
|
||||
const reader = stream.getReader();
|
||||
function progress({
|
||||
done,
|
||||
@@ -229,14 +228,9 @@ export function createFromReadableStream<T>(
|
||||
options.debugChannel.readable,
|
||||
handleDone,
|
||||
);
|
||||
startReadingFromStream(response, stream, handleDone, stream);
|
||||
startReadingFromStream(response, stream, handleDone);
|
||||
} else {
|
||||
startReadingFromStream(
|
||||
response,
|
||||
stream,
|
||||
close.bind(null, response),
|
||||
stream,
|
||||
);
|
||||
startReadingFromStream(response, stream, close.bind(null, response));
|
||||
}
|
||||
return getRoot(response);
|
||||
}
|
||||
@@ -265,13 +259,12 @@ export function createFromFetch<T>(
|
||||
options.debugChannel.readable,
|
||||
handleDone,
|
||||
);
|
||||
startReadingFromStream(response, (r.body: any), handleDone, r);
|
||||
startReadingFromStream(response, (r.body: any), handleDone);
|
||||
} else {
|
||||
startReadingFromStream(
|
||||
response,
|
||||
(r.body: any),
|
||||
close.bind(null, response),
|
||||
r,
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -115,9 +115,8 @@ function startReadingFromStream(
|
||||
response: FlightResponse,
|
||||
stream: ReadableStream,
|
||||
onDone: () => void,
|
||||
debugValue: mixed,
|
||||
): void {
|
||||
const streamState = createStreamState(response, debugValue);
|
||||
const streamState = createStreamState();
|
||||
const reader = stream.getReader();
|
||||
function progress({
|
||||
done,
|
||||
@@ -159,14 +158,9 @@ export function createFromReadableStream<T>(
|
||||
}
|
||||
};
|
||||
startReadingFromStream(response, options.debugChannel.readable, handleDone);
|
||||
startReadingFromStream(response, stream, handleDone, stream);
|
||||
startReadingFromStream(response, stream, handleDone);
|
||||
} else {
|
||||
startReadingFromStream(
|
||||
response,
|
||||
stream,
|
||||
close.bind(null, response),
|
||||
stream,
|
||||
);
|
||||
startReadingFromStream(response, stream, close.bind(null, response));
|
||||
}
|
||||
|
||||
return getRoot(response);
|
||||
@@ -196,13 +190,12 @@ export function createFromFetch<T>(
|
||||
options.debugChannel.readable,
|
||||
handleDone,
|
||||
);
|
||||
startReadingFromStream(response, (r.body: any), handleDone, r);
|
||||
startReadingFromStream(response, (r.body: any), handleDone);
|
||||
} else {
|
||||
startReadingFromStream(
|
||||
response,
|
||||
(r.body: any),
|
||||
close.bind(null, response),
|
||||
r,
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -61,7 +61,7 @@ function startReadingFromStream(
|
||||
stream: Readable,
|
||||
onEnd: () => void,
|
||||
): void {
|
||||
const streamState = createStreamState(response, stream);
|
||||
const streamState = createStreamState();
|
||||
|
||||
stream.on('data', chunk => {
|
||||
if (typeof chunk === 'string') {
|
||||
|
||||
@@ -114,7 +114,7 @@ function startReadingFromUniversalStream(
|
||||
// This is the same as startReadingFromStream except this allows WebSocketStreams which
|
||||
// return ArrayBuffer and string chunks instead of Uint8Array chunks. We could potentially
|
||||
// always allow streams with variable chunk types.
|
||||
const streamState = createStreamState(response, stream);
|
||||
const streamState = createStreamState();
|
||||
const reader = stream.getReader();
|
||||
function progress({
|
||||
done,
|
||||
@@ -148,9 +148,8 @@ function startReadingFromStream(
|
||||
response: FlightResponse,
|
||||
stream: ReadableStream,
|
||||
onDone: () => void,
|
||||
debugValue: mixed,
|
||||
): void {
|
||||
const streamState = createStreamState(response, debugValue);
|
||||
const streamState = createStreamState();
|
||||
const reader = stream.getReader();
|
||||
function progress({
|
||||
done,
|
||||
@@ -195,14 +194,9 @@ function createFromReadableStream<T>(
|
||||
options.debugChannel.readable,
|
||||
handleDone,
|
||||
);
|
||||
startReadingFromStream(response, stream, handleDone, stream);
|
||||
startReadingFromStream(response, stream, handleDone);
|
||||
} else {
|
||||
startReadingFromStream(
|
||||
response,
|
||||
stream,
|
||||
close.bind(null, response),
|
||||
stream,
|
||||
);
|
||||
startReadingFromStream(response, stream, close.bind(null, response));
|
||||
}
|
||||
return getRoot(response);
|
||||
}
|
||||
@@ -231,13 +225,12 @@ function createFromFetch<T>(
|
||||
options.debugChannel.readable,
|
||||
handleDone,
|
||||
);
|
||||
startReadingFromStream(response, (r.body: any), handleDone, r);
|
||||
startReadingFromStream(response, (r.body: any), handleDone);
|
||||
} else {
|
||||
startReadingFromStream(
|
||||
response,
|
||||
(r.body: any),
|
||||
close.bind(null, response),
|
||||
r,
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -117,9 +117,8 @@ function startReadingFromStream(
|
||||
response: FlightResponse,
|
||||
stream: ReadableStream,
|
||||
onDone: () => void,
|
||||
debugValue: mixed,
|
||||
): void {
|
||||
const streamState = createStreamState(response, debugValue);
|
||||
const streamState = createStreamState();
|
||||
const reader = stream.getReader();
|
||||
function progress({
|
||||
done,
|
||||
@@ -161,14 +160,9 @@ function createFromReadableStream<T>(
|
||||
}
|
||||
};
|
||||
startReadingFromStream(response, options.debugChannel.readable, handleDone);
|
||||
startReadingFromStream(response, stream, handleDone, stream);
|
||||
startReadingFromStream(response, stream, handleDone);
|
||||
} else {
|
||||
startReadingFromStream(
|
||||
response,
|
||||
stream,
|
||||
close.bind(null, response),
|
||||
stream,
|
||||
);
|
||||
startReadingFromStream(response, stream, close.bind(null, response));
|
||||
}
|
||||
|
||||
return getRoot(response);
|
||||
@@ -198,13 +192,12 @@ function createFromFetch<T>(
|
||||
options.debugChannel.readable,
|
||||
handleDone,
|
||||
);
|
||||
startReadingFromStream(response, (r.body: any), handleDone, r);
|
||||
startReadingFromStream(response, (r.body: any), handleDone);
|
||||
} else {
|
||||
startReadingFromStream(
|
||||
response,
|
||||
(r.body: any),
|
||||
close.bind(null, response),
|
||||
r,
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -69,7 +69,7 @@ function startReadingFromStream(
|
||||
stream: Readable,
|
||||
onEnd: () => void,
|
||||
): void {
|
||||
const streamState = createStreamState(response, stream);
|
||||
const streamState = createStreamState();
|
||||
|
||||
stream.on('data', chunk => {
|
||||
if (typeof chunk === 'string') {
|
||||
|
||||
@@ -1239,24 +1239,17 @@ describe('ReactFlightDOMEdge', () => {
|
||||
name: 'Greeting',
|
||||
env: 'Server',
|
||||
});
|
||||
if (gate(flags => flags.enableAsyncDebugInfo)) {
|
||||
expect(lazyWrapper._debugInfo).toEqual([
|
||||
{time: 12},
|
||||
greetInfo,
|
||||
{time: 13},
|
||||
expect.objectContaining({
|
||||
name: 'Container',
|
||||
env: 'Server',
|
||||
owner: greetInfo,
|
||||
}),
|
||||
{time: 14},
|
||||
expect.objectContaining({
|
||||
awaited: expect.objectContaining({
|
||||
name: 'RSC stream',
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
}
|
||||
expect(lazyWrapper._debugInfo).toEqual([
|
||||
{time: 12},
|
||||
greetInfo,
|
||||
{time: 13},
|
||||
expect.objectContaining({
|
||||
name: 'Container',
|
||||
env: 'Server',
|
||||
owner: greetInfo,
|
||||
}),
|
||||
{time: 14},
|
||||
]);
|
||||
// The owner that created the span was the outer server component.
|
||||
// We expect the debug info to be referentially equal to the owner.
|
||||
expect(greeting._owner).toBe(lazyWrapper._debugInfo[1]);
|
||||
|
||||
@@ -114,7 +114,7 @@ function startReadingFromUniversalStream(
|
||||
// This is the same as startReadingFromStream except this allows WebSocketStreams which
|
||||
// return ArrayBuffer and string chunks instead of Uint8Array chunks. We could potentially
|
||||
// always allow streams with variable chunk types.
|
||||
const streamState = createStreamState(response, stream);
|
||||
const streamState = createStreamState();
|
||||
const reader = stream.getReader();
|
||||
function progress({
|
||||
done,
|
||||
@@ -148,9 +148,8 @@ function startReadingFromStream(
|
||||
response: FlightResponse,
|
||||
stream: ReadableStream,
|
||||
onDone: () => void,
|
||||
debugValue: mixed,
|
||||
): void {
|
||||
const streamState = createStreamState(response, debugValue);
|
||||
const streamState = createStreamState();
|
||||
const reader = stream.getReader();
|
||||
function progress({
|
||||
done,
|
||||
@@ -195,14 +194,9 @@ function createFromReadableStream<T>(
|
||||
options.debugChannel.readable,
|
||||
handleDone,
|
||||
);
|
||||
startReadingFromStream(response, stream, handleDone, stream);
|
||||
startReadingFromStream(response, stream, handleDone);
|
||||
} else {
|
||||
startReadingFromStream(
|
||||
response,
|
||||
stream,
|
||||
close.bind(null, response),
|
||||
stream,
|
||||
);
|
||||
startReadingFromStream(response, stream, close.bind(null, response));
|
||||
}
|
||||
return getRoot(response);
|
||||
}
|
||||
@@ -231,13 +225,12 @@ function createFromFetch<T>(
|
||||
options.debugChannel.readable,
|
||||
handleDone,
|
||||
);
|
||||
startReadingFromStream(response, (r.body: any), handleDone, r);
|
||||
startReadingFromStream(response, (r.body: any), handleDone);
|
||||
} else {
|
||||
startReadingFromStream(
|
||||
response,
|
||||
(r.body: any),
|
||||
close.bind(null, response),
|
||||
r,
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -117,9 +117,8 @@ function startReadingFromStream(
|
||||
response: FlightResponse,
|
||||
stream: ReadableStream,
|
||||
onDone: () => void,
|
||||
debugValue: mixed,
|
||||
): void {
|
||||
const streamState = createStreamState(response, debugValue);
|
||||
const streamState = createStreamState();
|
||||
const reader = stream.getReader();
|
||||
function progress({
|
||||
done,
|
||||
@@ -161,14 +160,9 @@ function createFromReadableStream<T>(
|
||||
}
|
||||
};
|
||||
startReadingFromStream(response, options.debugChannel.readable, handleDone);
|
||||
startReadingFromStream(response, stream, handleDone, stream);
|
||||
startReadingFromStream(response, stream, handleDone);
|
||||
} else {
|
||||
startReadingFromStream(
|
||||
response,
|
||||
stream,
|
||||
close.bind(null, response),
|
||||
stream,
|
||||
);
|
||||
startReadingFromStream(response, stream, close.bind(null, response));
|
||||
}
|
||||
|
||||
return getRoot(response);
|
||||
@@ -198,13 +192,12 @@ function createFromFetch<T>(
|
||||
options.debugChannel.readable,
|
||||
handleDone,
|
||||
);
|
||||
startReadingFromStream(response, (r.body: any), handleDone, r);
|
||||
startReadingFromStream(response, (r.body: any), handleDone);
|
||||
} else {
|
||||
startReadingFromStream(
|
||||
response,
|
||||
(r.body: any),
|
||||
close.bind(null, response),
|
||||
r,
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -69,7 +69,7 @@ function startReadingFromStream(
|
||||
stream: Readable,
|
||||
onEnd: () => void,
|
||||
): void {
|
||||
const streamState = createStreamState(response, stream);
|
||||
const streamState = createStreamState();
|
||||
|
||||
stream.on('data', chunk => {
|
||||
if (typeof chunk === 'string') {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user