Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c9faa3ec3 |
@@ -272,10 +272,12 @@ function runWithEnvironment(
|
||||
validateNoSetStateInRender(hir).unwrap();
|
||||
}
|
||||
|
||||
if (env.config.validateNoDerivedComputationsInEffects) {
|
||||
validateNoDerivedComputationsInEffects(hir);
|
||||
}
|
||||
|
||||
if (env.config.validateNoDerivedComputationsInEffects_exp) {
|
||||
env.logErrors(validateNoDerivedComputationsInEffects_exp(hir));
|
||||
} else if (env.config.validateNoDerivedComputationsInEffects) {
|
||||
validateNoDerivedComputationsInEffects(hir);
|
||||
}
|
||||
|
||||
if (env.config.validateNoSetStateInEffects) {
|
||||
|
||||
@@ -1875,14 +1875,6 @@ export function isSetStateType(id: Identifier): boolean {
|
||||
return id.type.kind === 'Function' && id.type.shapeId === 'BuiltInSetState';
|
||||
}
|
||||
|
||||
export function isAnySetStateType(id: Identifier): boolean {
|
||||
return (
|
||||
id.type.kind === 'Function' &&
|
||||
(id.type.shapeId === 'InferredSetState' ||
|
||||
id.type.shapeId === 'BuiltInSetState')
|
||||
);
|
||||
}
|
||||
|
||||
export function isUseActionStateType(id: Identifier): boolean {
|
||||
return (
|
||||
id.type.kind === 'Object' && id.type.shapeId === 'BuiltInUseActionState'
|
||||
|
||||
@@ -386,7 +386,6 @@ export const BuiltInJsxId = 'BuiltInJsx';
|
||||
export const BuiltInObjectId = 'BuiltInObject';
|
||||
export const BuiltInUseStateId = 'BuiltInUseState';
|
||||
export const BuiltInSetStateId = 'BuiltInSetState';
|
||||
export const InferredSetState = 'InferredSetState';
|
||||
export const BuiltInUseActionStateId = 'BuiltInUseActionState';
|
||||
export const BuiltInSetActionStateId = 'BuiltInSetActionState';
|
||||
export const BuiltInUseRefId = 'BuiltInUseRefId';
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
ReactiveScopeDependencies,
|
||||
Terminal,
|
||||
isUseRefType,
|
||||
isAnySetStateType,
|
||||
isSetStateType,
|
||||
isFireFunctionType,
|
||||
makeScopeId,
|
||||
HIR,
|
||||
@@ -223,7 +223,7 @@ export function inferEffectDependencies(fn: HIRFunction): void {
|
||||
for (const maybeDep of minimalDeps) {
|
||||
if (
|
||||
((isUseRefType(maybeDep.identifier) ||
|
||||
isAnySetStateType(maybeDep.identifier)) &&
|
||||
isSetStateType(maybeDep.identifier)) &&
|
||||
!reactiveIds.has(maybeDep.identifier.id)) ||
|
||||
isFireFunctionType(maybeDep.identifier) ||
|
||||
isEffectEventFunctionType(maybeDep.identifier)
|
||||
|
||||
@@ -31,8 +31,8 @@ import {
|
||||
BuiltInObjectId,
|
||||
BuiltInPropsId,
|
||||
BuiltInRefValueId,
|
||||
BuiltInSetStateId,
|
||||
BuiltInUseRefId,
|
||||
InferredSetState,
|
||||
} from '../HIR/ObjectShape';
|
||||
import {eachInstructionLValue, eachInstructionOperand} from '../HIR/visitors';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
@@ -281,7 +281,7 @@ function* generateInstructionTypes(
|
||||
if (env.config.enableTreatSetIdentifiersAsStateSetters) {
|
||||
const name = getName(names, value.callee.identifier.id);
|
||||
if (name.startsWith('set')) {
|
||||
shapeId = InferredSetState;
|
||||
shapeId = BuiltInSetStateId;
|
||||
}
|
||||
}
|
||||
yield equation(value.callee.identifier.type, {
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
isUseStateType,
|
||||
BasicBlock,
|
||||
isUseRefType,
|
||||
GeneratedSource,
|
||||
SourceLocation,
|
||||
} from '../HIR';
|
||||
import {eachInstructionLValue, eachInstructionOperand} from '../HIR/visitors';
|
||||
@@ -41,8 +42,8 @@ type ValidationContext = {
|
||||
readonly errors: CompilerError;
|
||||
readonly derivationCache: DerivationCache;
|
||||
readonly effects: Set<HIRFunction>;
|
||||
readonly setStateLoads: Map<IdentifierId, IdentifierId | null>;
|
||||
readonly setStateUsages: Map<IdentifierId, Set<SourceLocation>>;
|
||||
readonly setStateCache: Map<string | undefined | null, Array<Place>>;
|
||||
readonly effectSetStateCache: Map<string | undefined | null, Array<Place>>;
|
||||
};
|
||||
|
||||
class DerivationCache {
|
||||
@@ -179,16 +180,19 @@ export function validateNoDerivedComputationsInEffects_exp(
|
||||
const errors = new CompilerError();
|
||||
const effects: Set<HIRFunction> = new Set();
|
||||
|
||||
const setStateLoads: Map<IdentifierId, IdentifierId> = new Map();
|
||||
const setStateUsages: Map<IdentifierId, Set<SourceLocation>> = new Map();
|
||||
const setStateCache: Map<string | undefined | null, Array<Place>> = new Map();
|
||||
const effectSetStateCache: Map<
|
||||
string | undefined | null,
|
||||
Array<Place>
|
||||
> = new Map();
|
||||
|
||||
const context: ValidationContext = {
|
||||
functions,
|
||||
errors,
|
||||
derivationCache,
|
||||
effects,
|
||||
setStateLoads,
|
||||
setStateUsages,
|
||||
setStateCache,
|
||||
effectSetStateCache,
|
||||
};
|
||||
|
||||
if (fn.fnType === 'Hook') {
|
||||
@@ -277,61 +281,11 @@ function joinValue(
|
||||
return 'fromPropsAndState';
|
||||
}
|
||||
|
||||
function getRootSetState(
|
||||
key: IdentifierId,
|
||||
loads: Map<IdentifierId, IdentifierId | null>,
|
||||
visited: Set<IdentifierId> = new Set(),
|
||||
): IdentifierId | null {
|
||||
if (visited.has(key)) {
|
||||
return null;
|
||||
}
|
||||
visited.add(key);
|
||||
|
||||
const parentId = loads.get(key);
|
||||
|
||||
if (parentId === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (parentId === null) {
|
||||
return key;
|
||||
}
|
||||
|
||||
return getRootSetState(parentId, loads, visited);
|
||||
}
|
||||
|
||||
function maybeRecordSetState(
|
||||
instr: Instruction,
|
||||
loads: Map<IdentifierId, IdentifierId | null>,
|
||||
usages: Map<IdentifierId, Set<SourceLocation>>,
|
||||
): void {
|
||||
for (const operand of eachInstructionLValue(instr)) {
|
||||
if (
|
||||
instr.value.kind === 'LoadLocal' &&
|
||||
loads.has(instr.value.place.identifier.id)
|
||||
) {
|
||||
loads.set(operand.identifier.id, instr.value.place.identifier.id);
|
||||
} else {
|
||||
if (isSetStateType(operand.identifier)) {
|
||||
// this is a root setState
|
||||
loads.set(operand.identifier.id, null);
|
||||
}
|
||||
}
|
||||
|
||||
const rootSetState = getRootSetState(operand.identifier.id, loads);
|
||||
if (rootSetState !== null && usages.get(rootSetState) === undefined) {
|
||||
usages.set(rootSetState, new Set([operand.loc]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function recordInstructionDerivations(
|
||||
instr: Instruction,
|
||||
context: ValidationContext,
|
||||
isFirstPass: boolean,
|
||||
): void {
|
||||
maybeRecordSetState(instr, context.setStateLoads, context.setStateUsages);
|
||||
|
||||
let typeOfValue: TypeOfValue = 'ignored';
|
||||
let isSource: boolean = false;
|
||||
const sources: Set<IdentifierId> = new Set();
|
||||
@@ -358,25 +312,21 @@ function recordInstructionDerivations(
|
||||
context.effects.add(effectFunction.loweredFunc.func);
|
||||
}
|
||||
} else if (isUseStateType(lvalue.identifier) && value.args.length > 0) {
|
||||
typeOfValue = 'fromState';
|
||||
context.derivationCache.addDerivationEntry(
|
||||
lvalue,
|
||||
new Set(),
|
||||
typeOfValue,
|
||||
true,
|
||||
);
|
||||
return;
|
||||
isSource = true;
|
||||
typeOfValue = joinValue(typeOfValue, 'fromState');
|
||||
}
|
||||
}
|
||||
|
||||
for (const operand of eachInstructionOperand(instr)) {
|
||||
if (context.setStateLoads.has(operand.identifier.id)) {
|
||||
const rootSetStateId = getRootSetState(
|
||||
operand.identifier.id,
|
||||
context.setStateLoads,
|
||||
);
|
||||
if (rootSetStateId !== null) {
|
||||
context.setStateUsages.get(rootSetStateId)?.add(operand.loc);
|
||||
if (
|
||||
isSetStateType(operand.identifier) &&
|
||||
operand.loc !== GeneratedSource &&
|
||||
isFirstPass
|
||||
) {
|
||||
if (context.setStateCache.has(operand.loc.identifierName)) {
|
||||
context.setStateCache.get(operand.loc.identifierName)!.push(operand);
|
||||
} else {
|
||||
context.setStateCache.set(operand.loc.identifierName, [operand]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -574,26 +524,6 @@ function renderTree(
|
||||
return result;
|
||||
}
|
||||
|
||||
function getFnLocalDeps(
|
||||
fn: FunctionExpression | undefined,
|
||||
): Set<IdentifierId> | undefined {
|
||||
if (!fn) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const deps: Set<IdentifierId> = new Set();
|
||||
|
||||
for (const [, block] of fn.loweredFunc.func.body.blocks) {
|
||||
for (const instr of block.instructions) {
|
||||
if (instr.value.kind === 'LoadLocal') {
|
||||
deps.add(instr.value.place.identifier.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return deps;
|
||||
}
|
||||
|
||||
function validateEffect(
|
||||
effectFunction: HIRFunction,
|
||||
context: ValidationContext,
|
||||
@@ -602,33 +532,13 @@ function validateEffect(
|
||||
|
||||
const effectDerivedSetStateCalls: Array<{
|
||||
value: CallExpression;
|
||||
id: IdentifierId;
|
||||
loc: SourceLocation;
|
||||
sourceIds: Set<IdentifierId>;
|
||||
typeOfValue: TypeOfValue;
|
||||
}> = [];
|
||||
|
||||
const effectSetStateUsages: Map<
|
||||
IdentifierId,
|
||||
Set<SourceLocation>
|
||||
> = new Map();
|
||||
|
||||
let cleanUpFunctionDeps: Set<IdentifierId> | undefined;
|
||||
|
||||
const globals: Set<IdentifierId> = new Set();
|
||||
for (const block of effectFunction.body.blocks.values()) {
|
||||
/*
|
||||
* if the block is in an effect and is of type return then its an effect's cleanup function
|
||||
* if the cleanup function depends on a value from which effect-set state is derived then
|
||||
* we can't validate
|
||||
*/
|
||||
if (
|
||||
block.terminal.kind === 'return' &&
|
||||
block.terminal.returnVariant === 'Explicit'
|
||||
) {
|
||||
cleanUpFunctionDeps = getFnLocalDeps(
|
||||
context.functions.get(block.terminal.value.identifier.id),
|
||||
);
|
||||
}
|
||||
for (const pred of block.preds) {
|
||||
if (!seenBlocks.has(pred)) {
|
||||
// skip if block has a back edge
|
||||
@@ -642,16 +552,19 @@ function validateEffect(
|
||||
return;
|
||||
}
|
||||
|
||||
maybeRecordSetState(instr, context.setStateLoads, effectSetStateUsages);
|
||||
|
||||
for (const operand of eachInstructionOperand(instr)) {
|
||||
if (context.setStateLoads.has(operand.identifier.id)) {
|
||||
const rootSetStateId = getRootSetState(
|
||||
operand.identifier.id,
|
||||
context.setStateLoads,
|
||||
);
|
||||
if (rootSetStateId !== null) {
|
||||
effectSetStateUsages.get(rootSetStateId)?.add(operand.loc);
|
||||
if (
|
||||
isSetStateType(operand.identifier) &&
|
||||
operand.loc !== GeneratedSource
|
||||
) {
|
||||
if (context.effectSetStateCache.has(operand.loc.identifierName)) {
|
||||
context.effectSetStateCache
|
||||
.get(operand.loc.identifierName)!
|
||||
.push(operand);
|
||||
} else {
|
||||
context.effectSetStateCache.set(operand.loc.identifierName, [
|
||||
operand,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -669,7 +582,7 @@ function validateEffect(
|
||||
if (argMetadata !== undefined) {
|
||||
effectDerivedSetStateCalls.push({
|
||||
value: instr.value,
|
||||
id: instr.value.callee.identifier.id,
|
||||
loc: instr.value.callee.loc,
|
||||
sourceIds: argMetadata.sourcesIds,
|
||||
typeOfValue: argMetadata.typeOfValue,
|
||||
});
|
||||
@@ -703,17 +616,15 @@ function validateEffect(
|
||||
}
|
||||
|
||||
for (const derivedSetStateCall of effectDerivedSetStateCalls) {
|
||||
const rootSetStateCall = getRootSetState(
|
||||
derivedSetStateCall.id,
|
||||
context.setStateLoads,
|
||||
);
|
||||
|
||||
if (
|
||||
rootSetStateCall !== null &&
|
||||
effectSetStateUsages.has(rootSetStateCall) &&
|
||||
context.setStateUsages.has(rootSetStateCall) &&
|
||||
effectSetStateUsages.get(rootSetStateCall)!.size ===
|
||||
context.setStateUsages.get(rootSetStateCall)!.size - 1
|
||||
derivedSetStateCall.loc !== GeneratedSource &&
|
||||
context.effectSetStateCache.has(derivedSetStateCall.loc.identifierName) &&
|
||||
context.setStateCache.has(derivedSetStateCall.loc.identifierName) &&
|
||||
context.effectSetStateCache.get(derivedSetStateCall.loc.identifierName)!
|
||||
.length ===
|
||||
context.setStateCache.get(derivedSetStateCall.loc.identifierName)!
|
||||
.length -
|
||||
1
|
||||
) {
|
||||
const propsSet = new Set<string>();
|
||||
const stateSet = new Set<string>();
|
||||
@@ -739,12 +650,6 @@ function validateEffect(
|
||||
),
|
||||
);
|
||||
|
||||
for (const dep of derivedSetStateCall.sourceIds) {
|
||||
if (cleanUpFunctionDeps !== undefined && cleanUpFunctionDeps.has(dep)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const propsArr = Array.from(propsSet);
|
||||
const stateArr = Array.from(stateSet);
|
||||
|
||||
|
||||
@@ -5,13 +5,12 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {Environment} from '../HIR/Environment';
|
||||
import {
|
||||
CompilerDiagnostic,
|
||||
CompilerError,
|
||||
ErrorCategory,
|
||||
} from '../CompilerError';
|
||||
import {HIRFunction, IdentifierId, isAnySetStateType} from '../HIR';
|
||||
import {HIRFunction, IdentifierId, isSetStateType} from '../HIR';
|
||||
import {computeUnconditionalBlocks} from '../HIR/ComputeUnconditionalBlocks';
|
||||
import {eachInstructionValueOperand} from '../HIR/visitors';
|
||||
import {Result} from '../Utils/Result';
|
||||
@@ -86,7 +85,7 @@ function validateNoSetStateInRenderImpl(
|
||||
// faster-path to check if the function expression references a setState
|
||||
[...eachInstructionValueOperand(instr.value)].some(
|
||||
operand =>
|
||||
isAnySetStateType(operand.identifier) ||
|
||||
isSetStateType(operand.identifier) ||
|
||||
unconditionalSetStateFunctions.has(operand.identifier.id),
|
||||
) &&
|
||||
// if yes, does it unconditonally call it?
|
||||
@@ -137,7 +136,7 @@ function validateNoSetStateInRenderImpl(
|
||||
case 'CallExpression': {
|
||||
const callee = instr.value.callee;
|
||||
if (
|
||||
isAnySetStateType(callee.identifier) ||
|
||||
isSetStateType(callee.identifier) ||
|
||||
unconditionalSetStateFunctions.has(callee.identifier.id)
|
||||
) {
|
||||
if (activeManualMemoId !== null) {
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component(file: File) {
|
||||
const [imageUrl, setImageUrl] = useState(null);
|
||||
|
||||
/*
|
||||
* Cleaning up the variable or a source of the variable used to setState
|
||||
* inside the effect communicates that we always need to clean up something
|
||||
* which is a valid use case for useEffect. In which case we want to
|
||||
* avoid an throwing
|
||||
*/
|
||||
useEffect(() => {
|
||||
const imageUrlPrepared = URL.createObjectURL(file);
|
||||
setImageUrl(imageUrlPrepared);
|
||||
return () => URL.revokeObjectURL(imageUrlPrepared);
|
||||
}, [file]);
|
||||
|
||||
return <Image src={imageUrl} xstyle={styles.imageSizeLimits} />;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
function Component(file) {
|
||||
const $ = _c(5);
|
||||
const [imageUrl, setImageUrl] = useState(null);
|
||||
let t0;
|
||||
let t1;
|
||||
if ($[0] !== file) {
|
||||
t0 = () => {
|
||||
const imageUrlPrepared = URL.createObjectURL(file);
|
||||
setImageUrl(imageUrlPrepared);
|
||||
return () => URL.revokeObjectURL(imageUrlPrepared);
|
||||
};
|
||||
t1 = [file];
|
||||
$[0] = file;
|
||||
$[1] = t0;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
t1 = $[2];
|
||||
}
|
||||
useEffect(t0, t1);
|
||||
let t2;
|
||||
if ($[3] !== imageUrl) {
|
||||
t2 = <Image src={imageUrl} xstyle={styles.imageSizeLimits} />;
|
||||
$[3] = imageUrl;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```
|
||||
{"kind":"CompileSuccess","fnLoc":{"start":{"line":5,"column":0,"index":108},"end":{"line":21,"column":1,"index":700},"filename":"effect-with-cleanup-function-depending-on-derived-computation-value.ts"},"fnName":"Component","memoSlots":5,"memoBlocks":2,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -1,21 +0,0 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component(file: File) {
|
||||
const [imageUrl, setImageUrl] = useState(null);
|
||||
|
||||
/*
|
||||
* Cleaning up the variable or a source of the variable used to setState
|
||||
* inside the effect communicates that we always need to clean up something
|
||||
* which is a valid use case for useEffect. In which case we want to
|
||||
* avoid an throwing
|
||||
*/
|
||||
useEffect(() => {
|
||||
const imageUrlPrepared = URL.createObjectURL(file);
|
||||
setImageUrl(imageUrlPrepared);
|
||||
return () => URL.revokeObjectURL(imageUrlPrepared);
|
||||
}, [file]);
|
||||
|
||||
return <Image src={imageUrl} xstyle={styles.imageSizeLimits} />;
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
|
||||
function Component({prop}) {
|
||||
const [s, setS] = useState();
|
||||
const [second, setSecond] = useState(prop);
|
||||
|
||||
/*
|
||||
* `second` is a source of state. It will inherit the value of `prop` in
|
||||
* the first render, but after that it will no longer be updated when
|
||||
* `prop` changes. So we shouldn't consider `second` as being derived from
|
||||
* `prop`
|
||||
*/
|
||||
useEffect(() => {
|
||||
setS(second);
|
||||
}, [second]);
|
||||
|
||||
return <div>{s}</div>;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(5);
|
||||
const { prop } = t0;
|
||||
const [s, setS] = useState();
|
||||
const [second] = useState(prop);
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== second) {
|
||||
t1 = () => {
|
||||
setS(second);
|
||||
};
|
||||
t2 = [second];
|
||||
$[0] = second;
|
||||
$[1] = t1;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
t2 = $[2];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[3] !== s) {
|
||||
t3 = <div>{s}</div>;
|
||||
$[3] = s;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```
|
||||
{"kind":"CompileError","detail":{"options":{"description":"Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user\n\nThis setState call is setting a derived value that depends on the following reactive sources:\n\nState: [second]\n\nData Flow Tree:\n└── second (State)\n\nSee: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state","category":"EffectDerivationsOfState","reason":"You might not need an effect. Derive values in render, not effects.","details":[{"kind":"error","loc":{"start":{"line":14,"column":4,"index":443},"end":{"line":14,"column":8,"index":447},"filename":"usestate-derived-from-prop-no-show-in-data-flow-tree.ts","identifierName":"setS"},"message":"This should be computed during render, not in an effect"}]}},"fnLoc":null}
|
||||
{"kind":"CompileSuccess","fnLoc":{"start":{"line":3,"column":0,"index":64},"end":{"line":18,"column":1,"index":500},"filename":"usestate-derived-from-prop-no-show-in-data-flow-tree.ts"},"fnName":"Component","memoSlots":5,"memoBlocks":2,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -1,18 +0,0 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
|
||||
function Component({prop}) {
|
||||
const [s, setS] = useState();
|
||||
const [second, setSecond] = useState(prop);
|
||||
|
||||
/*
|
||||
* `second` is a source of state. It will inherit the value of `prop` in
|
||||
* the first render, but after that it will no longer be updated when
|
||||
* `prop` changes. So we shouldn't consider `second` as being derived from
|
||||
* `prop`
|
||||
*/
|
||||
useEffect(() => {
|
||||
setS(second);
|
||||
}, [second]);
|
||||
|
||||
return <div>{s}</div>;
|
||||
}
|
||||
@@ -10,17 +10,11 @@
|
||||
'use strict';
|
||||
|
||||
let React;
|
||||
let ReactDOM;
|
||||
let ReactDOMClient;
|
||||
let Scheduler;
|
||||
let act;
|
||||
let Activity;
|
||||
let useState;
|
||||
let useLayoutEffect;
|
||||
let useEffect;
|
||||
let LegacyHidden;
|
||||
let assertLog;
|
||||
let Suspense;
|
||||
let ReactDOM;
|
||||
let ReactDOMClient;
|
||||
let act;
|
||||
|
||||
describe('ReactDOMActivity', () => {
|
||||
let container;
|
||||
@@ -28,19 +22,11 @@ describe('ReactDOMActivity', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
React = require('react');
|
||||
Scheduler = require('scheduler/unstable_mock');
|
||||
Activity = React.Activity;
|
||||
useState = React.useState;
|
||||
Suspense = React.Suspense;
|
||||
useState = React.useState;
|
||||
LegacyHidden = React.unstable_LegacyHidden;
|
||||
useLayoutEffect = React.useLayoutEffect;
|
||||
useEffect = React.useEffect;
|
||||
ReactDOM = require('react-dom');
|
||||
ReactDOMClient = require('react-dom/client');
|
||||
const InternalTestUtils = require('internal-test-utils');
|
||||
act = InternalTestUtils.act;
|
||||
assertLog = InternalTestUtils.assertLog;
|
||||
act = require('internal-test-utils').act;
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
});
|
||||
@@ -49,11 +35,6 @@ describe('ReactDOMActivity', () => {
|
||||
document.body.removeChild(container);
|
||||
});
|
||||
|
||||
function Text(props) {
|
||||
Scheduler.log(props.text);
|
||||
return <span prop={props.text}>{props.children}</span>;
|
||||
}
|
||||
|
||||
// @gate enableActivity
|
||||
it(
|
||||
'hiding an Activity boundary also hides the direct children of any ' +
|
||||
@@ -72,7 +53,7 @@ describe('ReactDOMActivity', () => {
|
||||
);
|
||||
}
|
||||
|
||||
function App() {
|
||||
function App({portalContents}) {
|
||||
return (
|
||||
<Accordion>
|
||||
<div>
|
||||
@@ -118,7 +99,7 @@ describe('ReactDOMActivity', () => {
|
||||
);
|
||||
}
|
||||
|
||||
function App() {
|
||||
function App({portalContents}) {
|
||||
return (
|
||||
<Activity mode="hidden">
|
||||
<div>
|
||||
@@ -150,416 +131,4 @@ describe('ReactDOMActivity', () => {
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
// @gate enableActivity
|
||||
it('hides new portals added to an already hidden tree', async () => {
|
||||
function Child() {
|
||||
return <Text text="Child" />;
|
||||
}
|
||||
|
||||
const portalContainer = document.createElement('div');
|
||||
|
||||
function Portal({children}) {
|
||||
return <div>{ReactDOM.createPortal(children, portalContainer)}</div>;
|
||||
}
|
||||
|
||||
const root = ReactDOMClient.createRoot(container);
|
||||
// Mount hidden tree.
|
||||
await act(() => {
|
||||
root.render(
|
||||
<Activity mode="hidden">
|
||||
<Text text="Parent" />
|
||||
</Activity>,
|
||||
);
|
||||
});
|
||||
assertLog(['Parent']);
|
||||
expect(container.innerHTML).toBe(
|
||||
'<span prop="Parent" style="display: none;"></span>',
|
||||
);
|
||||
expect(portalContainer.innerHTML).toBe('');
|
||||
|
||||
// Add a portal inside the hidden tree.
|
||||
await act(() => {
|
||||
root.render(
|
||||
<Activity mode="hidden">
|
||||
<Text text="Parent" />
|
||||
<Portal>
|
||||
<Child />
|
||||
</Portal>
|
||||
</Activity>,
|
||||
);
|
||||
});
|
||||
assertLog(['Parent', 'Child']);
|
||||
expect(container.innerHTML).toBe(
|
||||
'<span prop="Parent" style="display: none;"></span><div style="display: none;"></div>',
|
||||
);
|
||||
expect(portalContainer.innerHTML).toBe(
|
||||
'<span prop="Child" style="display: none;"></span>',
|
||||
);
|
||||
|
||||
// Now reveal it.
|
||||
await act(() => {
|
||||
root.render(
|
||||
<Activity mode="visible">
|
||||
<Text text="Parent" />
|
||||
<Portal>
|
||||
<Child />
|
||||
</Portal>
|
||||
</Activity>,
|
||||
);
|
||||
});
|
||||
|
||||
assertLog(['Parent', 'Child']);
|
||||
expect(container.innerHTML).toBe(
|
||||
'<span prop="Parent" style=""></span><div style=""></div>',
|
||||
);
|
||||
expect(portalContainer.innerHTML).toBe(
|
||||
'<span prop="Child" style=""></span>',
|
||||
);
|
||||
});
|
||||
|
||||
// @gate enableActivity
|
||||
it('hides new insertions inside an already hidden portal', async () => {
|
||||
function Child({text}) {
|
||||
useLayoutEffect(() => {
|
||||
Scheduler.log(`Mount layout ${text}`);
|
||||
return () => {
|
||||
Scheduler.log(`Unmount layout ${text}`);
|
||||
};
|
||||
}, [text]);
|
||||
return <Text text={text} />;
|
||||
}
|
||||
|
||||
const portalContainer = document.createElement('div');
|
||||
|
||||
function Portal({children}) {
|
||||
return <div>{ReactDOM.createPortal(children, portalContainer)}</div>;
|
||||
}
|
||||
|
||||
const root = ReactDOMClient.createRoot(container);
|
||||
// Mount hidden tree.
|
||||
await act(() => {
|
||||
root.render(
|
||||
<Activity mode="hidden">
|
||||
<Portal>
|
||||
<Child text="A" />
|
||||
</Portal>
|
||||
</Activity>,
|
||||
);
|
||||
});
|
||||
assertLog(['A']);
|
||||
expect(container.innerHTML).toBe('<div style="display: none;"></div>');
|
||||
expect(portalContainer.innerHTML).toBe(
|
||||
'<span prop="A" style="display: none;"></span>',
|
||||
);
|
||||
|
||||
// Add a node inside the hidden portal.
|
||||
await act(() => {
|
||||
root.render(
|
||||
<Activity mode="hidden">
|
||||
<Portal>
|
||||
<Child text="A" />
|
||||
<Child text="B" />
|
||||
</Portal>
|
||||
</Activity>,
|
||||
);
|
||||
});
|
||||
assertLog(['A', 'B']);
|
||||
expect(container.innerHTML).toBe('<div style="display: none;"></div>');
|
||||
expect(portalContainer.innerHTML).toBe(
|
||||
'<span prop="A" style="display: none;"></span><span prop="B" style="display: none;"></span>',
|
||||
);
|
||||
|
||||
// Now reveal it.
|
||||
await act(() => {
|
||||
root.render(
|
||||
<Activity mode="visible">
|
||||
<Portal>
|
||||
<Child text="A" />
|
||||
<Child text="B" />
|
||||
</Portal>
|
||||
</Activity>,
|
||||
);
|
||||
});
|
||||
|
||||
assertLog(['A', 'B', 'Mount layout A', 'Mount layout B']);
|
||||
expect(container.innerHTML).toBe('<div style=""></div>');
|
||||
expect(portalContainer.innerHTML).toBe(
|
||||
'<span prop="A" style=""></span><span prop="B" style=""></span>',
|
||||
);
|
||||
});
|
||||
|
||||
// @gate enableActivity
|
||||
it('reveal an inner Suspense boundary without revealing an outer Activity on the same host child', async () => {
|
||||
const promise = new Promise(() => {});
|
||||
|
||||
function Child({showInner}) {
|
||||
useLayoutEffect(() => {
|
||||
Scheduler.log('Mount layout');
|
||||
return () => {
|
||||
Scheduler.log('Unmount layout');
|
||||
};
|
||||
}, []);
|
||||
return (
|
||||
<>
|
||||
{showInner ? null : promise}
|
||||
<Text text="Child" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const portalContainer = document.createElement('div');
|
||||
|
||||
function Portal({children}) {
|
||||
return <div>{ReactDOM.createPortal(children, portalContainer)}</div>;
|
||||
}
|
||||
|
||||
const root = ReactDOMClient.createRoot(container);
|
||||
|
||||
// Prerender the whole tree.
|
||||
await act(() => {
|
||||
root.render(
|
||||
<Activity mode="hidden">
|
||||
<Portal>
|
||||
<Suspense name="Inner" fallback={<span>Loading</span>}>
|
||||
<Child showInner={true} />
|
||||
</Suspense>
|
||||
</Portal>
|
||||
</Activity>,
|
||||
);
|
||||
});
|
||||
|
||||
assertLog(['Child']);
|
||||
expect(container.innerHTML).toBe('<div style="display: none;"></div>');
|
||||
expect(portalContainer.innerHTML).toBe(
|
||||
'<span prop="Child" style="display: none;"></span>',
|
||||
);
|
||||
|
||||
// Re-suspend the inner.
|
||||
await act(() => {
|
||||
root.render(
|
||||
<Activity mode="hidden">
|
||||
<Portal>
|
||||
<Suspense name="Inner" fallback={<span>Loading</span>}>
|
||||
<Child showInner={false} />
|
||||
</Suspense>
|
||||
</Portal>
|
||||
</Activity>,
|
||||
);
|
||||
});
|
||||
assertLog([]);
|
||||
expect(container.innerHTML).toBe('<div style="display: none;"></div>');
|
||||
expect(portalContainer.innerHTML).toBe(
|
||||
'<span prop="Child" style="display: none;"></span><span style="display: none;">Loading</span>',
|
||||
);
|
||||
|
||||
// Toggle to visible while suspended.
|
||||
await act(() => {
|
||||
root.render(
|
||||
<Activity mode="visible">
|
||||
<Portal>
|
||||
<Suspense name="Inner" fallback={<span>Loading</span>}>
|
||||
<Child showInner={false} />
|
||||
</Suspense>
|
||||
</Portal>
|
||||
</Activity>,
|
||||
);
|
||||
});
|
||||
assertLog([]);
|
||||
expect(container.innerHTML).toBe('<div style=""></div>');
|
||||
expect(portalContainer.innerHTML).toBe(
|
||||
'<span prop="Child" style="display: none;"></span><span style="">Loading</span>',
|
||||
);
|
||||
|
||||
// Now reveal.
|
||||
await act(() => {
|
||||
root.render(
|
||||
<Activity mode="visible">
|
||||
<Portal>
|
||||
<Suspense name="Inner" fallback={<span>Loading</span>}>
|
||||
<Child showInner={true} />
|
||||
</Suspense>
|
||||
</Portal>
|
||||
</Activity>,
|
||||
);
|
||||
});
|
||||
assertLog(['Child', 'Mount layout']);
|
||||
expect(container.innerHTML).toBe('<div style=""></div>');
|
||||
expect(portalContainer.innerHTML).toBe(
|
||||
'<span prop="Child" style=""></span>',
|
||||
);
|
||||
});
|
||||
|
||||
// @gate enableActivity
|
||||
it('mounts/unmounts layout effects in portal when visibility changes (starting visible)', async () => {
|
||||
function Child() {
|
||||
useLayoutEffect(() => {
|
||||
Scheduler.log('Mount layout');
|
||||
return () => {
|
||||
Scheduler.log('Unmount layout');
|
||||
};
|
||||
}, []);
|
||||
return <Text text="Child" />;
|
||||
}
|
||||
|
||||
const portalContainer = document.createElement('div');
|
||||
|
||||
function Portal({children}) {
|
||||
return <div>{ReactDOM.createPortal(children, portalContainer)}</div>;
|
||||
}
|
||||
|
||||
const root = ReactDOMClient.createRoot(container);
|
||||
// Mount visible tree.
|
||||
await act(() => {
|
||||
root.render(
|
||||
<Activity mode="visible">
|
||||
<Portal>
|
||||
<Child />
|
||||
</Portal>
|
||||
</Activity>,
|
||||
);
|
||||
});
|
||||
assertLog(['Child', 'Mount layout']);
|
||||
expect(container.innerHTML).toBe('<div></div>');
|
||||
expect(portalContainer.innerHTML).toBe('<span prop="Child"></span>');
|
||||
|
||||
// Hide the tree. The layout effect is unmounted.
|
||||
await act(() => {
|
||||
root.render(
|
||||
<Activity mode="hidden">
|
||||
<Portal>
|
||||
<Child />
|
||||
</Portal>
|
||||
</Activity>,
|
||||
);
|
||||
});
|
||||
assertLog(['Unmount layout', 'Child']);
|
||||
expect(container.innerHTML).toBe('<div style="display: none;"></div>');
|
||||
expect(portalContainer.innerHTML).toBe(
|
||||
'<span prop="Child" style="display: none;"></span>',
|
||||
);
|
||||
});
|
||||
|
||||
// @gate enableActivity
|
||||
it('mounts/unmounts layout effects in portal when visibility changes (starting hidden)', async () => {
|
||||
function Child() {
|
||||
useLayoutEffect(() => {
|
||||
Scheduler.log('Mount layout');
|
||||
return () => {
|
||||
Scheduler.log('Unmount layout');
|
||||
};
|
||||
}, []);
|
||||
return <Text text="Child" />;
|
||||
}
|
||||
|
||||
const portalContainer = document.createElement('div');
|
||||
|
||||
function Portal({children}) {
|
||||
return <div>{ReactDOM.createPortal(children, portalContainer)}</div>;
|
||||
}
|
||||
|
||||
const root = ReactDOMClient.createRoot(container);
|
||||
// Mount hidden tree.
|
||||
await act(() => {
|
||||
root.render(
|
||||
<Activity mode="hidden">
|
||||
<Portal>
|
||||
<Child />
|
||||
</Portal>
|
||||
</Activity>,
|
||||
);
|
||||
});
|
||||
// No layout effect.
|
||||
assertLog(['Child']);
|
||||
expect(container.innerHTML).toBe('<div style="display: none;"></div>');
|
||||
expect(portalContainer.innerHTML).toBe(
|
||||
'<span prop="Child" style="display: none;"></span>',
|
||||
);
|
||||
|
||||
// Unhide the tree. The layout effect is mounted.
|
||||
await act(() => {
|
||||
root.render(
|
||||
<Activity mode="visible">
|
||||
<Portal>
|
||||
<Child />
|
||||
</Portal>
|
||||
</Activity>,
|
||||
);
|
||||
});
|
||||
assertLog(['Child', 'Mount layout']);
|
||||
expect(container.innerHTML).toBe('<div style=""></div>');
|
||||
expect(portalContainer.innerHTML).toBe(
|
||||
'<span prop="Child" style=""></span>',
|
||||
);
|
||||
});
|
||||
|
||||
// @gate enableLegacyHidden
|
||||
it('does not toggle effects or hide nodes for LegacyHidden component inside portal', async () => {
|
||||
function Child() {
|
||||
useLayoutEffect(() => {
|
||||
Scheduler.log('Mount layout');
|
||||
return () => {
|
||||
Scheduler.log('Unmount layout');
|
||||
};
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
Scheduler.log('Mount passive');
|
||||
return () => {
|
||||
Scheduler.log('Unmount passive');
|
||||
};
|
||||
}, []);
|
||||
return <Text text="Child" />;
|
||||
}
|
||||
|
||||
const portalContainer = document.createElement('div');
|
||||
|
||||
function Portal({children}) {
|
||||
return <div>{ReactDOM.createPortal(children, portalContainer)}</div>;
|
||||
}
|
||||
|
||||
const root = ReactDOMClient.createRoot(container);
|
||||
// Mount visible tree.
|
||||
await act(() => {
|
||||
root.render(
|
||||
<LegacyHidden mode="visible">
|
||||
<Portal>
|
||||
<Child />
|
||||
</Portal>
|
||||
</LegacyHidden>,
|
||||
);
|
||||
});
|
||||
assertLog(['Child', 'Mount layout', 'Mount passive']);
|
||||
expect(container.innerHTML).toBe('<div></div>');
|
||||
expect(portalContainer.innerHTML).toBe('<span prop="Child"></span>');
|
||||
|
||||
// Hide the tree.
|
||||
await act(() => {
|
||||
root.render(
|
||||
<LegacyHidden mode="hidden">
|
||||
<Portal>
|
||||
<Child />
|
||||
</Portal>
|
||||
</LegacyHidden>,
|
||||
);
|
||||
});
|
||||
// Effects not unmounted.
|
||||
assertLog(['Child']);
|
||||
expect(container.innerHTML).toBe('<div></div>');
|
||||
expect(portalContainer.innerHTML).toBe('<span prop="Child"></span>');
|
||||
|
||||
// Unhide the tree.
|
||||
await act(() => {
|
||||
root.render(
|
||||
<LegacyHidden mode="visible">
|
||||
<Portal>
|
||||
<Child />
|
||||
</Portal>
|
||||
</LegacyHidden>,
|
||||
);
|
||||
});
|
||||
// Effects already mounted.
|
||||
assertLog(['Child']);
|
||||
expect(container.innerHTML).toBe('<div></div>');
|
||||
expect(portalContainer.innerHTML).toBe('<span prop="Child"></span>');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user