Compare commits
60 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd1d0dc2dc | ||
|
|
93fc57400b | ||
|
|
093b3246e1 | ||
|
|
3a495ae722 | ||
|
|
bbe3f4d322 | ||
|
|
1ea46df8ba | ||
|
|
8c15edd57c | ||
|
|
5e94655cbb | ||
|
|
db8273c12f | ||
|
|
04ee54cd12 | ||
|
|
100fc4a8cf | ||
|
|
92ac4e8b80 | ||
|
|
f76c3617e0 | ||
|
|
7296120396 | ||
|
|
6347c6d373 | ||
|
|
01fb328632 | ||
|
|
ce4054ebdd | ||
|
|
21c1d51acb | ||
|
|
be48396dbd | ||
|
|
5268492536 | ||
|
|
c83be7da9f | ||
|
|
6362b5c711 | ||
|
|
5a9921b839 | ||
|
|
717e70843e | ||
|
|
a10ff9c857 | ||
|
|
fa50caf5f8 | ||
|
|
1e986f514f | ||
|
|
38bdda1ca6 | ||
|
|
a44e750e87 | ||
|
|
37b089a59c | ||
|
|
1a31a814f1 | ||
|
|
5a2205ba28 | ||
|
|
fa767dade6 | ||
|
|
0ba2f01f74 | ||
|
|
dd048c3b2d | ||
|
|
c308cb5905 | ||
|
|
986323f8c6 | ||
|
|
8f8b336734 | ||
|
|
d000261eef | ||
|
|
f646e8ffd8 | ||
|
|
edd05f181b | ||
|
|
67f7d47a9b | ||
|
|
561ee24d4a | ||
|
|
488d88b018 | ||
|
|
26cf280480 | ||
|
|
c9ddee7e36 | ||
|
|
6fb7754494 | ||
|
|
3a0ab8a7ee | ||
|
|
0a5fb67ddf | ||
|
|
4f93170066 | ||
|
|
0fa32506da | ||
|
|
fb0d96073c | ||
|
|
b4455a6ee6 | ||
|
|
17b3765244 | ||
|
|
69f3e9d034 | ||
|
|
dd53a946ec | ||
|
|
90817f8810 | ||
|
|
0d721b60c2 | ||
|
|
d3d0ce329e | ||
|
|
ba0590f306 |
@@ -11,7 +11,7 @@ body:
|
||||
options:
|
||||
- label: React Compiler core (the JS output is incorrect, or your app works incorrectly after optimization)
|
||||
- label: babel-plugin-react-compiler (build issue installing or using the Babel plugin)
|
||||
- label: eslint-plugin-react-compiler (build issue installing or using the eslint plugin)
|
||||
- label: eslint-plugin-react-hooks (build issue installing or using the eslint plugin)
|
||||
- label: react-compiler-healthcheck (build issue installing or using the healthcheck script)
|
||||
- type: input
|
||||
attributes:
|
||||
|
||||
@@ -162,10 +162,13 @@ jobs:
|
||||
mv build/facebook-react-native/react-is/cjs/ $BASE_FOLDER/RKJSModules/vendor/react/react-is/
|
||||
mv build/facebook-react-native/react-test-renderer/cjs/ $BASE_FOLDER/RKJSModules/vendor/react/react-test-renderer/
|
||||
|
||||
# Delete OSS renderer. OSS renderer is synced through internal script.
|
||||
# Delete the OSS renderers, these are sync'd to RN separately.
|
||||
RENDERER_FOLDER=$BASE_FOLDER/react-native-github/Libraries/Renderer/implementations/
|
||||
rm $RENDERER_FOLDER/ReactFabric-{dev,prod,profiling}.js
|
||||
rm $RENDERER_FOLDER/ReactNativeRenderer-{dev,prod,profiling}.js
|
||||
|
||||
# Delete the legacy renderer shim, this is not sync'd and will get deleted in the future.
|
||||
SHIM_FOLDER=$BASE_FOLDER/react-native-github/Libraries/Renderer/shims/
|
||||
rm $SHIM_FOLDER/ReactNative.js
|
||||
|
||||
# Copy eslint-plugin-react-hooks
|
||||
# NOTE: This is different from www, here we include the full package
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
"@babel/types": "7.26.3",
|
||||
"@heroicons/react": "^1.0.6",
|
||||
"@monaco-editor/react": "^4.8.0-rc.2",
|
||||
"@playwright/test": "^1.51.1",
|
||||
"@playwright/test": "^1.56.1",
|
||||
"@use-gesture/react": "^10.2.22",
|
||||
"hermes-eslint": "^0.25.0",
|
||||
"hermes-parser": "^0.25.0",
|
||||
|
||||
@@ -798,12 +798,12 @@
|
||||
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
|
||||
integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==
|
||||
|
||||
"@playwright/test@^1.51.1":
|
||||
version "1.51.1"
|
||||
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.51.1.tgz#75357d513221a7be0baad75f01e966baf9c41a2e"
|
||||
integrity sha512-nM+kEaTSAoVlXmMPH10017vn3FSiFqr/bh4fKg9vmAdMfd9SDqRZNvPSiAHADc/itWak+qPvMPZQOPwCBW7k7Q==
|
||||
"@playwright/test@^1.56.1":
|
||||
version "1.56.1"
|
||||
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.56.1.tgz#6e3bf3d0c90c5cf94bf64bdb56fd15a805c8bd3f"
|
||||
integrity sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==
|
||||
dependencies:
|
||||
playwright "1.51.1"
|
||||
playwright "1.56.1"
|
||||
|
||||
"@rtsao/scc@^1.1.0":
|
||||
version "1.1.0"
|
||||
@@ -854,23 +854,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.9.tgz#02d013de7058cea16d36168ef2fc653464cfbad4"
|
||||
integrity sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==
|
||||
|
||||
"@types/react-dom@19.1.9":
|
||||
version "19.1.9"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-19.1.9.tgz#5ab695fce1e804184767932365ae6569c11b4b4b"
|
||||
integrity sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==
|
||||
|
||||
"@types/react-dom@19.2":
|
||||
version "19.2.2"
|
||||
resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.2.tgz#a4cc874797b7ddc9cb180ef0d5dc23f596fc2332"
|
||||
integrity sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==
|
||||
|
||||
"@types/react@19.1.12":
|
||||
version "19.1.12"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-19.1.12.tgz#7bfaa76aabbb0b4fe0493c21a3a7a93d33e8937b"
|
||||
integrity sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w==
|
||||
dependencies:
|
||||
csstype "^3.0.2"
|
||||
|
||||
"@types/react@19.2":
|
||||
version "19.2.2"
|
||||
resolved "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz#ba123a75d4c2a51158697160a4ea2ff70aa6bf36"
|
||||
@@ -3465,17 +3453,17 @@ pirates@^4.0.1:
|
||||
resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.6.tgz#3018ae32ecfcff6c29ba2267cbf21166ac1f36b9"
|
||||
integrity sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==
|
||||
|
||||
playwright-core@1.51.1:
|
||||
version "1.51.1"
|
||||
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.51.1.tgz#d57f0393e02416f32a47cf82b27533656a8acce1"
|
||||
integrity sha512-/crRMj8+j/Nq5s8QcvegseuyeZPxpQCZb6HNk3Sos3BlZyAknRjoyJPFWkpNn8v0+P3WiwqFF8P+zQo4eqiNuw==
|
||||
playwright-core@1.56.1:
|
||||
version "1.56.1"
|
||||
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.56.1.tgz#24a66481e5cd33a045632230aa2c4f0cb6b1db3d"
|
||||
integrity sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==
|
||||
|
||||
playwright@1.51.1:
|
||||
version "1.51.1"
|
||||
resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.51.1.tgz#ae1467ee318083968ad28d6990db59f47a55390f"
|
||||
integrity sha512-kkx+MB2KQRkyxjYPc3a0wLZZoDczmppyGJIvQ43l+aZihkaVvmu/21kiyaHeHjiFxjxNNFnUncKmcGIyOojsaw==
|
||||
playwright@1.56.1:
|
||||
version "1.56.1"
|
||||
resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.56.1.tgz#62e3b99ddebed0d475e5936a152c88e68be55fbf"
|
||||
integrity sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==
|
||||
dependencies:
|
||||
playwright-core "1.51.1"
|
||||
playwright-core "1.56.1"
|
||||
optionalDependencies:
|
||||
fsevents "2.3.2"
|
||||
|
||||
|
||||
@@ -105,6 +105,7 @@ import {inferMutationAliasingRanges} from '../Inference/InferMutationAliasingRan
|
||||
import {validateNoDerivedComputationsInEffects} from '../Validation/ValidateNoDerivedComputationsInEffects';
|
||||
import {validateNoDerivedComputationsInEffects_exp} from '../Validation/ValidateNoDerivedComputationsInEffects_exp';
|
||||
import {nameAnonymousFunctions} from '../Transform/NameAnonymousFunctions';
|
||||
import {validateSourceLocations} from '../Validation/ValidateSourceLocations';
|
||||
|
||||
export type CompilerPipelineValue =
|
||||
| {kind: 'ast'; name: string; value: CodegenFunction}
|
||||
@@ -272,12 +273,10 @@ function runWithEnvironment(
|
||||
validateNoSetStateInRender(hir).unwrap();
|
||||
}
|
||||
|
||||
if (env.config.validateNoDerivedComputationsInEffects) {
|
||||
validateNoDerivedComputationsInEffects(hir);
|
||||
}
|
||||
|
||||
if (env.config.validateNoDerivedComputationsInEffects_exp) {
|
||||
validateNoDerivedComputationsInEffects_exp(hir);
|
||||
env.logErrors(validateNoDerivedComputationsInEffects_exp(hir));
|
||||
} else if (env.config.validateNoDerivedComputationsInEffects) {
|
||||
validateNoDerivedComputationsInEffects(hir);
|
||||
}
|
||||
|
||||
if (env.config.validateNoSetStateInEffects) {
|
||||
@@ -559,6 +558,10 @@ function runWithEnvironment(
|
||||
log({kind: 'ast', name: 'Codegen (outlined)', value: outlined.fn});
|
||||
}
|
||||
|
||||
if (env.config.validateSourceLocations) {
|
||||
validateSourceLocations(func, ast).unwrap();
|
||||
}
|
||||
|
||||
/**
|
||||
* This flag should be only set for unit / fixture tests to check
|
||||
* that Forget correctly handles unexpected errors (e.g. exceptions
|
||||
|
||||
@@ -364,6 +364,13 @@ export const EnvironmentConfigSchema = z.object({
|
||||
validateNoCapitalizedCalls: z.nullable(z.array(z.string())).default(null),
|
||||
validateBlocklistedImports: z.nullable(z.array(z.string())).default(null),
|
||||
|
||||
/**
|
||||
* Validates that AST nodes generated during codegen have proper source locations.
|
||||
* This is useful for debugging issues with source maps and Istanbul coverage.
|
||||
* When enabled, the compiler will error if important source locations are missing in the generated AST.
|
||||
*/
|
||||
validateSourceLocations: z.boolean().default(false),
|
||||
|
||||
/**
|
||||
* Validate against impure functions called during render
|
||||
*/
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {Result} from '../Utils/Result';
|
||||
import {CompilerDiagnostic, CompilerError, Effect} from '..';
|
||||
import {ErrorCategory} from '../CompilerError';
|
||||
import {
|
||||
@@ -20,8 +21,8 @@ import {
|
||||
isUseStateType,
|
||||
BasicBlock,
|
||||
isUseRefType,
|
||||
GeneratedSource,
|
||||
SourceLocation,
|
||||
ArrayExpression,
|
||||
} from '../HIR';
|
||||
import {eachInstructionLValue, eachInstructionOperand} from '../HIR/visitors';
|
||||
import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables';
|
||||
@@ -33,15 +34,22 @@ type DerivationMetadata = {
|
||||
typeOfValue: TypeOfValue;
|
||||
place: Place;
|
||||
sourcesIds: Set<IdentifierId>;
|
||||
isStateSource: boolean;
|
||||
};
|
||||
|
||||
type EffectMetadata = {
|
||||
effect: HIRFunction;
|
||||
dependencies: ArrayExpression;
|
||||
};
|
||||
|
||||
type ValidationContext = {
|
||||
readonly functions: Map<IdentifierId, FunctionExpression>;
|
||||
readonly candidateDependencies: Map<IdentifierId, ArrayExpression>;
|
||||
readonly errors: CompilerError;
|
||||
readonly derivationCache: DerivationCache;
|
||||
readonly effects: Set<HIRFunction>;
|
||||
readonly setStateCache: Map<string | undefined | null, Array<Place>>;
|
||||
readonly effectSetStateCache: Map<string | undefined | null, Array<Place>>;
|
||||
readonly effectsCache: Map<IdentifierId, EffectMetadata>;
|
||||
readonly setStateLoads: Map<IdentifierId, IdentifierId | null>;
|
||||
readonly setStateUsages: Map<IdentifierId, Set<SourceLocation>>;
|
||||
};
|
||||
|
||||
class DerivationCache {
|
||||
@@ -56,6 +64,7 @@ class DerivationCache {
|
||||
place: value.place,
|
||||
sourcesIds: new Set(value.sourcesIds),
|
||||
typeOfValue: value.typeOfValue,
|
||||
isStateSource: value.isStateSource,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -95,41 +104,28 @@ class DerivationCache {
|
||||
derivedVar: Place,
|
||||
sourcesIds: Set<IdentifierId>,
|
||||
typeOfValue: TypeOfValue,
|
||||
isStateSource: boolean,
|
||||
): void {
|
||||
let newValue: DerivationMetadata = {
|
||||
place: derivedVar,
|
||||
sourcesIds: new Set(),
|
||||
typeOfValue: typeOfValue ?? 'ignored',
|
||||
};
|
||||
|
||||
if (sourcesIds !== undefined) {
|
||||
for (const id of sourcesIds) {
|
||||
const sourcePlace = this.cache.get(id)?.place;
|
||||
|
||||
if (sourcePlace === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/*
|
||||
* If the identifier of the source is a promoted identifier, then
|
||||
* we should set the target as the source.
|
||||
*/
|
||||
let finalIsSource = isStateSource;
|
||||
if (!finalIsSource) {
|
||||
for (const sourceId of sourcesIds) {
|
||||
const sourceMetadata = this.cache.get(sourceId);
|
||||
if (
|
||||
sourcePlace.identifier.name === null ||
|
||||
sourcePlace.identifier.name?.kind === 'promoted'
|
||||
sourceMetadata?.isStateSource &&
|
||||
sourceMetadata.place.identifier.name?.kind !== 'named'
|
||||
) {
|
||||
newValue.sourcesIds.add(derivedVar.identifier.id);
|
||||
} else {
|
||||
newValue.sourcesIds.add(sourcePlace.identifier.id);
|
||||
finalIsSource = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (newValue.sourcesIds.size === 0) {
|
||||
newValue.sourcesIds.add(derivedVar.identifier.id);
|
||||
}
|
||||
|
||||
this.cache.set(derivedVar.identifier.id, newValue);
|
||||
this.cache.set(derivedVar.identifier.id, {
|
||||
place: derivedVar,
|
||||
sourcesIds: sourcesIds,
|
||||
typeOfValue: typeOfValue ?? 'ignored',
|
||||
isStateSource: finalIsSource,
|
||||
});
|
||||
}
|
||||
|
||||
private isDerivationEqual(
|
||||
@@ -151,6 +147,14 @@ class DerivationCache {
|
||||
}
|
||||
}
|
||||
|
||||
function isNamedIdentifier(place: Place): place is Place & {
|
||||
identifier: {name: NonNullable<Place['identifier']['name']>};
|
||||
} {
|
||||
return (
|
||||
place.identifier.name !== null && place.identifier.name.kind === 'named'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that useEffect is not used for derived computations which could/should
|
||||
* be performed in render.
|
||||
@@ -176,25 +180,24 @@ class DerivationCache {
|
||||
*/
|
||||
export function validateNoDerivedComputationsInEffects_exp(
|
||||
fn: HIRFunction,
|
||||
): void {
|
||||
): Result<void, CompilerError> {
|
||||
const functions: Map<IdentifierId, FunctionExpression> = new Map();
|
||||
const candidateDependencies: Map<IdentifierId, ArrayExpression> = new Map();
|
||||
const derivationCache = new DerivationCache();
|
||||
const errors = new CompilerError();
|
||||
const effects: Set<HIRFunction> = new Set();
|
||||
const effectsCache: Map<IdentifierId, EffectMetadata> = new Map();
|
||||
|
||||
const setStateCache: Map<string | undefined | null, Array<Place>> = new Map();
|
||||
const effectSetStateCache: Map<
|
||||
string | undefined | null,
|
||||
Array<Place>
|
||||
> = new Map();
|
||||
const setStateLoads: Map<IdentifierId, IdentifierId> = new Map();
|
||||
const setStateUsages: Map<IdentifierId, Set<SourceLocation>> = new Map();
|
||||
|
||||
const context: ValidationContext = {
|
||||
functions,
|
||||
candidateDependencies,
|
||||
errors,
|
||||
derivationCache,
|
||||
effects,
|
||||
setStateCache,
|
||||
effectSetStateCache,
|
||||
effectsCache,
|
||||
setStateLoads,
|
||||
setStateUsages,
|
||||
};
|
||||
|
||||
if (fn.fnType === 'Hook') {
|
||||
@@ -202,8 +205,9 @@ export function validateNoDerivedComputationsInEffects_exp(
|
||||
if (param.kind === 'Identifier') {
|
||||
context.derivationCache.cache.set(param.identifier.id, {
|
||||
place: param,
|
||||
sourcesIds: new Set([param.identifier.id]),
|
||||
sourcesIds: new Set(),
|
||||
typeOfValue: 'fromProps',
|
||||
isStateSource: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -212,8 +216,9 @@ export function validateNoDerivedComputationsInEffects_exp(
|
||||
if (props != null && props.kind === 'Identifier') {
|
||||
context.derivationCache.cache.set(props.identifier.id, {
|
||||
place: props,
|
||||
sourcesIds: new Set([props.identifier.id]),
|
||||
sourcesIds: new Set(),
|
||||
typeOfValue: 'fromProps',
|
||||
isStateSource: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -233,13 +238,11 @@ export function validateNoDerivedComputationsInEffects_exp(
|
||||
isFirstPass = false;
|
||||
} while (context.derivationCache.snapshot());
|
||||
|
||||
for (const effect of effects) {
|
||||
validateEffect(effect, context);
|
||||
for (const [, effect] of effectsCache) {
|
||||
validateEffect(effect.effect, effect.dependencies, context);
|
||||
}
|
||||
|
||||
if (errors.hasAnyErrors()) {
|
||||
throw errors;
|
||||
}
|
||||
return errors.asResult();
|
||||
}
|
||||
|
||||
function recordPhiDerivations(
|
||||
@@ -267,6 +270,7 @@ function recordPhiDerivations(
|
||||
phi.place,
|
||||
sourcesIds,
|
||||
typeOfValue,
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -282,17 +286,69 @@ function joinValue(
|
||||
return 'fromPropsAndState';
|
||||
}
|
||||
|
||||
function getRootSetState(
|
||||
key: IdentifierId,
|
||||
loads: Map<IdentifierId, IdentifierId | null>,
|
||||
visited: Set<IdentifierId> = new Set(),
|
||||
): IdentifierId | null {
|
||||
if (visited.has(key)) {
|
||||
return null;
|
||||
}
|
||||
visited.add(key);
|
||||
|
||||
const parentId = loads.get(key);
|
||||
|
||||
if (parentId === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (parentId === null) {
|
||||
return key;
|
||||
}
|
||||
|
||||
return getRootSetState(parentId, loads, visited);
|
||||
}
|
||||
|
||||
function maybeRecordSetState(
|
||||
instr: Instruction,
|
||||
loads: Map<IdentifierId, IdentifierId | null>,
|
||||
usages: Map<IdentifierId, Set<SourceLocation>>,
|
||||
): void {
|
||||
for (const operand of eachInstructionLValue(instr)) {
|
||||
if (
|
||||
instr.value.kind === 'LoadLocal' &&
|
||||
loads.has(instr.value.place.identifier.id)
|
||||
) {
|
||||
loads.set(operand.identifier.id, instr.value.place.identifier.id);
|
||||
} else {
|
||||
if (isSetStateType(operand.identifier)) {
|
||||
// this is a root setState
|
||||
loads.set(operand.identifier.id, null);
|
||||
}
|
||||
}
|
||||
|
||||
const rootSetState = getRootSetState(operand.identifier.id, loads);
|
||||
if (rootSetState !== null && usages.get(rootSetState) === undefined) {
|
||||
usages.set(rootSetState, new Set([operand.loc]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function recordInstructionDerivations(
|
||||
instr: Instruction,
|
||||
context: ValidationContext,
|
||||
isFirstPass: boolean,
|
||||
): void {
|
||||
maybeRecordSetState(instr, context.setStateLoads, context.setStateUsages);
|
||||
|
||||
let typeOfValue: TypeOfValue = 'ignored';
|
||||
let isSource: boolean = false;
|
||||
const sources: Set<IdentifierId> = new Set();
|
||||
const {lvalue, value} = instr;
|
||||
if (value.kind === 'FunctionExpression') {
|
||||
context.functions.set(lvalue.identifier.id, value);
|
||||
for (const [, block] of value.loweredFunc.func.body.blocks) {
|
||||
recordPhiDerivations(block, context);
|
||||
for (const instr of block.instructions) {
|
||||
recordInstructionDerivations(instr, context, isFirstPass);
|
||||
}
|
||||
@@ -307,28 +363,37 @@ function recordInstructionDerivations(
|
||||
value.args[1].kind === 'Identifier'
|
||||
) {
|
||||
const effectFunction = context.functions.get(value.args[0].identifier.id);
|
||||
if (effectFunction != null) {
|
||||
context.effects.add(effectFunction.loweredFunc.func);
|
||||
const deps = context.candidateDependencies.get(
|
||||
value.args[1].identifier.id,
|
||||
);
|
||||
if (effectFunction != null && deps != null) {
|
||||
context.effectsCache.set(value.args[0].identifier.id, {
|
||||
effect: effectFunction.loweredFunc.func,
|
||||
dependencies: deps,
|
||||
});
|
||||
}
|
||||
} else if (isUseStateType(lvalue.identifier) && value.args.length > 0) {
|
||||
const stateValueSource = value.args[0];
|
||||
if (stateValueSource.kind === 'Identifier') {
|
||||
sources.add(stateValueSource.identifier.id);
|
||||
}
|
||||
typeOfValue = joinValue(typeOfValue, 'fromState');
|
||||
typeOfValue = 'fromState';
|
||||
context.derivationCache.addDerivationEntry(
|
||||
lvalue,
|
||||
new Set(),
|
||||
typeOfValue,
|
||||
true,
|
||||
);
|
||||
return;
|
||||
}
|
||||
} else if (value.kind === 'ArrayExpression') {
|
||||
context.candidateDependencies.set(lvalue.identifier.id, value);
|
||||
}
|
||||
|
||||
for (const operand of eachInstructionOperand(instr)) {
|
||||
if (
|
||||
isSetStateType(operand.identifier) &&
|
||||
operand.loc !== GeneratedSource &&
|
||||
isFirstPass
|
||||
) {
|
||||
if (context.setStateCache.has(operand.loc.identifierName)) {
|
||||
context.setStateCache.get(operand.loc.identifierName)!.push(operand);
|
||||
} else {
|
||||
context.setStateCache.set(operand.loc.identifierName, [operand]);
|
||||
if (context.setStateLoads.has(operand.identifier.id)) {
|
||||
const rootSetStateId = getRootSetState(
|
||||
operand.identifier.id,
|
||||
context.setStateLoads,
|
||||
);
|
||||
if (rootSetStateId !== null) {
|
||||
context.setStateUsages.get(rootSetStateId)?.add(operand.loc);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -341,9 +406,7 @@ function recordInstructionDerivations(
|
||||
}
|
||||
|
||||
typeOfValue = joinValue(typeOfValue, operandMetadata.typeOfValue);
|
||||
for (const id of operandMetadata.sourcesIds) {
|
||||
sources.add(id);
|
||||
}
|
||||
sources.add(operand.identifier.id);
|
||||
}
|
||||
|
||||
if (typeOfValue === 'ignored') {
|
||||
@@ -351,7 +414,12 @@ function recordInstructionDerivations(
|
||||
}
|
||||
|
||||
for (const lvalue of eachInstructionLValue(instr)) {
|
||||
context.derivationCache.addDerivationEntry(lvalue, sources, typeOfValue);
|
||||
context.derivationCache.addDerivationEntry(
|
||||
lvalue,
|
||||
sources,
|
||||
typeOfValue,
|
||||
isSource,
|
||||
);
|
||||
}
|
||||
|
||||
for (const operand of eachInstructionOperand(instr)) {
|
||||
@@ -378,6 +446,7 @@ function recordInstructionDerivations(
|
||||
operand,
|
||||
sources,
|
||||
typeOfValue,
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -411,21 +480,183 @@ function recordInstructionDerivations(
|
||||
}
|
||||
}
|
||||
|
||||
type TreeNode = {
|
||||
name: string;
|
||||
typeOfValue: TypeOfValue;
|
||||
isSource: boolean;
|
||||
children: Array<TreeNode>;
|
||||
};
|
||||
|
||||
function buildTreeNode(
|
||||
sourceId: IdentifierId,
|
||||
context: ValidationContext,
|
||||
visited: Set<string> = new Set(),
|
||||
): Array<TreeNode> {
|
||||
const sourceMetadata = context.derivationCache.cache.get(sourceId);
|
||||
if (!sourceMetadata) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (sourceMetadata.isStateSource && isNamedIdentifier(sourceMetadata.place)) {
|
||||
return [
|
||||
{
|
||||
name: sourceMetadata.place.identifier.name.value,
|
||||
typeOfValue: sourceMetadata.typeOfValue,
|
||||
isSource: sourceMetadata.isStateSource,
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const children: Array<TreeNode> = [];
|
||||
|
||||
const namedSiblings: Set<string> = new Set();
|
||||
for (const childId of sourceMetadata.sourcesIds) {
|
||||
const childNodes = buildTreeNode(
|
||||
childId,
|
||||
context,
|
||||
new Set([
|
||||
...visited,
|
||||
...(isNamedIdentifier(sourceMetadata.place)
|
||||
? [sourceMetadata.place.identifier.name.value]
|
||||
: []),
|
||||
]),
|
||||
);
|
||||
if (childNodes) {
|
||||
for (const childNode of childNodes) {
|
||||
if (!namedSiblings.has(childNode.name)) {
|
||||
children.push(childNode);
|
||||
namedSiblings.add(childNode.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
isNamedIdentifier(sourceMetadata.place) &&
|
||||
!visited.has(sourceMetadata.place.identifier.name.value)
|
||||
) {
|
||||
return [
|
||||
{
|
||||
name: sourceMetadata.place.identifier.name.value,
|
||||
typeOfValue: sourceMetadata.typeOfValue,
|
||||
isSource: sourceMetadata.isStateSource,
|
||||
children: children,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
function renderTree(
|
||||
node: TreeNode,
|
||||
indent: string = '',
|
||||
isLast: boolean = true,
|
||||
propsSet: Set<string>,
|
||||
stateSet: Set<string>,
|
||||
): string {
|
||||
const prefix = indent + (isLast ? '└── ' : '├── ');
|
||||
const childIndent = indent + (isLast ? ' ' : '│ ');
|
||||
|
||||
let result = `${prefix}${node.name}`;
|
||||
|
||||
if (node.isSource) {
|
||||
let typeLabel: string;
|
||||
if (node.typeOfValue === 'fromProps') {
|
||||
propsSet.add(node.name);
|
||||
typeLabel = 'Prop';
|
||||
} else if (node.typeOfValue === 'fromState') {
|
||||
stateSet.add(node.name);
|
||||
typeLabel = 'State';
|
||||
} else {
|
||||
propsSet.add(node.name);
|
||||
stateSet.add(node.name);
|
||||
typeLabel = 'Prop and State';
|
||||
}
|
||||
result += ` (${typeLabel})`;
|
||||
}
|
||||
|
||||
if (node.children.length > 0) {
|
||||
result += '\n';
|
||||
node.children.forEach((child, index) => {
|
||||
const isLastChild = index === node.children.length - 1;
|
||||
result += renderTree(child, childIndent, isLastChild, propsSet, stateSet);
|
||||
if (index < node.children.length - 1) {
|
||||
result += '\n';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function getFnLocalDeps(
|
||||
fn: FunctionExpression | undefined,
|
||||
): Set<IdentifierId> | undefined {
|
||||
if (!fn) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const deps: Set<IdentifierId> = new Set();
|
||||
|
||||
for (const [, block] of fn.loweredFunc.func.body.blocks) {
|
||||
for (const instr of block.instructions) {
|
||||
if (instr.value.kind === 'LoadLocal') {
|
||||
deps.add(instr.value.place.identifier.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return deps;
|
||||
}
|
||||
|
||||
function validateEffect(
|
||||
effectFunction: HIRFunction,
|
||||
dependencies: ArrayExpression,
|
||||
context: ValidationContext,
|
||||
): void {
|
||||
const seenBlocks: Set<BlockId> = new Set();
|
||||
|
||||
const effectDerivedSetStateCalls: Array<{
|
||||
value: CallExpression;
|
||||
loc: SourceLocation;
|
||||
id: IdentifierId;
|
||||
sourceIds: Set<IdentifierId>;
|
||||
typeOfValue: TypeOfValue;
|
||||
}> = [];
|
||||
|
||||
const effectSetStateUsages: Map<
|
||||
IdentifierId,
|
||||
Set<SourceLocation>
|
||||
> = new Map();
|
||||
|
||||
// Consider setStates in the effect's dependency array as being part of effectSetStateUsages
|
||||
for (const dep of dependencies.elements) {
|
||||
if (dep.kind === 'Identifier') {
|
||||
const root = getRootSetState(dep.identifier.id, context.setStateLoads);
|
||||
if (root !== null) {
|
||||
effectSetStateUsages.set(root, new Set([dep.loc]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let cleanUpFunctionDeps: Set<IdentifierId> | undefined;
|
||||
|
||||
const globals: Set<IdentifierId> = new Set();
|
||||
for (const block of effectFunction.body.blocks.values()) {
|
||||
/*
|
||||
* if the block is in an effect and is of type return then its an effect's cleanup function
|
||||
* if the cleanup function depends on a value from which effect-set state is derived then
|
||||
* we can't validate
|
||||
*/
|
||||
if (
|
||||
block.terminal.kind === 'return' &&
|
||||
block.terminal.returnVariant === 'Explicit'
|
||||
) {
|
||||
cleanUpFunctionDeps = getFnLocalDeps(
|
||||
context.functions.get(block.terminal.value.identifier.id),
|
||||
);
|
||||
}
|
||||
for (const pred of block.preds) {
|
||||
if (!seenBlocks.has(pred)) {
|
||||
// skip if block has a back edge
|
||||
@@ -439,19 +670,16 @@ function validateEffect(
|
||||
return;
|
||||
}
|
||||
|
||||
maybeRecordSetState(instr, context.setStateLoads, effectSetStateUsages);
|
||||
|
||||
for (const operand of eachInstructionOperand(instr)) {
|
||||
if (
|
||||
isSetStateType(operand.identifier) &&
|
||||
operand.loc !== GeneratedSource
|
||||
) {
|
||||
if (context.effectSetStateCache.has(operand.loc.identifierName)) {
|
||||
context.effectSetStateCache
|
||||
.get(operand.loc.identifierName)!
|
||||
.push(operand);
|
||||
} else {
|
||||
context.effectSetStateCache.set(operand.loc.identifierName, [
|
||||
operand,
|
||||
]);
|
||||
if (context.setStateLoads.has(operand.identifier.id)) {
|
||||
const rootSetStateId = getRootSetState(
|
||||
operand.identifier.id,
|
||||
context.setStateLoads,
|
||||
);
|
||||
if (rootSetStateId !== null) {
|
||||
effectSetStateUsages.get(rootSetStateId)?.add(operand.loc);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -469,7 +697,7 @@ function validateEffect(
|
||||
if (argMetadata !== undefined) {
|
||||
effectDerivedSetStateCalls.push({
|
||||
value: instr.value,
|
||||
loc: instr.value.callee.loc,
|
||||
id: instr.value.callee.identifier.id,
|
||||
sourceIds: argMetadata.sourcesIds,
|
||||
typeOfValue: argMetadata.typeOfValue,
|
||||
});
|
||||
@@ -503,37 +731,74 @@ function validateEffect(
|
||||
}
|
||||
|
||||
for (const derivedSetStateCall of effectDerivedSetStateCalls) {
|
||||
const rootSetStateCall = getRootSetState(
|
||||
derivedSetStateCall.id,
|
||||
context.setStateLoads,
|
||||
);
|
||||
|
||||
if (
|
||||
derivedSetStateCall.loc !== GeneratedSource &&
|
||||
context.effectSetStateCache.has(derivedSetStateCall.loc.identifierName) &&
|
||||
context.setStateCache.has(derivedSetStateCall.loc.identifierName) &&
|
||||
context.effectSetStateCache.get(derivedSetStateCall.loc.identifierName)!
|
||||
.length ===
|
||||
context.setStateCache.get(derivedSetStateCall.loc.identifierName)!
|
||||
.length -
|
||||
1
|
||||
rootSetStateCall !== null &&
|
||||
effectSetStateUsages.has(rootSetStateCall) &&
|
||||
context.setStateUsages.has(rootSetStateCall) &&
|
||||
effectSetStateUsages.get(rootSetStateCall)!.size ===
|
||||
context.setStateUsages.get(rootSetStateCall)!.size - 1
|
||||
) {
|
||||
const derivedDepsStr = Array.from(derivedSetStateCall.sourceIds)
|
||||
.map(sourceId => {
|
||||
const sourceMetadata = context.derivationCache.cache.get(sourceId);
|
||||
return sourceMetadata?.place.identifier.name?.value;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join(', ');
|
||||
const propsSet = new Set<string>();
|
||||
const stateSet = new Set<string>();
|
||||
|
||||
let description;
|
||||
|
||||
if (derivedSetStateCall.typeOfValue === 'fromProps') {
|
||||
description = `From props: [${derivedDepsStr}]`;
|
||||
} else if (derivedSetStateCall.typeOfValue === 'fromState') {
|
||||
description = `From local state: [${derivedDepsStr}]`;
|
||||
} else {
|
||||
description = `From props and local state: [${derivedDepsStr}]`;
|
||||
const rootNodesMap = new Map<string, TreeNode>();
|
||||
for (const id of derivedSetStateCall.sourceIds) {
|
||||
const nodes = buildTreeNode(id, context);
|
||||
for (const node of nodes) {
|
||||
if (!rootNodesMap.has(node.name)) {
|
||||
rootNodesMap.set(node.name, node);
|
||||
}
|
||||
}
|
||||
}
|
||||
const rootNodes = Array.from(rootNodesMap.values());
|
||||
|
||||
const trees = rootNodes.map((node, index) =>
|
||||
renderTree(
|
||||
node,
|
||||
'',
|
||||
index === rootNodes.length - 1,
|
||||
propsSet,
|
||||
stateSet,
|
||||
),
|
||||
);
|
||||
|
||||
for (const dep of derivedSetStateCall.sourceIds) {
|
||||
if (cleanUpFunctionDeps !== undefined && cleanUpFunctionDeps.has(dep)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const propsArr = Array.from(propsSet);
|
||||
const stateArr = Array.from(stateSet);
|
||||
|
||||
let rootSources = '';
|
||||
if (propsArr.length > 0) {
|
||||
rootSources += `Props: [${propsArr.join(', ')}]`;
|
||||
}
|
||||
if (stateArr.length > 0) {
|
||||
if (rootSources) rootSources += '\n';
|
||||
rootSources += `State: [${stateArr.join(', ')}]`;
|
||||
}
|
||||
|
||||
const description = `Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user
|
||||
|
||||
This setState call is setting a derived value that depends on the following reactive sources:
|
||||
|
||||
${rootSources}
|
||||
|
||||
Data Flow Tree:
|
||||
${trees.join('\n')}
|
||||
|
||||
See: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state`;
|
||||
|
||||
context.errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
description: `Derived values (${description}) should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user`,
|
||||
description: description,
|
||||
category: ErrorCategory.EffectDerivationsOfState,
|
||||
reason:
|
||||
'You might not need an effect. Derive values in render, not effects.',
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {NodePath} from '@babel/traverse';
|
||||
import * as t from '@babel/types';
|
||||
import {CompilerDiagnostic, CompilerError, ErrorCategory} from '..';
|
||||
import {CodegenFunction} from '../ReactiveScopes';
|
||||
import {Result} from '../Utils/Result';
|
||||
|
||||
/**
|
||||
* IMPORTANT: This validation is only intended for use in unit tests.
|
||||
* It is not intended for use in production.
|
||||
*
|
||||
* This validation is used to ensure that the generated AST has proper source locations
|
||||
* for "important" original nodes.
|
||||
*
|
||||
* There's one big gotcha with this validation: it only works if the "important" original nodes
|
||||
* are not optimized away by the compiler.
|
||||
*
|
||||
* When that scenario happens, we should just update the fixture to not include a node that has no
|
||||
* corresponding node in the generated AST due to being completely removed during compilation.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Some common node types that are important for coverage tracking.
|
||||
* Based on istanbul-lib-instrument
|
||||
*/
|
||||
const IMPORTANT_INSTRUMENTED_TYPES = new Set([
|
||||
'ArrowFunctionExpression',
|
||||
'AssignmentPattern',
|
||||
'ObjectMethod',
|
||||
'ExpressionStatement',
|
||||
'BreakStatement',
|
||||
'ContinueStatement',
|
||||
'ReturnStatement',
|
||||
'ThrowStatement',
|
||||
'TryStatement',
|
||||
'VariableDeclarator',
|
||||
'IfStatement',
|
||||
'ForStatement',
|
||||
'ForInStatement',
|
||||
'ForOfStatement',
|
||||
'WhileStatement',
|
||||
'DoWhileStatement',
|
||||
'SwitchStatement',
|
||||
'SwitchCase',
|
||||
'WithStatement',
|
||||
'FunctionDeclaration',
|
||||
'FunctionExpression',
|
||||
'LabeledStatement',
|
||||
'ConditionalExpression',
|
||||
'LogicalExpression',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Check if a node is a manual memoization call that the compiler optimizes away.
|
||||
* These include useMemo and useCallback calls, which are intentionally removed
|
||||
* by the DropManualMemoization pass.
|
||||
*/
|
||||
function isManualMemoization(node: t.Node): boolean {
|
||||
// Check if this is a useMemo/useCallback call expression
|
||||
if (t.isCallExpression(node)) {
|
||||
const callee = node.callee;
|
||||
if (t.isIdentifier(callee)) {
|
||||
return callee.name === 'useMemo' || callee.name === 'useCallback';
|
||||
}
|
||||
if (
|
||||
t.isMemberExpression(callee) &&
|
||||
t.isIdentifier(callee.property) &&
|
||||
t.isIdentifier(callee.object)
|
||||
) {
|
||||
return (
|
||||
callee.object.name === 'React' &&
|
||||
(callee.property.name === 'useMemo' ||
|
||||
callee.property.name === 'useCallback')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a location key for comparison. We compare by line/column/source,
|
||||
* not by object identity.
|
||||
*/
|
||||
function locationKey(loc: t.SourceLocation): string {
|
||||
return `${loc.start.line}:${loc.start.column}-${loc.end.line}:${loc.end.column}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that important source locations from the original code are preserved
|
||||
* in the generated AST. This ensures that Istanbul coverage instrumentation can
|
||||
* properly map back to the original source code.
|
||||
*
|
||||
* The validator:
|
||||
* 1. Collects locations from "important" nodes in the original AST (those that
|
||||
* Istanbul instruments for coverage tracking)
|
||||
* 2. Exempts known compiler optimizations (useMemo/useCallback removal)
|
||||
* 3. Verifies that all important locations appear somewhere in the generated AST
|
||||
*
|
||||
* Missing locations can cause Istanbul to fail to track coverage for certain
|
||||
* code paths, leading to inaccurate coverage reports.
|
||||
*/
|
||||
export function validateSourceLocations(
|
||||
func: NodePath<
|
||||
t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
|
||||
>,
|
||||
generatedAst: CodegenFunction,
|
||||
): Result<void, CompilerError> {
|
||||
const errors = new CompilerError();
|
||||
|
||||
// Step 1: Collect important locations from the original source
|
||||
const importantOriginalLocations = new Map<
|
||||
string,
|
||||
{loc: t.SourceLocation; nodeType: string}
|
||||
>();
|
||||
|
||||
func.traverse({
|
||||
enter(path) {
|
||||
const node = path.node;
|
||||
|
||||
// Only track node types that Istanbul instruments
|
||||
if (!IMPORTANT_INSTRUMENTED_TYPES.has(node.type)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip manual memoization that the compiler intentionally removes
|
||||
if (isManualMemoization(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Collect the location if it exists
|
||||
if (node.loc) {
|
||||
const key = locationKey(node.loc);
|
||||
importantOriginalLocations.set(key, {
|
||||
loc: node.loc,
|
||||
nodeType: node.type,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Step 2: Collect all locations from the generated AST
|
||||
const generatedLocations = new Set<string>();
|
||||
|
||||
function collectGeneratedLocations(node: t.Node): void {
|
||||
if (node.loc) {
|
||||
generatedLocations.add(locationKey(node.loc));
|
||||
}
|
||||
|
||||
// Use Babel's VISITOR_KEYS to traverse only actual node properties
|
||||
const keys = t.VISITOR_KEYS[node.type as keyof typeof t.VISITOR_KEYS];
|
||||
|
||||
if (!keys) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const key of keys) {
|
||||
const value = (node as any)[key];
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
for (const item of value) {
|
||||
if (t.isNode(item)) {
|
||||
collectGeneratedLocations(item);
|
||||
}
|
||||
}
|
||||
} else if (t.isNode(value)) {
|
||||
collectGeneratedLocations(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Collect from main function body
|
||||
collectGeneratedLocations(generatedAst.body);
|
||||
|
||||
// Collect from outlined functions
|
||||
for (const outlined of generatedAst.outlined) {
|
||||
collectGeneratedLocations(outlined.fn.body);
|
||||
}
|
||||
|
||||
// Step 3: Validate that all important locations are preserved
|
||||
for (const [key, {loc, nodeType}] of importantOriginalLocations) {
|
||||
if (!generatedLocations.has(key)) {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Todo,
|
||||
reason: 'Important source location missing in generated code',
|
||||
description:
|
||||
`Source location for ${nodeType} is missing in the generated output. This can cause coverage instrumentation ` +
|
||||
`to fail to track this code properly, resulting in inaccurate coverage reports.`,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc,
|
||||
message: null,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return errors.asResult();
|
||||
}
|
||||
@@ -12,4 +12,5 @@ export {validateNoCapitalizedCalls} from './ValidateNoCapitalizedCalls';
|
||||
export {validateNoRefAccessInRender} from './ValidateNoRefAccessInRender';
|
||||
export {validateNoSetStateInRender} from './ValidateNoSetStateInRender';
|
||||
export {validatePreservedManualMemoization} from './ValidatePreservedManualMemoization';
|
||||
export {validateSourceLocations} from './ValidateSourceLocations';
|
||||
export {validateUseMemo} from './ValidateUseMemo';
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({value, enabled}) {
|
||||
const [localValue, setLocalValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (enabled) {
|
||||
setLocalValue(value);
|
||||
} else {
|
||||
setLocalValue('disabled');
|
||||
}
|
||||
}, [value, enabled]);
|
||||
|
||||
return <div>{localValue}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 'test', enabled: true}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(6);
|
||||
const { value, enabled } = t0;
|
||||
const [localValue, setLocalValue] = useState("");
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== enabled || $[1] !== value) {
|
||||
t1 = () => {
|
||||
if (enabled) {
|
||||
setLocalValue(value);
|
||||
} else {
|
||||
setLocalValue("disabled");
|
||||
}
|
||||
};
|
||||
|
||||
t2 = [value, enabled];
|
||||
$[0] = enabled;
|
||||
$[1] = value;
|
||||
$[2] = t1;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
t2 = $[3];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[4] !== localValue) {
|
||||
t3 = <div>{localValue}</div>;
|
||||
$[4] = localValue;
|
||||
$[5] = t3;
|
||||
} else {
|
||||
t3 = $[5];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ value: "test", enabled: true }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```
|
||||
{"kind":"CompileError","detail":{"options":{"description":"Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user\n\nThis setState call is setting a derived value that depends on the following reactive sources:\n\nProps: [value]\n\nData Flow Tree:\n└── value (Prop)\n\nSee: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state","category":"EffectDerivationsOfState","reason":"You might not need an effect. Derive values in render, not effects.","details":[{"kind":"error","loc":{"start":{"line":9,"column":6,"index":244},"end":{"line":9,"column":19,"index":257},"filename":"derived-state-conditionally-in-effect.ts","identifierName":"setLocalValue"},"message":"This should be computed during render, not in an effect"}]}},"fnLoc":null}
|
||||
{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":0,"index":107},"end":{"line":16,"column":1,"index":378},"filename":"derived-state-conditionally-in-effect.ts"},"fnName":"Component","memoSlots":6,"memoBlocks":2,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>test</div>
|
||||
@@ -1,4 +1,4 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({value, enabled}) {
|
||||
@@ -0,0 +1,78 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
export default function Component({input = 'empty'}) {
|
||||
const [currInput, setCurrInput] = useState(input);
|
||||
const localConst = 'local const';
|
||||
|
||||
useEffect(() => {
|
||||
setCurrInput(input + localConst);
|
||||
}, [input, localConst]);
|
||||
|
||||
return <div>{currInput}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{input: 'test'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function Component(t0) {
|
||||
const $ = _c(5);
|
||||
const { input: t1 } = t0;
|
||||
const input = t1 === undefined ? "empty" : t1;
|
||||
const [currInput, setCurrInput] = useState(input);
|
||||
let t2;
|
||||
let t3;
|
||||
if ($[0] !== input) {
|
||||
t2 = () => {
|
||||
setCurrInput(input + "local const");
|
||||
};
|
||||
t3 = [input, "local const"];
|
||||
$[0] = input;
|
||||
$[1] = t2;
|
||||
$[2] = t3;
|
||||
} else {
|
||||
t2 = $[1];
|
||||
t3 = $[2];
|
||||
}
|
||||
useEffect(t2, t3);
|
||||
let t4;
|
||||
if ($[3] !== currInput) {
|
||||
t4 = <div>{currInput}</div>;
|
||||
$[3] = currInput;
|
||||
$[4] = t4;
|
||||
} else {
|
||||
t4 = $[4];
|
||||
}
|
||||
return t4;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ input: "test" }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```
|
||||
{"kind":"CompileError","detail":{"options":{"description":"Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user\n\nThis setState call is setting a derived value that depends on the following reactive sources:\n\nProps: [input]\n\nData Flow Tree:\n└── input (Prop)\n\nSee: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state","category":"EffectDerivationsOfState","reason":"You might not need an effect. Derive values in render, not effects.","details":[{"kind":"error","loc":{"start":{"line":9,"column":4,"index":276},"end":{"line":9,"column":16,"index":288},"filename":"derived-state-from-default-props.ts","identifierName":"setCurrInput"},"message":"This should be computed during render, not in an effect"}]}},"fnLoc":null}
|
||||
{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":15,"index":122},"end":{"line":13,"column":1,"index":372},"filename":"derived-state-from-default-props.ts"},"fnName":"Component","memoSlots":5,"memoBlocks":2,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>testlocal const</div>
|
||||
@@ -1,4 +1,4 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
export default function Component({input = 'empty'}) {
|
||||
@@ -0,0 +1,77 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({shouldChange}) {
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldChange) {
|
||||
setCount(count + 1);
|
||||
}
|
||||
}, [count]);
|
||||
|
||||
return <div>{count}</div>;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(7);
|
||||
const { shouldChange } = t0;
|
||||
const [count, setCount] = useState(0);
|
||||
let t1;
|
||||
if ($[0] !== count || $[1] !== shouldChange) {
|
||||
t1 = () => {
|
||||
if (shouldChange) {
|
||||
setCount(count + 1);
|
||||
}
|
||||
};
|
||||
$[0] = count;
|
||||
$[1] = shouldChange;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
let t2;
|
||||
if ($[3] !== count) {
|
||||
t2 = [count];
|
||||
$[3] = count;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[5] !== count) {
|
||||
t3 = <div>{count}</div>;
|
||||
$[5] = count;
|
||||
$[6] = t3;
|
||||
} else {
|
||||
t3 = $[6];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```
|
||||
{"kind":"CompileError","detail":{"options":{"description":"Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user\n\nThis setState call is setting a derived value that depends on the following reactive sources:\n\nState: [count]\n\nData Flow Tree:\n└── count (State)\n\nSee: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state","category":"EffectDerivationsOfState","reason":"You might not need an effect. Derive values in render, not effects.","details":[{"kind":"error","loc":{"start":{"line":10,"column":6,"index":237},"end":{"line":10,"column":14,"index":245},"filename":"derived-state-from-local-state-in-effect.ts","identifierName":"setCount"},"message":"This should be computed during render, not in an effect"}]}},"fnLoc":null}
|
||||
{"kind":"CompileSuccess","fnLoc":{"start":{"line":5,"column":0,"index":108},"end":{"line":15,"column":1,"index":310},"filename":"derived-state-from-local-state-in-effect.ts"},"fnName":"Component","memoSlots":7,"memoBlocks":3,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -1,4 +1,4 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({firstName}) {
|
||||
const [lastName, setLastName] = useState('Doe');
|
||||
const [fullName, setFullName] = useState('John');
|
||||
|
||||
const middleName = 'D.';
|
||||
|
||||
useEffect(() => {
|
||||
setFullName(firstName + ' ' + middleName + ' ' + lastName);
|
||||
}, [firstName, middleName, lastName]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input value={lastName} onChange={e => setLastName(e.target.value)} />
|
||||
<div>{fullName}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{firstName: 'John'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(12);
|
||||
const { firstName } = t0;
|
||||
const [lastName, setLastName] = useState("Doe");
|
||||
const [fullName, setFullName] = useState("John");
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== firstName || $[1] !== lastName) {
|
||||
t1 = () => {
|
||||
setFullName(firstName + " " + "D." + " " + lastName);
|
||||
};
|
||||
t2 = [firstName, "D.", lastName];
|
||||
$[0] = firstName;
|
||||
$[1] = lastName;
|
||||
$[2] = t1;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
t2 = $[3];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t3 = (e) => setLastName(e.target.value);
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
let t4;
|
||||
if ($[5] !== lastName) {
|
||||
t4 = <input value={lastName} onChange={t3} />;
|
||||
$[5] = lastName;
|
||||
$[6] = t4;
|
||||
} else {
|
||||
t4 = $[6];
|
||||
}
|
||||
let t5;
|
||||
if ($[7] !== fullName) {
|
||||
t5 = <div>{fullName}</div>;
|
||||
$[7] = fullName;
|
||||
$[8] = t5;
|
||||
} else {
|
||||
t5 = $[8];
|
||||
}
|
||||
let t6;
|
||||
if ($[9] !== t4 || $[10] !== t5) {
|
||||
t6 = (
|
||||
<div>
|
||||
{t4}
|
||||
{t5}
|
||||
</div>
|
||||
);
|
||||
$[9] = t4;
|
||||
$[10] = t5;
|
||||
$[11] = t6;
|
||||
} else {
|
||||
t6 = $[11];
|
||||
}
|
||||
return t6;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ firstName: "John" }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```
|
||||
{"kind":"CompileError","detail":{"options":{"description":"Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user\n\nThis setState call is setting a derived value that depends on the following reactive sources:\n\nProps: [firstName]\nState: [lastName]\n\nData Flow Tree:\n├── firstName (Prop)\n└── lastName (State)\n\nSee: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state","category":"EffectDerivationsOfState","reason":"You might not need an effect. Derive values in render, not effects.","details":[{"kind":"error","loc":{"start":{"line":11,"column":4,"index":297},"end":{"line":11,"column":15,"index":308},"filename":"derived-state-from-prop-local-state-and-component-scope.ts","identifierName":"setFullName"},"message":"This should be computed during render, not in an effect"}]}},"fnLoc":null}
|
||||
{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":0,"index":107},"end":{"line":20,"column":1,"index":542},"filename":"derived-state-from-prop-local-state-and-component-scope.ts"},"fnName":"Component","memoSlots":12,"memoBlocks":5,"memoValues":6,"prunedMemoBlocks":0,"prunedMemoValues":0}
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div><input value="Doe"><div>John D. Doe</div></div>
|
||||
@@ -1,4 +1,4 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({firstName}) {
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({initialName}) {
|
||||
@@ -29,7 +29,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
function Component(t0) {
|
||||
@@ -79,6 +79,12 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```
|
||||
{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":0,"index":107},"end":{"line":16,"column":1,"index":359},"filename":"derived-state-from-prop-setter-call-outside-effect-no-error.ts"},"fnName":"Component","memoSlots":6,"memoBlocks":3,"memoValues":4,"prunedMemoBlocks":0,"prunedMemoValues":0}
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div><input value="John"></div>
|
||||
@@ -1,4 +1,4 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({initialName}) {
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
|
||||
function Component({value}) {
|
||||
const [checked, setChecked] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setChecked(value === '' ? [] : value.split(','));
|
||||
}, [value]);
|
||||
|
||||
return <div>{checked}</div>;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(5);
|
||||
const { value } = t0;
|
||||
const [checked, setChecked] = useState("");
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== value) {
|
||||
t1 = () => {
|
||||
setChecked(value === "" ? [] : value.split(","));
|
||||
};
|
||||
t2 = [value];
|
||||
$[0] = value;
|
||||
$[1] = t1;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
t2 = $[2];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[3] !== checked) {
|
||||
t3 = <div>{checked}</div>;
|
||||
$[3] = checked;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -0,0 +1,11 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
|
||||
function Component({value}) {
|
||||
const [checked, setChecked] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setChecked(value === '' ? [] : value.split(','));
|
||||
}, [value]);
|
||||
|
||||
return <div>{checked}</div>;
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function MockComponent({onSet}) {
|
||||
@@ -28,7 +28,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
function MockComponent(t0) {
|
||||
@@ -80,6 +80,13 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```
|
||||
{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":0,"index":107},"end":{"line":6,"column":1,"index":211},"filename":"derived-state-from-prop-setter-used-outside-effect-no-error.ts"},"fnName":"MockComponent","memoSlots":2,"memoBlocks":1,"memoValues":1,"prunedMemoBlocks":0,"prunedMemoValues":0}
|
||||
{"kind":"CompileSuccess","fnLoc":{"start":{"line":8,"column":0,"index":213},"end":{"line":15,"column":1,"index":402},"filename":"derived-state-from-prop-setter-used-outside-effect-no-error.ts"},"fnName":"Component","memoSlots":4,"memoBlocks":2,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>Mock Component</div>
|
||||
@@ -1,4 +1,4 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function MockComponent({onSet}) {
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({value}) {
|
||||
const [localValue, setLocalValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setLocalValue(value);
|
||||
document.title = `Value: ${value}`;
|
||||
}, [value]);
|
||||
|
||||
return <div>{localValue}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 'test'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(5);
|
||||
const { value } = t0;
|
||||
const [localValue, setLocalValue] = useState("");
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== value) {
|
||||
t1 = () => {
|
||||
setLocalValue(value);
|
||||
document.title = `Value: ${value}`;
|
||||
};
|
||||
t2 = [value];
|
||||
$[0] = value;
|
||||
$[1] = t1;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
t2 = $[2];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[3] !== localValue) {
|
||||
t3 = <div>{localValue}</div>;
|
||||
$[3] = localValue;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ value: "test" }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```
|
||||
{"kind":"CompileError","detail":{"options":{"description":"Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user\n\nThis setState call is setting a derived value that depends on the following reactive sources:\n\nProps: [value]\n\nData Flow Tree:\n└── value (Prop)\n\nSee: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state","category":"EffectDerivationsOfState","reason":"You might not need an effect. Derive values in render, not effects.","details":[{"kind":"error","loc":{"start":{"line":8,"column":4,"index":214},"end":{"line":8,"column":17,"index":227},"filename":"derived-state-from-prop-with-side-effect.ts","identifierName":"setLocalValue"},"message":"This should be computed during render, not in an effect"}]}},"fnLoc":null}
|
||||
{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":0,"index":107},"end":{"line":13,"column":1,"index":327},"filename":"derived-state-from-prop-with-side-effect.ts"},"fnName":"Component","memoSlots":5,"memoBlocks":2,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>test</div>
|
||||
@@ -1,4 +1,4 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({value}) {
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import {useEffect, useState, useRef} from 'react';
|
||||
|
||||
export default function Component({test}) {
|
||||
@@ -27,7 +27,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
|
||||
export default function Component(t0) {
|
||||
@@ -68,6 +68,12 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```
|
||||
{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":15,"index":130},"end":{"line":14,"column":1,"index":328},"filename":"derived-state-from-ref-and-state-no-error.ts"},"fnName":"Component","memoSlots":5,"memoBlocks":2,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) nulltestString
|
||||
@@ -1,4 +1,4 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import {useEffect, useState, useRef} from 'react';
|
||||
|
||||
export default function Component({test}) {
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({propValue}) {
|
||||
const [value, setValue] = useState(null);
|
||||
|
||||
function localFunction() {
|
||||
console.log('local function');
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setValue(propValue);
|
||||
localFunction();
|
||||
}, [propValue]);
|
||||
|
||||
return <div>{value}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{propValue: 'test'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(6);
|
||||
const { propValue } = t0;
|
||||
const [value, setValue] = useState(null);
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = function localFunction() {
|
||||
console.log("local function");
|
||||
};
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
}
|
||||
const localFunction = t1;
|
||||
let t2;
|
||||
let t3;
|
||||
if ($[1] !== propValue) {
|
||||
t2 = () => {
|
||||
setValue(propValue);
|
||||
localFunction();
|
||||
};
|
||||
t3 = [propValue];
|
||||
$[1] = propValue;
|
||||
$[2] = t2;
|
||||
$[3] = t3;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
t3 = $[3];
|
||||
}
|
||||
useEffect(t2, t3);
|
||||
let t4;
|
||||
if ($[4] !== value) {
|
||||
t4 = <div>{value}</div>;
|
||||
$[4] = value;
|
||||
$[5] = t4;
|
||||
} else {
|
||||
t4 = $[5];
|
||||
}
|
||||
return t4;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ propValue: "test" }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```
|
||||
{"kind":"CompileError","detail":{"options":{"description":"Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user\n\nThis setState call is setting a derived value that depends on the following reactive sources:\n\nProps: [propValue]\n\nData Flow Tree:\n└── propValue (Prop)\n\nSee: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state","category":"EffectDerivationsOfState","reason":"You might not need an effect. Derive values in render, not effects.","details":[{"kind":"error","loc":{"start":{"line":12,"column":4,"index":279},"end":{"line":12,"column":12,"index":287},"filename":"effect-contains-local-function-call.ts","identifierName":"setValue"},"message":"This should be computed during render, not in an effect"}]}},"fnLoc":null}
|
||||
{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":0,"index":107},"end":{"line":17,"column":1,"index":371},"filename":"effect-contains-local-function-call.ts"},"fnName":"Component","memoSlots":6,"memoBlocks":3,"memoValues":4,"prunedMemoBlocks":0,"prunedMemoValues":0}
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>test</div>
|
||||
logs: ['local function']
|
||||
@@ -1,4 +1,4 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({propValue}) {
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({propValue, onChange}) {
|
||||
@@ -25,7 +25,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
function Component(t0) {
|
||||
@@ -70,6 +70,13 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```
|
||||
{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":0,"index":107},"end":{"line":12,"column":1,"index":306},"filename":"effect-contains-prop-function-call-no-error.ts"},"fnName":"Component","memoSlots":7,"memoBlocks":3,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
|
||||
{"kind":"CompileSuccess","fnLoc":{"start":{"line":16,"column":41,"index":402},"end":{"line":16,"column":49,"index":410},"filename":"effect-contains-prop-function-call-no-error.ts"},"fnName":null,"memoSlots":0,"memoBlocks":0,"memoValues":0,"prunedMemoBlocks":0,"prunedMemoValues":0}
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>test</div>
|
||||
@@ -1,4 +1,4 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({propValue, onChange}) {
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
|
||||
function Component({prop}) {
|
||||
const [s, setS] = useState(0);
|
||||
useEffect(() => {
|
||||
setS(prop);
|
||||
}, [prop, setS]);
|
||||
|
||||
return <div>{prop}</div>;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(5);
|
||||
const { prop } = t0;
|
||||
const [, setS] = useState(0);
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== prop) {
|
||||
t1 = () => {
|
||||
setS(prop);
|
||||
};
|
||||
t2 = [prop, setS];
|
||||
$[0] = prop;
|
||||
$[1] = t1;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
t2 = $[2];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[3] !== prop) {
|
||||
t3 = <div>{prop}</div>;
|
||||
$[3] = prop;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```
|
||||
{"kind":"CompileError","detail":{"options":{"description":"Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user\n\nThis setState call is setting a derived value that depends on the following reactive sources:\n\nProps: [prop]\n\nData Flow Tree:\n└── prop (Prop)\n\nSee: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state","category":"EffectDerivationsOfState","reason":"You might not need an effect. Derive values in render, not effects.","details":[{"kind":"error","loc":{"start":{"line":6,"column":4,"index":150},"end":{"line":6,"column":8,"index":154},"filename":"effect-used-in-dep-array-still-errors.ts","identifierName":"setS"},"message":"This should be computed during render, not in an effect"}]}},"fnLoc":null}
|
||||
{"kind":"CompileSuccess","fnLoc":{"start":{"line":3,"column":0,"index":64},"end":{"line":10,"column":1,"index":212},"filename":"effect-used-in-dep-array-still-errors.ts"},"fnName":"Component","memoSlots":5,"memoBlocks":2,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -0,0 +1,10 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
|
||||
function Component({prop}) {
|
||||
const [s, setS] = useState(0);
|
||||
useEffect(() => {
|
||||
setS(prop);
|
||||
}, [prop, setS]);
|
||||
|
||||
return <div>{prop}</div>;
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component(file: File) {
|
||||
const [imageUrl, setImageUrl] = useState(null);
|
||||
|
||||
/*
|
||||
* Cleaning up the variable or a source of the variable used to setState
|
||||
* inside the effect communicates that we always need to clean up something
|
||||
* which is a valid use case for useEffect. In which case we want to
|
||||
* avoid an throwing
|
||||
*/
|
||||
useEffect(() => {
|
||||
const imageUrlPrepared = URL.createObjectURL(file);
|
||||
setImageUrl(imageUrlPrepared);
|
||||
return () => URL.revokeObjectURL(imageUrlPrepared);
|
||||
}, [file]);
|
||||
|
||||
return <Image src={imageUrl} xstyle={styles.imageSizeLimits} />;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
function Component(file) {
|
||||
const $ = _c(5);
|
||||
const [imageUrl, setImageUrl] = useState(null);
|
||||
let t0;
|
||||
let t1;
|
||||
if ($[0] !== file) {
|
||||
t0 = () => {
|
||||
const imageUrlPrepared = URL.createObjectURL(file);
|
||||
setImageUrl(imageUrlPrepared);
|
||||
return () => URL.revokeObjectURL(imageUrlPrepared);
|
||||
};
|
||||
t1 = [file];
|
||||
$[0] = file;
|
||||
$[1] = t0;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
t1 = $[2];
|
||||
}
|
||||
useEffect(t0, t1);
|
||||
let t2;
|
||||
if ($[3] !== imageUrl) {
|
||||
t2 = <Image src={imageUrl} xstyle={styles.imageSizeLimits} />;
|
||||
$[3] = imageUrl;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```
|
||||
{"kind":"CompileSuccess","fnLoc":{"start":{"line":5,"column":0,"index":108},"end":{"line":21,"column":1,"index":700},"filename":"effect-with-cleanup-function-depending-on-derived-computation-value.ts"},"fnName":"Component","memoSlots":5,"memoBlocks":2,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -0,0 +1,21 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component(file: File) {
|
||||
const [imageUrl, setImageUrl] = useState(null);
|
||||
|
||||
/*
|
||||
* Cleaning up the variable or a source of the variable used to setState
|
||||
* inside the effect communicates that we always need to clean up something
|
||||
* which is a valid use case for useEffect. In which case we want to
|
||||
* avoid an throwing
|
||||
*/
|
||||
useEffect(() => {
|
||||
const imageUrlPrepared = URL.createObjectURL(file);
|
||||
setImageUrl(imageUrlPrepared);
|
||||
return () => URL.revokeObjectURL(imageUrlPrepared);
|
||||
}, [file]);
|
||||
|
||||
return <Image src={imageUrl} xstyle={styles.imageSizeLimits} />;
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({propValue}) {
|
||||
@@ -25,7 +25,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
function Component(t0) {
|
||||
@@ -65,6 +65,12 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```
|
||||
{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":0,"index":107},"end":{"line":12,"column":1,"index":298},"filename":"effect-with-global-function-call-no-error.ts"},"fnName":"Component","memoSlots":5,"memoBlocks":2,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) globalCall is not defined
|
||||
@@ -1,4 +1,4 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({propValue}) {
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({value, enabled}) {
|
||||
const [localValue, setLocalValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (enabled) {
|
||||
setLocalValue(value);
|
||||
} else {
|
||||
setLocalValue('disabled');
|
||||
}
|
||||
}, [value, enabled]);
|
||||
|
||||
return <div>{localValue}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 'test', enabled: true}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: You might not need an effect. Derive values in render, not effects.
|
||||
|
||||
Derived values (From props: [value]) should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
|
||||
|
||||
error.derived-state-conditionally-in-effect.ts:9:6
|
||||
7 | useEffect(() => {
|
||||
8 | if (enabled) {
|
||||
> 9 | setLocalValue(value);
|
||||
| ^^^^^^^^^^^^^ This should be computed during render, not in an effect
|
||||
10 | } else {
|
||||
11 | setLocalValue('disabled');
|
||||
12 | }
|
||||
```
|
||||
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
export default function Component({input = 'empty'}) {
|
||||
const [currInput, setCurrInput] = useState(input);
|
||||
const localConst = 'local const';
|
||||
|
||||
useEffect(() => {
|
||||
setCurrInput(input + localConst);
|
||||
}, [input, localConst]);
|
||||
|
||||
return <div>{currInput}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{input: 'test'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: You might not need an effect. Derive values in render, not effects.
|
||||
|
||||
Derived values (From props: [input]) should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
|
||||
|
||||
error.derived-state-from-default-props.ts:9:4
|
||||
7 |
|
||||
8 | useEffect(() => {
|
||||
> 9 | setCurrInput(input + localConst);
|
||||
| ^^^^^^^^^^^^ This should be computed during render, not in an effect
|
||||
10 | }, [input, localConst]);
|
||||
11 |
|
||||
12 | return <div>{currInput}</div>;
|
||||
```
|
||||
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({shouldChange}) {
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldChange) {
|
||||
setCount(count + 1);
|
||||
}
|
||||
}, [count]);
|
||||
|
||||
return <div>{count}</div>;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: You might not need an effect. Derive values in render, not effects.
|
||||
|
||||
Derived values (From local state: [count]) should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
|
||||
|
||||
error.derived-state-from-local-state-in-effect.ts:10:6
|
||||
8 | useEffect(() => {
|
||||
9 | if (shouldChange) {
|
||||
> 10 | setCount(count + 1);
|
||||
| ^^^^^^^^ This should be computed during render, not in an effect
|
||||
11 | }
|
||||
12 | }, [count]);
|
||||
13 |
|
||||
```
|
||||
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({firstName}) {
|
||||
const [lastName, setLastName] = useState('Doe');
|
||||
const [fullName, setFullName] = useState('John');
|
||||
|
||||
const middleName = 'D.';
|
||||
|
||||
useEffect(() => {
|
||||
setFullName(firstName + ' ' + middleName + ' ' + lastName);
|
||||
}, [firstName, middleName, lastName]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input value={lastName} onChange={e => setLastName(e.target.value)} />
|
||||
<div>{fullName}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{firstName: 'John'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: You might not need an effect. Derive values in render, not effects.
|
||||
|
||||
Derived values (From props and local state: [firstName, lastName]) should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
|
||||
|
||||
error.derived-state-from-prop-local-state-and-component-scope.ts:11:4
|
||||
9 |
|
||||
10 | useEffect(() => {
|
||||
> 11 | setFullName(firstName + ' ' + middleName + ' ' + lastName);
|
||||
| ^^^^^^^^^^^ This should be computed during render, not in an effect
|
||||
12 | }, [firstName, middleName, lastName]);
|
||||
13 |
|
||||
14 | return (
|
||||
```
|
||||
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({value}) {
|
||||
const [localValue, setLocalValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setLocalValue(value);
|
||||
document.title = `Value: ${value}`;
|
||||
}, [value]);
|
||||
|
||||
return <div>{localValue}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 'test'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: You might not need an effect. Derive values in render, not effects.
|
||||
|
||||
Derived values (From props: [value]) should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
|
||||
|
||||
error.derived-state-from-prop-with-side-effect.ts:8:4
|
||||
6 |
|
||||
7 | useEffect(() => {
|
||||
> 8 | setLocalValue(value);
|
||||
| ^^^^^^^^^^^^^ This should be computed during render, not in an effect
|
||||
9 | document.title = `Value: ${value}`;
|
||||
10 | }, [value]);
|
||||
11 |
|
||||
```
|
||||
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({propValue}) {
|
||||
const [value, setValue] = useState(null);
|
||||
|
||||
function localFunction() {
|
||||
console.log('local function');
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setValue(propValue);
|
||||
localFunction();
|
||||
}, [propValue]);
|
||||
|
||||
return <div>{value}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{propValue: 'test'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: You might not need an effect. Derive values in render, not effects.
|
||||
|
||||
Derived values (From props: [propValue]) should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
|
||||
|
||||
error.effect-contains-local-function-call.ts:12:4
|
||||
10 |
|
||||
11 | useEffect(() => {
|
||||
> 12 | setValue(propValue);
|
||||
| ^^^^^^^^ This should be computed during render, not in an effect
|
||||
13 | localFunction();
|
||||
14 | }, [propValue]);
|
||||
15 |
|
||||
```
|
||||
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component() {
|
||||
const [firstName, setFirstName] = useState('Taylor');
|
||||
const lastName = 'Swift';
|
||||
|
||||
// 🔴 Avoid: redundant state and unnecessary Effect
|
||||
const [fullName, setFullName] = useState('');
|
||||
useEffect(() => {
|
||||
setFullName(firstName + ' ' + lastName);
|
||||
}, [firstName, lastName]);
|
||||
|
||||
return <div>{fullName}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: You might not need an effect. Derive values in render, not effects.
|
||||
|
||||
Derived values (From local state: [firstName]) should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
|
||||
|
||||
error.invalid-derived-computation-in-effect.ts:11:4
|
||||
9 | const [fullName, setFullName] = useState('');
|
||||
10 | useEffect(() => {
|
||||
> 11 | setFullName(firstName + ' ' + lastName);
|
||||
| ^^^^^^^^^^^ This should be computed during render, not in an effect
|
||||
12 | }, [firstName, lastName]);
|
||||
13 |
|
||||
14 | return <div>{fullName}</div>;
|
||||
```
|
||||
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
export default function Component(props) {
|
||||
const [displayValue, setDisplayValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const computed = props.prefix + props.value + props.suffix;
|
||||
setDisplayValue(computed);
|
||||
}, [props.prefix, props.value, props.suffix]);
|
||||
|
||||
return <div>{displayValue}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{prefix: '[', value: 'test', suffix: ']'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: You might not need an effect. Derive values in render, not effects.
|
||||
|
||||
Derived values (From props: [props]) should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
|
||||
|
||||
error.invalid-derived-state-from-computed-props.ts:9:4
|
||||
7 | useEffect(() => {
|
||||
8 | const computed = props.prefix + props.value + props.suffix;
|
||||
> 9 | setDisplayValue(computed);
|
||||
| ^^^^^^^^^^^^^^^ This should be computed during render, not in an effect
|
||||
10 | }, [props.prefix, props.value, props.suffix]);
|
||||
11 |
|
||||
12 | return <div>{displayValue}</div>;
|
||||
```
|
||||
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
export default function Component({props}) {
|
||||
const [fullName, setFullName] = useState(
|
||||
props.firstName + ' ' + props.lastName
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setFullName(props.firstName + ' ' + props.lastName);
|
||||
}, [props.firstName, props.lastName]);
|
||||
|
||||
return <div>{fullName}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{props: {firstName: 'John', lastName: 'Doe'}}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: You might not need an effect. Derive values in render, not effects.
|
||||
|
||||
Derived values (From props: [props]) should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
|
||||
|
||||
error.invalid-derived-state-from-destructured-props.ts:10:4
|
||||
8 |
|
||||
9 | useEffect(() => {
|
||||
> 10 | setFullName(props.firstName + ' ' + props.lastName);
|
||||
| ^^^^^^^^^^^ This should be computed during render, not in an effect
|
||||
11 | }, [props.firstName, props.lastName]);
|
||||
12 |
|
||||
13 | return <div>{fullName}</div>;
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component() {
|
||||
const [firstName, setFirstName] = useState('Taylor');
|
||||
const lastName = 'Swift';
|
||||
|
||||
// 🔴 Avoid: redundant state and unnecessary Effect
|
||||
const [fullName, setFullName] = useState('');
|
||||
useEffect(() => {
|
||||
setFullName(firstName + ' ' + lastName);
|
||||
}, [firstName, lastName]);
|
||||
|
||||
return <div>{fullName}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
function Component() {
|
||||
const $ = _c(5);
|
||||
const [firstName] = useState("Taylor");
|
||||
|
||||
const [fullName, setFullName] = useState("");
|
||||
let t0;
|
||||
let t1;
|
||||
if ($[0] !== firstName) {
|
||||
t0 = () => {
|
||||
setFullName(firstName + " " + "Swift");
|
||||
};
|
||||
t1 = [firstName, "Swift"];
|
||||
$[0] = firstName;
|
||||
$[1] = t0;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
t1 = $[2];
|
||||
}
|
||||
useEffect(t0, t1);
|
||||
let t2;
|
||||
if ($[3] !== fullName) {
|
||||
t2 = <div>{fullName}</div>;
|
||||
$[3] = fullName;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```
|
||||
{"kind":"CompileError","detail":{"options":{"description":"Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user\n\nThis setState call is setting a derived value that depends on the following reactive sources:\n\nState: [firstName]\n\nData Flow Tree:\n└── firstName (State)\n\nSee: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state","category":"EffectDerivationsOfState","reason":"You might not need an effect. Derive values in render, not effects.","details":[{"kind":"error","loc":{"start":{"line":11,"column":4,"index":341},"end":{"line":11,"column":15,"index":352},"filename":"invalid-derived-computation-in-effect.ts","identifierName":"setFullName"},"message":"This should be computed during render, not in an effect"}]}},"fnLoc":null}
|
||||
{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":0,"index":107},"end":{"line":15,"column":1,"index":445},"filename":"invalid-derived-computation-in-effect.ts"},"fnName":"Component","memoSlots":5,"memoBlocks":2,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>Taylor Swift</div>
|
||||
@@ -1,4 +1,4 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component() {
|
||||
@@ -0,0 +1,79 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
export default function Component(props) {
|
||||
const [displayValue, setDisplayValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const computed = props.prefix + props.value + props.suffix;
|
||||
setDisplayValue(computed);
|
||||
}, [props.prefix, props.value, props.suffix]);
|
||||
|
||||
return <div>{displayValue}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{prefix: '[', value: 'test', suffix: ']'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function Component(props) {
|
||||
const $ = _c(7);
|
||||
const [displayValue, setDisplayValue] = useState("");
|
||||
let t0;
|
||||
let t1;
|
||||
if ($[0] !== props.prefix || $[1] !== props.suffix || $[2] !== props.value) {
|
||||
t0 = () => {
|
||||
const computed = props.prefix + props.value + props.suffix;
|
||||
setDisplayValue(computed);
|
||||
};
|
||||
t1 = [props.prefix, props.value, props.suffix];
|
||||
$[0] = props.prefix;
|
||||
$[1] = props.suffix;
|
||||
$[2] = props.value;
|
||||
$[3] = t0;
|
||||
$[4] = t1;
|
||||
} else {
|
||||
t0 = $[3];
|
||||
t1 = $[4];
|
||||
}
|
||||
useEffect(t0, t1);
|
||||
let t2;
|
||||
if ($[5] !== displayValue) {
|
||||
t2 = <div>{displayValue}</div>;
|
||||
$[5] = displayValue;
|
||||
$[6] = t2;
|
||||
} else {
|
||||
t2 = $[6];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ prefix: "[", value: "test", suffix: "]" }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```
|
||||
{"kind":"CompileError","detail":{"options":{"description":"Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user\n\nThis setState call is setting a derived value that depends on the following reactive sources:\n\nProps: [props]\n\nData Flow Tree:\n└── computed\n └── props (Prop)\n\nSee: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state","category":"EffectDerivationsOfState","reason":"You might not need an effect. Derive values in render, not effects.","details":[{"kind":"error","loc":{"start":{"line":9,"column":4,"index":295},"end":{"line":9,"column":19,"index":310},"filename":"invalid-derived-state-from-computed-props.ts","identifierName":"setDisplayValue"},"message":"This should be computed during render, not in an effect"}]}},"fnLoc":null}
|
||||
{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":15,"index":122},"end":{"line":13,"column":1,"index":409},"filename":"invalid-derived-state-from-computed-props.ts"},"fnName":"Component","memoSlots":7,"memoBlocks":2,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>[test]</div>
|
||||
@@ -1,4 +1,4 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
export default function Component(props) {
|
||||
@@ -0,0 +1,81 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
export default function Component({props}) {
|
||||
const [fullName, setFullName] = useState(
|
||||
props.firstName + ' ' + props.lastName
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setFullName(props.firstName + ' ' + props.lastName);
|
||||
}, [props.firstName, props.lastName]);
|
||||
|
||||
return <div>{fullName}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{props: {firstName: 'John', lastName: 'Doe'}}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function Component(t0) {
|
||||
const $ = _c(6);
|
||||
const { props } = t0;
|
||||
const [fullName, setFullName] = useState(
|
||||
props.firstName + " " + props.lastName,
|
||||
);
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== props.firstName || $[1] !== props.lastName) {
|
||||
t1 = () => {
|
||||
setFullName(props.firstName + " " + props.lastName);
|
||||
};
|
||||
t2 = [props.firstName, props.lastName];
|
||||
$[0] = props.firstName;
|
||||
$[1] = props.lastName;
|
||||
$[2] = t1;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
t2 = $[3];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[4] !== fullName) {
|
||||
t3 = <div>{fullName}</div>;
|
||||
$[4] = fullName;
|
||||
$[5] = t3;
|
||||
} else {
|
||||
t3 = $[5];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ props: { firstName: "John", lastName: "Doe" } }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```
|
||||
{"kind":"CompileError","detail":{"options":{"description":"Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user\n\nThis setState call is setting a derived value that depends on the following reactive sources:\n\nProps: [props]\n\nData Flow Tree:\n└── props (Prop)\n\nSee: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state","category":"EffectDerivationsOfState","reason":"You might not need an effect. Derive values in render, not effects.","details":[{"kind":"error","loc":{"start":{"line":10,"column":4,"index":269},"end":{"line":10,"column":15,"index":280},"filename":"invalid-derived-state-from-destructured-props.ts","identifierName":"setFullName"},"message":"This should be computed during render, not in an effect"}]}},"fnLoc":null}
|
||||
{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":15,"index":122},"end":{"line":14,"column":1,"index":397},"filename":"invalid-derived-state-from-destructured-props.ts"},"fnName":"Component","memoSlots":6,"memoBlocks":2,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>John Doe</div>
|
||||
@@ -1,4 +1,4 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
export default function Component({props}) {
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import {useEffect, useState, useRef} from 'react';
|
||||
|
||||
export default function Component({test}) {
|
||||
@@ -31,7 +31,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
|
||||
export default function Component(t0) {
|
||||
@@ -77,6 +77,12 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```
|
||||
{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":15,"index":130},"end":{"line":18,"column":1,"index":386},"filename":"ref-conditional-in-effect-no-error.ts"},"fnName":"Component","memoSlots":5,"memoBlocks":2,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) 8
|
||||
@@ -1,4 +1,4 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
import {useEffect, useState, useRef} from 'react';
|
||||
|
||||
export default function Component({test}) {
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
|
||||
## 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
|
||||
@@ -0,0 +1,18 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
|
||||
|
||||
function Component({prop}) {
|
||||
const [s, setS] = useState();
|
||||
const [second, setSecond] = useState(prop);
|
||||
|
||||
/*
|
||||
* `second` is a source of state. It will inherit the value of `prop` in
|
||||
* the first render, but after that it will no longer be updated when
|
||||
* `prop` changes. So we shouldn't consider `second` as being derived from
|
||||
* `prop`
|
||||
*/
|
||||
useEffect(() => {
|
||||
setS(second);
|
||||
}, [second]);
|
||||
|
||||
return <div>{s}</div>;
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateSourceLocations
|
||||
import {useEffect, useCallback} from 'react';
|
||||
|
||||
function Component({prop1, prop2}) {
|
||||
const x = prop1 + prop2;
|
||||
const y = x * 2;
|
||||
const arr = [x, y];
|
||||
const obj = {x, y};
|
||||
const [a, b] = arr;
|
||||
const {x: c, y: d} = obj;
|
||||
|
||||
useEffect(() => {
|
||||
if (a > 10) {
|
||||
console.log(a);
|
||||
}
|
||||
}, [a]);
|
||||
|
||||
const foo = useCallback(() => {
|
||||
return a + b;
|
||||
}, [a, b]);
|
||||
|
||||
function bar() {
|
||||
return (c + d) * 2;
|
||||
}
|
||||
|
||||
console.log('Hello, world!');
|
||||
|
||||
return [y, foo, bar];
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 13 errors:
|
||||
|
||||
Todo: Important source location missing in generated code
|
||||
|
||||
Source location for VariableDeclarator is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
|
||||
|
||||
error.todo-missing-source-locations.ts:5:8
|
||||
3 |
|
||||
4 | function Component({prop1, prop2}) {
|
||||
> 5 | const x = prop1 + prop2;
|
||||
| ^^^^^^^^^^^^^^^^^
|
||||
6 | const y = x * 2;
|
||||
7 | const arr = [x, y];
|
||||
8 | const obj = {x, y};
|
||||
|
||||
Todo: Important source location missing in generated code
|
||||
|
||||
Source location for VariableDeclarator is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
|
||||
|
||||
error.todo-missing-source-locations.ts:6:8
|
||||
4 | function Component({prop1, prop2}) {
|
||||
5 | const x = prop1 + prop2;
|
||||
> 6 | const y = x * 2;
|
||||
| ^^^^^^^^^
|
||||
7 | const arr = [x, y];
|
||||
8 | const obj = {x, y};
|
||||
9 | const [a, b] = arr;
|
||||
|
||||
Todo: Important source location missing in generated code
|
||||
|
||||
Source location for VariableDeclarator is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
|
||||
|
||||
error.todo-missing-source-locations.ts:7:8
|
||||
5 | const x = prop1 + prop2;
|
||||
6 | const y = x * 2;
|
||||
> 7 | const arr = [x, y];
|
||||
| ^^^^^^^^^^^^
|
||||
8 | const obj = {x, y};
|
||||
9 | const [a, b] = arr;
|
||||
10 | const {x: c, y: d} = obj;
|
||||
|
||||
Todo: Important source location missing in generated code
|
||||
|
||||
Source location for VariableDeclarator is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
|
||||
|
||||
error.todo-missing-source-locations.ts:8:8
|
||||
6 | const y = x * 2;
|
||||
7 | const arr = [x, y];
|
||||
> 8 | const obj = {x, y};
|
||||
| ^^^^^^^^^^^^
|
||||
9 | const [a, b] = arr;
|
||||
10 | const {x: c, y: d} = obj;
|
||||
11 |
|
||||
|
||||
Todo: Important source location missing in generated code
|
||||
|
||||
Source location for VariableDeclarator is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
|
||||
|
||||
error.todo-missing-source-locations.ts:9:8
|
||||
7 | const arr = [x, y];
|
||||
8 | const obj = {x, y};
|
||||
> 9 | const [a, b] = arr;
|
||||
| ^^^^^^^^^^^^
|
||||
10 | const {x: c, y: d} = obj;
|
||||
11 |
|
||||
12 | useEffect(() => {
|
||||
|
||||
Todo: Important source location missing in generated code
|
||||
|
||||
Source location for VariableDeclarator is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
|
||||
|
||||
error.todo-missing-source-locations.ts:10:8
|
||||
8 | const obj = {x, y};
|
||||
9 | const [a, b] = arr;
|
||||
> 10 | const {x: c, y: d} = obj;
|
||||
| ^^^^^^^^^^^^^^^^^^
|
||||
11 |
|
||||
12 | useEffect(() => {
|
||||
13 | if (a > 10) {
|
||||
|
||||
Todo: Important source location missing in generated code
|
||||
|
||||
Source location for ExpressionStatement is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
|
||||
|
||||
error.todo-missing-source-locations.ts:12:2
|
||||
10 | const {x: c, y: d} = obj;
|
||||
11 |
|
||||
> 12 | useEffect(() => {
|
||||
| ^^^^^^^^^^^^^^^^^
|
||||
> 13 | if (a > 10) {
|
||||
| ^^^^^^^^^^^^^^^^^
|
||||
> 14 | console.log(a);
|
||||
| ^^^^^^^^^^^^^^^^^
|
||||
> 15 | }
|
||||
| ^^^^^^^^^^^^^^^^^
|
||||
> 16 | }, [a]);
|
||||
| ^^^^^^^^^^^
|
||||
17 |
|
||||
18 | const foo = useCallback(() => {
|
||||
19 | return a + b;
|
||||
|
||||
Todo: Important source location missing in generated code
|
||||
|
||||
Source location for ExpressionStatement is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
|
||||
|
||||
error.todo-missing-source-locations.ts:14:6
|
||||
12 | useEffect(() => {
|
||||
13 | if (a > 10) {
|
||||
> 14 | console.log(a);
|
||||
| ^^^^^^^^^^^^^^^
|
||||
15 | }
|
||||
16 | }, [a]);
|
||||
17 |
|
||||
|
||||
Todo: Important source location missing in generated code
|
||||
|
||||
Source location for VariableDeclarator is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
|
||||
|
||||
error.todo-missing-source-locations.ts:18:8
|
||||
16 | }, [a]);
|
||||
17 |
|
||||
> 18 | const foo = useCallback(() => {
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 19 | return a + b;
|
||||
| ^^^^^^^^^^^^^^^^^
|
||||
> 20 | }, [a, b]);
|
||||
| ^^^^^^^^^^^^^
|
||||
21 |
|
||||
22 | function bar() {
|
||||
23 | return (c + d) * 2;
|
||||
|
||||
Todo: Important source location missing in generated code
|
||||
|
||||
Source location for ReturnStatement is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
|
||||
|
||||
error.todo-missing-source-locations.ts:19:4
|
||||
17 |
|
||||
18 | const foo = useCallback(() => {
|
||||
> 19 | return a + b;
|
||||
| ^^^^^^^^^^^^^
|
||||
20 | }, [a, b]);
|
||||
21 |
|
||||
22 | function bar() {
|
||||
|
||||
Todo: Important source location missing in generated code
|
||||
|
||||
Source location for ReturnStatement is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
|
||||
|
||||
error.todo-missing-source-locations.ts:23:4
|
||||
21 |
|
||||
22 | function bar() {
|
||||
> 23 | return (c + d) * 2;
|
||||
| ^^^^^^^^^^^^^^^^^^^
|
||||
24 | }
|
||||
25 |
|
||||
26 | console.log('Hello, world!');
|
||||
|
||||
Todo: Important source location missing in generated code
|
||||
|
||||
Source location for ExpressionStatement is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
|
||||
|
||||
error.todo-missing-source-locations.ts:26:2
|
||||
24 | }
|
||||
25 |
|
||||
> 26 | console.log('Hello, world!');
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
27 |
|
||||
28 | return [y, foo, bar];
|
||||
29 | }
|
||||
|
||||
Todo: Important source location missing in generated code
|
||||
|
||||
Source location for ReturnStatement is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
|
||||
|
||||
error.todo-missing-source-locations.ts:28:2
|
||||
26 | console.log('Hello, world!');
|
||||
27 |
|
||||
> 28 | return [y, foo, bar];
|
||||
| ^^^^^^^^^^^^^^^^^^^^^
|
||||
29 | }
|
||||
30 |
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
// @validateSourceLocations
|
||||
import {useEffect, useCallback} from 'react';
|
||||
|
||||
function Component({prop1, prop2}) {
|
||||
const x = prop1 + prop2;
|
||||
const y = x * 2;
|
||||
const arr = [x, y];
|
||||
const obj = {x, y};
|
||||
const [a, b] = arr;
|
||||
const {x: c, y: d} = obj;
|
||||
|
||||
useEffect(() => {
|
||||
if (a > 10) {
|
||||
console.log(a);
|
||||
}
|
||||
}, [a]);
|
||||
|
||||
const foo = useCallback(() => {
|
||||
return a + b;
|
||||
}, [a, b]);
|
||||
|
||||
function bar() {
|
||||
return (c + d) * 2;
|
||||
}
|
||||
|
||||
console.log('Hello, world!');
|
||||
|
||||
return [y, foo, bar];
|
||||
}
|
||||
@@ -152,6 +152,7 @@
|
||||
"download-build-in-codesandbox-ci": "yarn build --type=node react/index react.react-server react-dom/index react-dom/client react-dom/src/server react-dom/test-utils react-dom.react-server scheduler/index react/jsx-runtime react/jsx-dev-runtime react-server-dom-webpack",
|
||||
"check-release-dependencies": "node ./scripts/release/check-release-dependencies",
|
||||
"generate-inline-fizz-runtime": "node ./scripts/rollup/generate-inline-fizz-runtime.js",
|
||||
"generate-changelog": "node ./scripts/tasks/generate-changelog/index.js",
|
||||
"flags": "node ./scripts/flags/flags.js"
|
||||
},
|
||||
"resolutions": {
|
||||
|
||||
@@ -585,6 +585,29 @@ const allTests = {
|
||||
code: normalizeIndent`
|
||||
// Valid: useEffectEvent can be called in custom effect hooks configured via ESLint settings
|
||||
function MyComponent({ theme }) {
|
||||
const onClick = useEffectEvent(() => {
|
||||
showNotification(theme);
|
||||
});
|
||||
useMyEffect(() => {
|
||||
onClick();
|
||||
});
|
||||
useServerEffect(() => {
|
||||
onClick();
|
||||
});
|
||||
}
|
||||
`,
|
||||
settings: {
|
||||
'react-hooks': {
|
||||
additionalEffectHooks: '(useMyEffect|useServerEffect)',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
syntax: 'flow',
|
||||
code: normalizeIndent`
|
||||
// Component syntax version
|
||||
// Valid: useEffectEvent can be called in custom effect hooks configured via ESLint settings
|
||||
component MyComponent(theme: any) {
|
||||
const onClick = useEffectEvent(() => {
|
||||
showNotification(theme);
|
||||
});
|
||||
@@ -618,6 +641,24 @@ const allTests = {
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
syntax: 'flow',
|
||||
code: normalizeIndent`
|
||||
// Component syntax version
|
||||
// Valid because functions created with useEffectEvent can be called in a useEffect.
|
||||
component MyComponent(theme: any) {
|
||||
const onClick = useEffectEvent(() => {
|
||||
showNotification(theme);
|
||||
});
|
||||
useEffect(() => {
|
||||
onClick();
|
||||
});
|
||||
React.useEffect(() => {
|
||||
onClick();
|
||||
});
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
code: normalizeIndent`
|
||||
// Valid because functions created with useEffectEvent can be passed by reference in useEffect
|
||||
@@ -644,6 +685,34 @@ const allTests = {
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
syntax: 'flow',
|
||||
code: normalizeIndent`
|
||||
// Component syntax version
|
||||
// Valid because functions created with useEffectEvent can be passed by reference in useEffect
|
||||
// and useEffectEvent.
|
||||
component MyComponent(theme: any) {
|
||||
const onClick = useEffectEvent(() => {
|
||||
showNotification(theme);
|
||||
});
|
||||
const onClick2 = useEffectEvent(() => {
|
||||
debounce(onClick);
|
||||
debounce(() => onClick());
|
||||
debounce(() => { onClick() });
|
||||
deboucne(() => debounce(onClick));
|
||||
});
|
||||
useEffect(() => {
|
||||
let id = setInterval(() => onClick(), 100);
|
||||
return () => clearInterval(onClick);
|
||||
}, []);
|
||||
React.useEffect(() => {
|
||||
let id = setInterval(() => onClick(), 100);
|
||||
return () => clearInterval(onClick);
|
||||
}, []);
|
||||
return null;
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
code: normalizeIndent`
|
||||
function MyComponent({ theme }) {
|
||||
@@ -656,6 +725,20 @@ const allTests = {
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
syntax: 'flow',
|
||||
code: normalizeIndent`
|
||||
// Component syntax version
|
||||
component MyComponent(theme: any) {
|
||||
useEffect(() => {
|
||||
onClick();
|
||||
});
|
||||
const onClick = useEffectEvent(() => {
|
||||
showNotification(theme);
|
||||
});
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
code: normalizeIndent`
|
||||
function MyComponent({ theme }) {
|
||||
@@ -673,6 +756,25 @@ const allTests = {
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
syntax: 'flow',
|
||||
code: normalizeIndent`
|
||||
// Component syntax version
|
||||
component MyComponent(theme: any) {
|
||||
// Can receive arguments
|
||||
const onEvent = useEffectEvent((text) => {
|
||||
console.log(text);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
onEvent('Hello world');
|
||||
});
|
||||
React.useEffect(() => {
|
||||
onEvent('Hello world');
|
||||
});
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
code: normalizeIndent`
|
||||
// Valid because functions created with useEffectEvent can be called in useLayoutEffect.
|
||||
@@ -689,6 +791,24 @@ const allTests = {
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
syntax: 'flow',
|
||||
code: normalizeIndent`
|
||||
// Component syntax version
|
||||
// Valid because functions created with useEffectEvent can be called in useLayoutEffect.
|
||||
component MyComponent(theme: any) {
|
||||
const onClick = useEffectEvent(() => {
|
||||
showNotification(theme);
|
||||
});
|
||||
useLayoutEffect(() => {
|
||||
onClick();
|
||||
});
|
||||
React.useLayoutEffect(() => {
|
||||
onClick();
|
||||
});
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
code: normalizeIndent`
|
||||
// Valid because functions created with useEffectEvent can be called in useInsertionEffect.
|
||||
@@ -705,6 +825,24 @@ const allTests = {
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
syntax: 'flow',
|
||||
code: normalizeIndent`
|
||||
// Component syntax version
|
||||
// Valid because functions created with useEffectEvent can be called in useInsertionEffect.
|
||||
component MyComponent(theme) {
|
||||
const onClick = useEffectEvent(() => {
|
||||
showNotification(theme);
|
||||
});
|
||||
useInsertionEffect(() => {
|
||||
onClick();
|
||||
});
|
||||
React.useInsertionEffect(() => {
|
||||
onClick();
|
||||
});
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
code: normalizeIndent`
|
||||
// Valid because functions created with useEffectEvent can be passed by reference in useLayoutEffect
|
||||
@@ -739,6 +877,42 @@ const allTests = {
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
syntax: 'flow',
|
||||
code: normalizeIndent`
|
||||
// Component syntax version
|
||||
// Valid because functions created with useEffectEvent can be passed by reference in useLayoutEffect.
|
||||
// and useInsertionEffect.
|
||||
component MyComponent(theme: any) {
|
||||
const onClick = useEffectEvent(() => {
|
||||
showNotification(theme);
|
||||
});
|
||||
const onClick2 = useEffectEvent(() => {
|
||||
debounce(onClick);
|
||||
debounce(() => onClick());
|
||||
debounce(() => { onClick() });
|
||||
deboucne(() => debounce(onClick));
|
||||
});
|
||||
useLayoutEffect(() => {
|
||||
let id = setInterval(() => onClick(), 100);
|
||||
return () => clearInterval(onClick);
|
||||
}, []);
|
||||
React.useLayoutEffect(() => {
|
||||
let id = setInterval(() => onClick(), 100);
|
||||
return () => clearInterval(onClick);
|
||||
}, []);
|
||||
useInsertionEffect(() => {
|
||||
let id = setInterval(() => onClick(), 100);
|
||||
return () => clearInterval(onClick);
|
||||
}, []);
|
||||
React.useInsertionEffect(() => {
|
||||
let id = setInterval(() => onClick(), 100);
|
||||
return () => clearInterval(onClick);
|
||||
}, []);
|
||||
return null;
|
||||
}
|
||||
`,
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
@@ -1525,6 +1699,22 @@ const allTests = {
|
||||
`,
|
||||
errors: [useEffectEventError('onClick', true)],
|
||||
},
|
||||
{
|
||||
syntax: 'flow',
|
||||
code: normalizeIndent`
|
||||
// Component syntax version
|
||||
// Invalid: useEffectEvent should not be callable in regular custom hooks without additional configuration
|
||||
component MyComponent() {
|
||||
const onClick = useEffectEvent(() => {
|
||||
showNotification(theme);
|
||||
});
|
||||
useCustomHook(() => {
|
||||
onClick();
|
||||
});
|
||||
}
|
||||
`,
|
||||
errors: [useEffectEventError('onClick', true)],
|
||||
},
|
||||
{
|
||||
code: normalizeIndent`
|
||||
// Invalid: useEffectEvent should not be callable in hooks not matching the settings regex
|
||||
@@ -1544,6 +1734,27 @@ const allTests = {
|
||||
},
|
||||
errors: [useEffectEventError('onClick', true)],
|
||||
},
|
||||
{
|
||||
syntax: 'flow',
|
||||
code: normalizeIndent`
|
||||
// Component syntax version
|
||||
// Invalid: useEffectEvent should not be callable in hooks not matching the settings regex
|
||||
component MyComponent(theme: any) {
|
||||
const onClick = useEffectEvent(() => {
|
||||
showNotification(theme);
|
||||
});
|
||||
useWrongHook(() => {
|
||||
onClick();
|
||||
});
|
||||
}
|
||||
`,
|
||||
settings: {
|
||||
'react-hooks': {
|
||||
additionalEffectHooks: 'useMyEffect',
|
||||
},
|
||||
},
|
||||
errors: [useEffectEventError('onClick', true)],
|
||||
},
|
||||
{
|
||||
code: normalizeIndent`
|
||||
function MyComponent({ theme }) {
|
||||
@@ -1555,6 +1766,19 @@ const allTests = {
|
||||
`,
|
||||
errors: [useEffectEventError('onClick', false)],
|
||||
},
|
||||
{
|
||||
syntax: 'flow',
|
||||
code: normalizeIndent`
|
||||
// Component syntax version
|
||||
component MyComponent(theme: any) {
|
||||
const onClick = useEffectEvent(() => {
|
||||
showNotification(theme);
|
||||
});
|
||||
return <Child onClick={onClick}></Child>;
|
||||
}
|
||||
`,
|
||||
errors: [useEffectEventError('onClick', false)],
|
||||
},
|
||||
{
|
||||
code: normalizeIndent`
|
||||
// Invalid because useEffectEvent is being passed down
|
||||
@@ -1566,6 +1790,19 @@ const allTests = {
|
||||
`,
|
||||
errors: [{...useEffectEventError(null, false), line: 4}],
|
||||
},
|
||||
{
|
||||
syntax: 'flow',
|
||||
code: normalizeIndent`
|
||||
// Component syntax version
|
||||
// Invalid because useEffectEvent is being passed down
|
||||
component MyComponent(theme: any) {
|
||||
return <Child onClick={useEffectEvent(() => {
|
||||
showNotification(theme);
|
||||
})} />;
|
||||
}
|
||||
`,
|
||||
errors: [{...useEffectEventError(null, false), line: 5}],
|
||||
},
|
||||
{
|
||||
code: normalizeIndent`
|
||||
// This should error even though it shares an identifier name with the below
|
||||
@@ -1601,6 +1838,43 @@ const allTests = {
|
||||
{...useEffectEventError('onClick', true), line: 15},
|
||||
],
|
||||
},
|
||||
{
|
||||
syntax: 'flow',
|
||||
code: normalizeIndent`
|
||||
// Component syntax version
|
||||
// This should error even though it shares an identifier name with the below
|
||||
component MyComponent(theme: any) {
|
||||
const onClick = useEffectEvent(() => {
|
||||
showNotification(theme)
|
||||
});
|
||||
return <Child onClick={onClick} />
|
||||
}
|
||||
|
||||
// The useEffectEvent function shares an identifier name with the above
|
||||
component MyOtherComponent(theme: any) {
|
||||
const onClick = useEffectEvent(() => {
|
||||
showNotification(theme)
|
||||
});
|
||||
return <Child onClick={() => onClick()} />
|
||||
}
|
||||
|
||||
// The useEffectEvent function shares an identifier name with the above
|
||||
component MyLastComponent(theme: any) {
|
||||
const onClick = useEffectEvent(() => {
|
||||
showNotification(theme)
|
||||
});
|
||||
useEffect(() => {
|
||||
onClick(); // No error here, errors on all other uses
|
||||
onClick;
|
||||
})
|
||||
return <Child />
|
||||
}
|
||||
`,
|
||||
errors: [
|
||||
{...useEffectEventError('onClick', false), line: 8},
|
||||
{...useEffectEventError('onClick', true), line: 16},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: normalizeIndent`
|
||||
const MyComponent = ({ theme }) => {
|
||||
@@ -1625,6 +1899,21 @@ const allTests = {
|
||||
`,
|
||||
errors: [{...useEffectEventError('onClick', false), line: 7}],
|
||||
},
|
||||
{
|
||||
syntax: 'flow',
|
||||
code: normalizeIndent`
|
||||
// Component syntax version
|
||||
// Invalid because onClick is being aliased to foo but not invoked
|
||||
component MyComponent(theme: any) {
|
||||
const onClick = useEffectEvent(() => {
|
||||
showNotification(theme);
|
||||
});
|
||||
let foo = onClick;
|
||||
return <Bar onClick={foo} />
|
||||
}
|
||||
`,
|
||||
errors: [{...useEffectEventError('onClick', false), line: 8}],
|
||||
},
|
||||
{
|
||||
code: normalizeIndent`
|
||||
// Should error because it's being passed down to JSX, although it's been referenced once
|
||||
@@ -1641,6 +1930,24 @@ const allTests = {
|
||||
`,
|
||||
errors: [useEffectEventError('onClick', false)],
|
||||
},
|
||||
{
|
||||
syntax: 'flow',
|
||||
code: normalizeIndent`
|
||||
// Component syntax version
|
||||
// Should error because it's being passed down to JSX, although it's been referenced once
|
||||
// in an effect
|
||||
component MyComponent(theme: any) {
|
||||
const onClick = useEffectEvent(() => {
|
||||
showNotification(them);
|
||||
});
|
||||
useEffect(() => {
|
||||
setTimeout(onClick, 100);
|
||||
});
|
||||
return <Child onClick={onClick} />
|
||||
}
|
||||
`,
|
||||
errors: [useEffectEventError('onClick', false)],
|
||||
},
|
||||
{
|
||||
code: normalizeIndent`
|
||||
// Invalid because functions created with useEffectEvent cannot be called in arbitrary closures.
|
||||
@@ -1676,6 +1983,43 @@ const allTests = {
|
||||
`It cannot be assigned to a variable or passed down.`,
|
||||
],
|
||||
},
|
||||
{
|
||||
syntax: 'flow',
|
||||
code: normalizeIndent`
|
||||
// Hook syntax version
|
||||
// Invalid because functions created with useEffectEvent cannot be called in arbitrary closures.
|
||||
hook useMyHook(theme: any) {
|
||||
const onClick = useEffectEvent(() => {
|
||||
showNotification(theme);
|
||||
});
|
||||
// error message 1
|
||||
const onClick2 = () => { onClick() };
|
||||
// error message 2
|
||||
const onClick3 = useCallback(() => onClick(), []);
|
||||
// error message 3
|
||||
const onClick4 = onClick;
|
||||
return <>
|
||||
{/** error message 4 */}
|
||||
<Child onClick={onClick}></Child>
|
||||
<Child onClick={onClick2}></Child>
|
||||
<Child onClick={onClick3}></Child>
|
||||
</>;
|
||||
}
|
||||
`,
|
||||
// Explicitly test error messages here for various cases
|
||||
errors: [
|
||||
`\`onClick\` is a function created with React Hook "useEffectEvent", and can only be called from ` +
|
||||
'Effects and Effect Events in the same component.',
|
||||
`\`onClick\` is a function created with React Hook "useEffectEvent", and can only be called from ` +
|
||||
'Effects and Effect Events in the same component.',
|
||||
`\`onClick\` is a function created with React Hook "useEffectEvent", and can only be called from ` +
|
||||
`Effects and Effect Events in the same component. ` +
|
||||
`It cannot be assigned to a variable or passed down.`,
|
||||
`\`onClick\` is a function created with React Hook "useEffectEvent", and can only be called from ` +
|
||||
`Effects and Effect Events in the same component. ` +
|
||||
`It cannot be assigned to a variable or passed down.`,
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -833,6 +833,18 @@ const rule = {
|
||||
recordAllUseEffectEventFunctions(getScope(node));
|
||||
}
|
||||
},
|
||||
|
||||
// @ts-expect-error parser-hermes produces these node types
|
||||
ComponentDeclaration(node) {
|
||||
// component MyComponent() { const onClick = useEffectEvent(...) }
|
||||
recordAllUseEffectEventFunctions(getScope(node));
|
||||
},
|
||||
|
||||
// @ts-expect-error parser-hermes produces these node types
|
||||
HookDeclaration(node) {
|
||||
// hook useMyHook() { const onClick = useEffectEvent(...) }
|
||||
recordAllUseEffectEventFunctions(getScope(node));
|
||||
},
|
||||
};
|
||||
},
|
||||
} satisfies Rule.RuleModule;
|
||||
|
||||
282
packages/react-client/src/ReactFlightClient.js
vendored
282
packages/react-client/src/ReactFlightClient.js
vendored
@@ -39,12 +39,9 @@ import type {
|
||||
EncodeFormActionCallback,
|
||||
} from './ReactFlightReplyClient';
|
||||
|
||||
import type {Postpone} from 'react/src/ReactPostpone';
|
||||
|
||||
import type {TemporaryReferenceSet} from './ReactFlightTemporaryReferences';
|
||||
|
||||
import {
|
||||
enablePostpone,
|
||||
enableProfilerTimer,
|
||||
enableComponentPerformanceTrack,
|
||||
enableAsyncDebugInfo,
|
||||
@@ -89,7 +86,6 @@ import {
|
||||
import {
|
||||
REACT_LAZY_TYPE,
|
||||
REACT_ELEMENT_TYPE,
|
||||
REACT_POSTPONE_TYPE,
|
||||
ASYNC_ITERATOR,
|
||||
REACT_FRAGMENT_TYPE,
|
||||
} from 'shared/ReactSymbols';
|
||||
@@ -367,6 +363,7 @@ type Response = {
|
||||
_debugRootStack?: null | Error, // DEV-only
|
||||
_debugRootTask?: null | ConsoleTask, // DEV-only
|
||||
_debugStartTime: number, // DEV-only
|
||||
_debugEndTime?: number, // DEV-only
|
||||
_debugIOStarted: boolean, // DEV-only
|
||||
_debugFindSourceMapURL?: void | FindSourceMapURLCallback, // DEV-only
|
||||
_debugChannel?: void | DebugChannel, // DEV-only
|
||||
@@ -500,6 +497,33 @@ function createErrorChunk<T>(
|
||||
return new ReactPromise(ERRORED, null, error);
|
||||
}
|
||||
|
||||
function filterDebugInfo(
|
||||
response: Response,
|
||||
value: {_debugInfo: ReactDebugInfo, ...},
|
||||
) {
|
||||
if (response._debugEndTime === null) {
|
||||
// No end time was defined, so we keep all debug info entries.
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove any debug info entries after the defined end time. For async info
|
||||
// that means we're including anything that was awaited before the end time,
|
||||
// but it doesn't need to be resolved before the end time.
|
||||
const relativeEndTime =
|
||||
response._debugEndTime -
|
||||
// $FlowFixMe[prop-missing]
|
||||
performance.timeOrigin;
|
||||
const debugInfo = [];
|
||||
for (let i = 0; i < value._debugInfo.length; i++) {
|
||||
const info = value._debugInfo[i];
|
||||
if (typeof info.time === 'number' && info.time > relativeEndTime) {
|
||||
break;
|
||||
}
|
||||
debugInfo.push(info);
|
||||
}
|
||||
value._debugInfo = debugInfo;
|
||||
}
|
||||
|
||||
function moveDebugInfoFromChunkToInnerValue<T>(
|
||||
chunk: InitializedChunk<T> | InitializedStreamChunk<any>,
|
||||
value: T,
|
||||
@@ -534,7 +558,17 @@ function moveDebugInfoFromChunkToInnerValue<T>(
|
||||
}
|
||||
}
|
||||
|
||||
function processChunkDebugInfo<T>(
|
||||
response: Response,
|
||||
chunk: InitializedChunk<T> | InitializedStreamChunk<any>,
|
||||
value: T,
|
||||
): void {
|
||||
filterDebugInfo(response, chunk);
|
||||
moveDebugInfoFromChunkToInnerValue(chunk, value);
|
||||
}
|
||||
|
||||
function wakeChunk<T>(
|
||||
response: Response,
|
||||
listeners: Array<InitializationReference | (T => mixed)>,
|
||||
value: T,
|
||||
chunk: InitializedChunk<T>,
|
||||
@@ -544,16 +578,17 @@ function wakeChunk<T>(
|
||||
if (typeof listener === 'function') {
|
||||
listener(value);
|
||||
} else {
|
||||
fulfillReference(listener, value, chunk);
|
||||
fulfillReference(response, listener, value, chunk);
|
||||
}
|
||||
}
|
||||
|
||||
if (__DEV__) {
|
||||
moveDebugInfoFromChunkToInnerValue(chunk, value);
|
||||
processChunkDebugInfo(response, chunk, value);
|
||||
}
|
||||
}
|
||||
|
||||
function rejectChunk(
|
||||
response: Response,
|
||||
listeners: Array<InitializationReference | (mixed => mixed)>,
|
||||
error: mixed,
|
||||
): void {
|
||||
@@ -562,7 +597,7 @@ function rejectChunk(
|
||||
if (typeof listener === 'function') {
|
||||
listener(error);
|
||||
} else {
|
||||
rejectReference(listener, error);
|
||||
rejectReference(response, listener.handler, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -595,13 +630,14 @@ function resolveBlockedCycle<T>(
|
||||
}
|
||||
|
||||
function wakeChunkIfInitialized<T>(
|
||||
response: Response,
|
||||
chunk: SomeChunk<T>,
|
||||
resolveListeners: Array<InitializationReference | (T => mixed)>,
|
||||
rejectListeners: null | Array<InitializationReference | (mixed => mixed)>,
|
||||
): void {
|
||||
switch (chunk.status) {
|
||||
case INITIALIZED:
|
||||
wakeChunk(resolveListeners, chunk.value, chunk);
|
||||
wakeChunk(response, resolveListeners, chunk.value, chunk);
|
||||
break;
|
||||
case BLOCKED:
|
||||
// It is possible that we're blocked on our own chunk if it's a cycle.
|
||||
@@ -615,7 +651,7 @@ function wakeChunkIfInitialized<T>(
|
||||
if (cyclicHandler !== null) {
|
||||
// This reference points back to this chunk. We can resolve the cycle by
|
||||
// using the value from that handler.
|
||||
fulfillReference(reference, cyclicHandler.value, chunk);
|
||||
fulfillReference(response, reference, cyclicHandler.value, chunk);
|
||||
resolveListeners.splice(i, 1);
|
||||
i--;
|
||||
if (rejectListeners !== null) {
|
||||
@@ -624,6 +660,23 @@ function wakeChunkIfInitialized<T>(
|
||||
rejectListeners.splice(rejectionIdx, 1);
|
||||
}
|
||||
}
|
||||
// The status might have changed after fulfilling the reference.
|
||||
switch ((chunk: SomeChunk<T>).status) {
|
||||
case INITIALIZED:
|
||||
const initializedChunk: InitializedChunk<T> = (chunk: any);
|
||||
wakeChunk(
|
||||
response,
|
||||
resolveListeners,
|
||||
initializedChunk.value,
|
||||
initializedChunk,
|
||||
);
|
||||
return;
|
||||
case ERRORED:
|
||||
if (rejectListeners !== null) {
|
||||
rejectChunk(response, rejectListeners, chunk.reason);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -650,7 +703,7 @@ function wakeChunkIfInitialized<T>(
|
||||
break;
|
||||
case ERRORED:
|
||||
if (rejectListeners) {
|
||||
rejectChunk(rejectListeners, chunk.reason);
|
||||
rejectChunk(response, rejectListeners, chunk.reason);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -708,7 +761,7 @@ function triggerErrorOnChunk<T>(
|
||||
erroredChunk.status = ERRORED;
|
||||
erroredChunk.reason = error;
|
||||
if (listeners !== null) {
|
||||
rejectChunk(listeners, error);
|
||||
rejectChunk(response, listeners, error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -816,7 +869,7 @@ function resolveModelChunk<T>(
|
||||
// longer be rendered or might not be the highest pri.
|
||||
initializeModelChunk(resolvedChunk);
|
||||
// The status might have changed after initialization.
|
||||
wakeChunkIfInitialized(chunk, resolveListeners, rejectListeners);
|
||||
wakeChunkIfInitialized(response, chunk, resolveListeners, rejectListeners);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -845,12 +898,11 @@ function resolveModuleChunk<T>(
|
||||
}
|
||||
if (resolveListeners !== null) {
|
||||
initializeModuleChunk(resolvedChunk);
|
||||
wakeChunkIfInitialized(chunk, resolveListeners, rejectListeners);
|
||||
wakeChunkIfInitialized(response, chunk, resolveListeners, rejectListeners);
|
||||
}
|
||||
}
|
||||
|
||||
type InitializationReference = {
|
||||
response: Response, // TODO: Remove Response from here and pass it through instead.
|
||||
handler: InitializationHandler,
|
||||
parentObject: Object,
|
||||
key: string,
|
||||
@@ -989,7 +1041,7 @@ function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): void {
|
||||
if (typeof listener === 'function') {
|
||||
listener(value);
|
||||
} else {
|
||||
fulfillReference(listener, value, cyclicChunk);
|
||||
fulfillReference(response, listener, value, cyclicChunk);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1010,7 +1062,7 @@ function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): void {
|
||||
initializedChunk.value = value;
|
||||
|
||||
if (__DEV__) {
|
||||
moveDebugInfoFromChunkToInnerValue(initializedChunk, value);
|
||||
processChunkDebugInfo(response, initializedChunk, value);
|
||||
}
|
||||
} catch (error) {
|
||||
const erroredChunk: ErroredChunk<T> = (chunk: any);
|
||||
@@ -1397,11 +1449,12 @@ function getChunk(response: Response, id: number): SomeChunk<any> {
|
||||
}
|
||||
|
||||
function fulfillReference(
|
||||
response: Response,
|
||||
reference: InitializationReference,
|
||||
value: any,
|
||||
fulfilledChunk: SomeChunk<any>,
|
||||
): void {
|
||||
const {response, handler, parentObject, key, map, path} = reference;
|
||||
const {handler, parentObject, key, map, path} = reference;
|
||||
|
||||
for (let i = 1; i < path.length; i++) {
|
||||
while (
|
||||
@@ -1471,7 +1524,11 @@ function fulfillReference(
|
||||
return;
|
||||
}
|
||||
default: {
|
||||
rejectReference(reference, referencedChunk.reason);
|
||||
rejectReference(
|
||||
response,
|
||||
reference.handler,
|
||||
referencedChunk.reason,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1569,21 +1626,20 @@ function fulfillReference(
|
||||
initializedChunk.value = handler.value;
|
||||
initializedChunk.reason = handler.reason; // Used by streaming chunks
|
||||
if (resolveListeners !== null) {
|
||||
wakeChunk(resolveListeners, handler.value, initializedChunk);
|
||||
wakeChunk(response, resolveListeners, handler.value, initializedChunk);
|
||||
} else {
|
||||
if (__DEV__) {
|
||||
moveDebugInfoFromChunkToInnerValue(initializedChunk, handler.value);
|
||||
processChunkDebugInfo(response, initializedChunk, handler.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function rejectReference(
|
||||
reference: InitializationReference,
|
||||
response: Response,
|
||||
handler: InitializationHandler,
|
||||
error: mixed,
|
||||
): void {
|
||||
const {handler, response} = reference;
|
||||
|
||||
if (handler.errored) {
|
||||
// We've already errored. We could instead build up an AggregateError
|
||||
// but if there are multiple errors we just take the first one like
|
||||
@@ -1674,7 +1730,6 @@ function waitForReference<T>(
|
||||
}
|
||||
|
||||
const reference: InitializationReference = {
|
||||
response,
|
||||
handler,
|
||||
parentObject,
|
||||
key,
|
||||
@@ -1822,10 +1877,10 @@ function loadServerReference<A: Iterable<any>, T>(
|
||||
initializedChunk.status = INITIALIZED;
|
||||
initializedChunk.value = handler.value;
|
||||
if (resolveListeners !== null) {
|
||||
wakeChunk(resolveListeners, handler.value, initializedChunk);
|
||||
wakeChunk(response, resolveListeners, handler.value, initializedChunk);
|
||||
} else {
|
||||
if (__DEV__) {
|
||||
moveDebugInfoFromChunkToInnerValue(initializedChunk, handler.value);
|
||||
processChunkDebugInfo(response, initializedChunk, handler.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2562,6 +2617,7 @@ function ResponseInstance(
|
||||
replayConsole: boolean, // DEV-only
|
||||
environmentName: void | string, // DEV-only
|
||||
debugStartTime: void | number, // DEV-only
|
||||
debugEndTime: void | number, // DEV-only
|
||||
debugChannel: void | DebugChannel, // DEV-only
|
||||
) {
|
||||
const chunks: Map<number, SomeChunk<any>> = new Map();
|
||||
@@ -2629,6 +2685,7 @@ function ResponseInstance(
|
||||
// and is not considered I/O required to load the stream.
|
||||
setTimeout(markIOStarted.bind(this), 0);
|
||||
}
|
||||
this._debugEndTime = debugEndTime == null ? null : debugEndTime;
|
||||
this._debugFindSourceMapURL = findSourceMapURL;
|
||||
this._debugChannel = debugChannel;
|
||||
this._blockedConsole = null;
|
||||
@@ -2672,6 +2729,7 @@ export function createResponse(
|
||||
replayConsole: boolean, // DEV-only
|
||||
environmentName: void | string, // DEV-only
|
||||
debugStartTime: void | number, // DEV-only
|
||||
debugEndTime: void | number, // DEV-only
|
||||
debugChannel: void | DebugChannel, // DEV-only
|
||||
): WeakResponse {
|
||||
return getWeakResponse(
|
||||
@@ -2688,6 +2746,7 @@ export function createResponse(
|
||||
replayConsole,
|
||||
environmentName,
|
||||
debugStartTime,
|
||||
debugEndTime,
|
||||
debugChannel,
|
||||
),
|
||||
);
|
||||
@@ -3059,10 +3118,10 @@ function resolveStream<T: ReadableStream | $AsyncIterable<any, any, void>>(
|
||||
resolvedChunk.value = stream;
|
||||
resolvedChunk.reason = controller;
|
||||
if (resolveListeners !== null) {
|
||||
wakeChunk(resolveListeners, chunk.value, (chunk: any));
|
||||
wakeChunk(response, resolveListeners, chunk.value, (chunk: any));
|
||||
} else {
|
||||
if (__DEV__) {
|
||||
moveDebugInfoFromChunkToInnerValue(resolvedChunk, stream);
|
||||
processChunkDebugInfo(response, resolvedChunk, stream);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3202,7 +3261,12 @@ function startAsyncIterable<T>(
|
||||
initializedChunk.status = INITIALIZED;
|
||||
initializedChunk.value = {done: false, value: value};
|
||||
if (resolveListeners !== null) {
|
||||
wakeChunkIfInitialized(chunk, resolveListeners, rejectListeners);
|
||||
wakeChunkIfInitialized(
|
||||
response,
|
||||
chunk,
|
||||
resolveListeners,
|
||||
rejectListeners,
|
||||
);
|
||||
}
|
||||
}
|
||||
nextWriteIndex++;
|
||||
@@ -3392,88 +3456,6 @@ function resolveErrorDev(
|
||||
return error;
|
||||
}
|
||||
|
||||
function resolvePostponeProd(
|
||||
response: Response,
|
||||
id: number,
|
||||
streamState: StreamState,
|
||||
): void {
|
||||
if (__DEV__) {
|
||||
// These errors should never make it into a build so we don't need to encode them in codes.json
|
||||
// eslint-disable-next-line react-internal/prod-error-codes
|
||||
throw new Error(
|
||||
'resolvePostponeProd should never be called in development mode. Use resolvePostponeDev instead. This is a bug in React.',
|
||||
);
|
||||
}
|
||||
const error = new Error(
|
||||
'A Server Component was postponed. The reason is omitted in production' +
|
||||
' builds to avoid leaking sensitive details.',
|
||||
);
|
||||
const postponeInstance: Postpone = (error: any);
|
||||
postponeInstance.$$typeof = REACT_POSTPONE_TYPE;
|
||||
postponeInstance.stack = 'Error: ' + error.message;
|
||||
const chunks = response._chunks;
|
||||
const chunk = chunks.get(id);
|
||||
if (!chunk) {
|
||||
const newChunk: ErroredChunk<any> = createErrorChunk(
|
||||
response,
|
||||
postponeInstance,
|
||||
);
|
||||
chunks.set(id, newChunk);
|
||||
} else {
|
||||
triggerErrorOnChunk(response, chunk, postponeInstance);
|
||||
}
|
||||
}
|
||||
|
||||
function resolvePostponeDev(
|
||||
response: Response,
|
||||
id: number,
|
||||
reason: string,
|
||||
stack: ReactStackTrace,
|
||||
env: string,
|
||||
streamState: StreamState,
|
||||
): void {
|
||||
if (!__DEV__) {
|
||||
// These errors should never make it into a build so we don't need to encode them in codes.json
|
||||
// eslint-disable-next-line react-internal/prod-error-codes
|
||||
throw new Error(
|
||||
'resolvePostponeDev should never be called in production mode. Use resolvePostponeProd instead. This is a bug in React.',
|
||||
);
|
||||
}
|
||||
let postponeInstance: Postpone;
|
||||
const callStack = buildFakeCallStack(
|
||||
response,
|
||||
stack,
|
||||
env,
|
||||
false,
|
||||
// $FlowFixMe[incompatible-use]
|
||||
Error.bind(null, reason || ''),
|
||||
);
|
||||
const rootTask = response._debugRootTask;
|
||||
if (rootTask != null) {
|
||||
postponeInstance = rootTask.run(callStack);
|
||||
} else {
|
||||
postponeInstance = callStack();
|
||||
}
|
||||
postponeInstance.$$typeof = REACT_POSTPONE_TYPE;
|
||||
const chunks = response._chunks;
|
||||
const chunk = chunks.get(id);
|
||||
if (!chunk) {
|
||||
const newChunk: ErroredChunk<any> = createErrorChunk(
|
||||
response,
|
||||
postponeInstance,
|
||||
);
|
||||
if (__DEV__) {
|
||||
resolveChunkDebugInfo(response, streamState, newChunk);
|
||||
}
|
||||
chunks.set(id, newChunk);
|
||||
} else {
|
||||
if (__DEV__) {
|
||||
resolveChunkDebugInfo(response, streamState, chunk);
|
||||
}
|
||||
triggerErrorOnChunk(response, chunk, postponeInstance);
|
||||
}
|
||||
}
|
||||
|
||||
function resolveErrorModel(
|
||||
response: Response,
|
||||
id: number,
|
||||
@@ -4825,25 +4807,6 @@ function processFullStringRow(
|
||||
return;
|
||||
}
|
||||
// Fallthrough
|
||||
case 80 /* "P" */: {
|
||||
if (enablePostpone) {
|
||||
if (__DEV__) {
|
||||
const postponeInfo = JSON.parse(row);
|
||||
resolvePostponeDev(
|
||||
response,
|
||||
id,
|
||||
postponeInfo.reason,
|
||||
postponeInfo.stack,
|
||||
postponeInfo.env,
|
||||
streamState,
|
||||
);
|
||||
} else {
|
||||
resolvePostponeProd(response, id, streamState);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Fallthrough
|
||||
default: /* """ "{" "[" "t" "f" "n" "0" - "9" */ {
|
||||
if (__DEV__ && row === '') {
|
||||
resolveDebugHalt(response, id);
|
||||
@@ -4894,6 +4857,7 @@ export function processBinaryChunk(
|
||||
resolvedRowTag === 65 /* "A" */ ||
|
||||
resolvedRowTag === 79 /* "O" */ ||
|
||||
resolvedRowTag === 111 /* "o" */ ||
|
||||
resolvedRowTag === 98 /* "b" */ ||
|
||||
resolvedRowTag === 85 /* "U" */ ||
|
||||
resolvedRowTag === 83 /* "S" */ ||
|
||||
resolvedRowTag === 115 /* "s" */ ||
|
||||
@@ -4953,14 +4917,31 @@ export function processBinaryChunk(
|
||||
// We found the last chunk of the row
|
||||
const length = lastIdx - i;
|
||||
const lastChunk = new Uint8Array(chunk.buffer, offset, length);
|
||||
processFullBinaryRow(
|
||||
response,
|
||||
streamState,
|
||||
rowID,
|
||||
rowTag,
|
||||
buffer,
|
||||
lastChunk,
|
||||
);
|
||||
|
||||
// Check if this is a Uint8Array for a byte stream. We enqueue it
|
||||
// immediately but need to determine if we can use zero-copy or must copy.
|
||||
if (rowTag === 98 /* "b" */) {
|
||||
resolveBuffer(
|
||||
response,
|
||||
rowID,
|
||||
// If we're at the end of the RSC chunk, no more parsing will access
|
||||
// this buffer and we don't need to copy the chunk to allow detaching
|
||||
// the buffer, otherwise we need to copy.
|
||||
lastIdx === chunkLength ? lastChunk : lastChunk.slice(),
|
||||
streamState,
|
||||
);
|
||||
} else {
|
||||
// Process all other row types.
|
||||
processFullBinaryRow(
|
||||
response,
|
||||
streamState,
|
||||
rowID,
|
||||
rowTag,
|
||||
buffer,
|
||||
lastChunk,
|
||||
);
|
||||
}
|
||||
|
||||
// Reset state machine for a new row
|
||||
i = lastIdx;
|
||||
if (rowState === ROW_CHUNK_BY_NEWLINE) {
|
||||
@@ -4973,14 +4954,27 @@ export function processBinaryChunk(
|
||||
rowLength = 0;
|
||||
buffer.length = 0;
|
||||
} else {
|
||||
// The rest of this row is in a future chunk. We stash the rest of the
|
||||
// current chunk until we can process the full row.
|
||||
// The rest of this row is in a future chunk.
|
||||
const length = chunk.byteLength - i;
|
||||
const remainingSlice = new Uint8Array(chunk.buffer, offset, length);
|
||||
buffer.push(remainingSlice);
|
||||
// Update how many bytes we're still waiting for. If we're looking for
|
||||
// a newline, this doesn't hurt since we'll just ignore it.
|
||||
rowLength -= remainingSlice.byteLength;
|
||||
|
||||
// For byte streams, we can enqueue the partial row immediately without
|
||||
// copying since we're at the end of the RSC chunk and no more parsing
|
||||
// will access this buffer.
|
||||
if (rowTag === 98 /* "b" */) {
|
||||
// Update how many bytes we're still waiting for. We need to do this
|
||||
// before enqueueing, as enqueue will detach the buffer and byteLength
|
||||
// will become 0.
|
||||
rowLength -= remainingSlice.byteLength;
|
||||
resolveBuffer(response, rowID, remainingSlice, streamState);
|
||||
} else {
|
||||
// For other row types, stash the rest of the current chunk until we can
|
||||
// process the full row.
|
||||
buffer.push(remainingSlice);
|
||||
// Update how many bytes we're still waiting for. If we're looking for
|
||||
// a newline, this doesn't hurt since we'll just ignore it.
|
||||
rowLength -= remainingSlice.byteLength;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ describe('Bridge', () => {
|
||||
// Check that we're wired up correctly.
|
||||
bridge.send('reloadAppForProfiling');
|
||||
jest.runAllTimers();
|
||||
expect(wall.send).toHaveBeenCalledWith('reloadAppForProfiling');
|
||||
expect(wall.send).toHaveBeenCalledWith('reloadAppForProfiling', undefined);
|
||||
|
||||
// Should flush pending messages and then shut down.
|
||||
wall.send.mockClear();
|
||||
@@ -37,7 +37,7 @@ describe('Bridge', () => {
|
||||
jest.runAllTimers();
|
||||
expect(wall.send).toHaveBeenCalledWith('update', '1');
|
||||
expect(wall.send).toHaveBeenCalledWith('update', '2');
|
||||
expect(wall.send).toHaveBeenCalledWith('shutdown');
|
||||
expect(wall.send).toHaveBeenCalledWith('shutdown', undefined);
|
||||
expect(shutdownCallback).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Verify that the Bridge doesn't send messages after shutdown.
|
||||
|
||||
@@ -116,6 +116,16 @@ function shouldIgnoreConsoleErrorOrWarn(args) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const maybeError = args[1];
|
||||
if (
|
||||
maybeError !== null &&
|
||||
typeof maybeError === 'object' &&
|
||||
maybeError.message === 'Simulated error coming from DevTools'
|
||||
) {
|
||||
// Error from forcing an error boundary.
|
||||
return true;
|
||||
}
|
||||
|
||||
return global._ignoredErrorOrWarningMessages.some(errorOrWarningMessage => {
|
||||
return firstArg.indexOf(errorOrWarningMessage) !== -1;
|
||||
});
|
||||
|
||||
@@ -3283,8 +3283,6 @@ describe('Store', () => {
|
||||
<Suspense name="Outer" rects={null}>
|
||||
`);
|
||||
|
||||
console.log('...........................');
|
||||
|
||||
await actAsync(() => {
|
||||
resolve('loaded');
|
||||
});
|
||||
@@ -3300,4 +3298,100 @@ describe('Store', () => {
|
||||
<Suspense name="Inner" rects={[{x:1,y:2,width:6,height:1}]}>
|
||||
`);
|
||||
});
|
||||
|
||||
// @reactVersion >= 19.0
|
||||
it('measures rects when reconnecting', async () => {
|
||||
function Component({children, promise}) {
|
||||
let content = '';
|
||||
if (promise) {
|
||||
const value = readValue(promise);
|
||||
if (typeof value === 'string') {
|
||||
content += value;
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
{content}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function App({outer, inner}) {
|
||||
return (
|
||||
<React.Suspense
|
||||
name="outer"
|
||||
fallback={<Component key="outer-fallback">loading outer</Component>}>
|
||||
<Component key="outer-content" promise={outer}>
|
||||
outer content
|
||||
</Component>
|
||||
<React.Suspense
|
||||
name="inner"
|
||||
fallback={
|
||||
<Component key="inner-fallback">loading inner</Component>
|
||||
}>
|
||||
<Component key="inner-content" promise={inner}>
|
||||
inner content
|
||||
</Component>
|
||||
</React.Suspense>
|
||||
</React.Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
await actAsync(() => {
|
||||
render(<App outer={null} inner={null} />);
|
||||
});
|
||||
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <App>
|
||||
▾ <Suspense name="outer">
|
||||
<Component key="outer-content">
|
||||
▾ <Suspense name="inner">
|
||||
<Component key="inner-content">
|
||||
[suspense-root] rects={[{x:1,y:2,width:13,height:1}, {x:1,y:2,width:13,height:1}]}
|
||||
<Suspense name="outer" rects={[{x:1,y:2,width:13,height:1}, {x:1,y:2,width:13,height:1}]}>
|
||||
<Suspense name="inner" rects={[{x:1,y:2,width:13,height:1}]}>
|
||||
`);
|
||||
|
||||
let outerResolve;
|
||||
const outerPromise = new Promise(resolve => {
|
||||
outerResolve = resolve;
|
||||
});
|
||||
|
||||
let innerResolve;
|
||||
const innerPromise = new Promise(resolve => {
|
||||
innerResolve = resolve;
|
||||
});
|
||||
await actAsync(() => {
|
||||
render(<App outer={outerPromise} inner={innerPromise} />);
|
||||
});
|
||||
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <App>
|
||||
▾ <Suspense name="outer">
|
||||
<Component key="outer-fallback">
|
||||
[suspense-root] rects={[{x:1,y:2,width:13,height:1}, {x:1,y:2,width:13,height:1}, {x:1,y:2,width:13,height:1}]}
|
||||
<Suspense name="outer" rects={[{x:1,y:2,width:13,height:1}, {x:1,y:2,width:13,height:1}]}>
|
||||
<Suspense name="inner" rects={[{x:1,y:2,width:13,height:1}]}>
|
||||
`);
|
||||
|
||||
await actAsync(() => {
|
||||
outerResolve('..');
|
||||
innerResolve('.');
|
||||
});
|
||||
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <App>
|
||||
▾ <Suspense name="outer">
|
||||
<Component key="outer-content">
|
||||
▾ <Suspense name="inner">
|
||||
<Component key="inner-content">
|
||||
[suspense-root] rects={[{x:1,y:2,width:15,height:1}, {x:1,y:2,width:14,height:1}]}
|
||||
<Suspense name="outer" rects={[{x:1,y:2,width:15,height:1}, {x:1,y:2,width:14,height:1}]}>
|
||||
<Suspense name="inner" rects={[{x:1,y:2,width:14,height:1}]}>
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,12 +18,24 @@ import {
|
||||
describe('Store component filters', () => {
|
||||
let React;
|
||||
let Types;
|
||||
let agent;
|
||||
let bridge: FrontendBridge;
|
||||
let store: Store;
|
||||
let utils;
|
||||
let actAsync;
|
||||
|
||||
beforeAll(() => {
|
||||
// JSDDOM doesn't implement getClientRects so we're just faking one for testing purposes
|
||||
Element.prototype.getClientRects = function (this: Element) {
|
||||
const textContent = this.textContent;
|
||||
return [
|
||||
new DOMRect(1, 2, textContent.length, textContent.split('\n').length),
|
||||
];
|
||||
};
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
agent = global.agent;
|
||||
bridge = global.bridge;
|
||||
store = global.store;
|
||||
store.collapseNodesByDefault = false;
|
||||
@@ -156,9 +168,9 @@ describe('Store component filters', () => {
|
||||
<div>
|
||||
▾ <Suspense>
|
||||
<div>
|
||||
[suspense-root] rects={[]}
|
||||
<Suspense name="Unknown" rects={[]}>
|
||||
<Suspense name="Unknown" rects={[]}>
|
||||
[suspense-root] rects={[{x:1,y:2,width:7,height:1}, {x:1,y:2,width:6,height:1}]}
|
||||
<Suspense name="Unknown" rects={[{x:1,y:2,width:7,height:1}]}>
|
||||
<Suspense name="Unknown" rects={[{x:1,y:2,width:6,height:1}]}>
|
||||
`);
|
||||
|
||||
await actAsync(
|
||||
@@ -174,9 +186,9 @@ describe('Store component filters', () => {
|
||||
<div>
|
||||
▾ <Suspense>
|
||||
<div>
|
||||
[suspense-root] rects={[]}
|
||||
<Suspense name="Unknown" rects={[]}>
|
||||
<Suspense name="Unknown" rects={[]}>
|
||||
[suspense-root] rects={[{x:1,y:2,width:7,height:1}, {x:1,y:2,width:6,height:1}]}
|
||||
<Suspense name="Unknown" rects={[{x:1,y:2,width:7,height:1}]}>
|
||||
<Suspense name="Unknown" rects={[{x:1,y:2,width:6,height:1}]}>
|
||||
`);
|
||||
|
||||
await actAsync(
|
||||
@@ -192,9 +204,9 @@ describe('Store component filters', () => {
|
||||
<div>
|
||||
▾ <Suspense>
|
||||
<div>
|
||||
[suspense-root] rects={[]}
|
||||
<Suspense name="Unknown" rects={[]}>
|
||||
<Suspense name="Unknown" rects={[]}>
|
||||
[suspense-root] rects={[{x:1,y:2,width:7,height:1}, {x:1,y:2,width:6,height:1}]}
|
||||
<Suspense name="Unknown" rects={[{x:1,y:2,width:7,height:1}]}>
|
||||
<Suspense name="Unknown" rects={[{x:1,y:2,width:6,height:1}]}>
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -740,4 +752,180 @@ describe('Store component filters', () => {
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
// @reactVersion >= 16.6
|
||||
it('resets forced error and fallback states when filters are changed', async () => {
|
||||
store.componentFilters = [];
|
||||
class ErrorBoundary extends React.Component {
|
||||
state = {hasError: false};
|
||||
|
||||
static getDerivedStateFromError() {
|
||||
return {hasError: true};
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return <div key="did-error" />;
|
||||
}
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<>
|
||||
<React.Suspense fallback={<div key="loading" />}>
|
||||
<div key="suspense-content" />
|
||||
</React.Suspense>
|
||||
<ErrorBoundary>
|
||||
<div key="error-content" />
|
||||
</ErrorBoundary>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
await actAsync(async () => {
|
||||
render(<App />);
|
||||
});
|
||||
const rendererID = utils.getRendererID();
|
||||
await actAsync(() => {
|
||||
agent.overrideSuspense({
|
||||
id: store.getElementIDAtIndex(2),
|
||||
rendererID,
|
||||
forceFallback: true,
|
||||
});
|
||||
agent.overrideError({
|
||||
id: store.getElementIDAtIndex(4),
|
||||
rendererID,
|
||||
forceError: true,
|
||||
});
|
||||
});
|
||||
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <App>
|
||||
▾ <Suspense>
|
||||
<div key="loading">
|
||||
▾ <ErrorBoundary>
|
||||
<div key="did-error">
|
||||
[suspense-root] rects={[{x:1,y:2,width:0,height:1}, {x:1,y:2,width:0,height:1}, {x:1,y:2,width:0,height:1}]}
|
||||
<Suspense name="App" rects={[{x:1,y:2,width:0,height:1}]}>
|
||||
`);
|
||||
|
||||
await actAsync(() => {
|
||||
store.componentFilters = [
|
||||
utils.createElementTypeFilter(Types.ElementTypeFunction, true),
|
||||
];
|
||||
});
|
||||
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <Suspense>
|
||||
<div key="suspense-content">
|
||||
▾ <ErrorBoundary>
|
||||
<div key="error-content">
|
||||
[suspense-root] rects={[{x:1,y:2,width:0,height:1}, {x:1,y:2,width:0,height:1}]}
|
||||
<Suspense name="Unknown" rects={[{x:1,y:2,width:0,height:1}]}>
|
||||
`);
|
||||
});
|
||||
|
||||
// @reactVersion >= 19.2
|
||||
it('can filter by Activity slices', async () => {
|
||||
const Activity = React.Activity;
|
||||
const immediate = Promise.resolve(<div>Immediate</div>);
|
||||
|
||||
function Root({children}) {
|
||||
return (
|
||||
<Activity name="/" mode="visible">
|
||||
<React.Suspense fallback="Loading...">
|
||||
<h1>Root</h1>
|
||||
<main>{children}</main>
|
||||
</React.Suspense>
|
||||
</Activity>
|
||||
);
|
||||
}
|
||||
|
||||
function Layout({children}) {
|
||||
return (
|
||||
<Activity name="/blog" mode="visible">
|
||||
<h2>Blog</h2>
|
||||
<section>{children}</section>
|
||||
</Activity>
|
||||
);
|
||||
}
|
||||
|
||||
function Page() {
|
||||
return <React.Suspense fallback="Loading...">{immediate}</React.Suspense>;
|
||||
}
|
||||
|
||||
await actAsync(async () =>
|
||||
render(
|
||||
<Root>
|
||||
<Layout>
|
||||
<Page />
|
||||
</Layout>
|
||||
</Root>,
|
||||
),
|
||||
);
|
||||
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <Root>
|
||||
▾ <Activity name="/">
|
||||
▾ <Suspense>
|
||||
<h1>
|
||||
▾ <main>
|
||||
▾ <Layout>
|
||||
▾ <Activity name="/blog">
|
||||
<h2>
|
||||
▾ <section>
|
||||
▾ <Page>
|
||||
▾ <Suspense>
|
||||
<div>
|
||||
[suspense-root] rects={[{x:1,y:2,width:4,height:1}, {x:1,y:2,width:13,height:1}]}
|
||||
<Suspense name="Root" rects={[{x:1,y:2,width:4,height:1}, {x:1,y:2,width:13,height:1}]}>
|
||||
<Suspense name="Page" rects={[{x:1,y:2,width:9,height:1}]}>
|
||||
`);
|
||||
|
||||
await actAsync(
|
||||
async () =>
|
||||
(store.componentFilters = [
|
||||
utils.createActivitySliceFilter(store.getElementIDAtIndex(1)),
|
||||
]),
|
||||
);
|
||||
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <Activity name="/">
|
||||
▾ <Suspense>
|
||||
<h1>
|
||||
▾ <main>
|
||||
▾ <Layout>
|
||||
▸ <Activity name="/blog">
|
||||
[suspense-root] rects={[{x:1,y:2,width:4,height:1}, {x:1,y:2,width:13,height:1}]}
|
||||
<Suspense name="Unknown" rects={[{x:1,y:2,width:4,height:1}, {x:1,y:2,width:13,height:1}]}>
|
||||
<Suspense name="Page" rects={[{x:1,y:2,width:9,height:1}]}>
|
||||
`);
|
||||
|
||||
await actAsync(async () => (store.componentFilters = []));
|
||||
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <Root>
|
||||
▾ <Activity name="/">
|
||||
▾ <Suspense>
|
||||
<h1>
|
||||
▾ <main>
|
||||
▾ <Layout>
|
||||
▾ <Activity name="/blog">
|
||||
<h2>
|
||||
▾ <section>
|
||||
▾ <Page>
|
||||
▾ <Suspense>
|
||||
<div>
|
||||
[suspense-root] rects={[{x:1,y:2,width:4,height:1}, {x:1,y:2,width:13,height:1}]}
|
||||
<Suspense name="Root" rects={[{x:1,y:2,width:4,height:1}, {x:1,y:2,width:13,height:1}]}>
|
||||
<Suspense name="Page" rects={[{x:1,y:2,width:9,height:1}]}>
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -328,6 +328,19 @@ export function createLocationFilter(
|
||||
};
|
||||
}
|
||||
|
||||
export function createActivitySliceFilter(
|
||||
activityID: Element['id'],
|
||||
isEnabled: boolean = true,
|
||||
) {
|
||||
const Types = require('react-devtools-shared/src/frontend/types');
|
||||
return {
|
||||
type: Types.ComponentFilterActivitySlice,
|
||||
isEnabled,
|
||||
isValid: true,
|
||||
activityID: activityID,
|
||||
};
|
||||
}
|
||||
|
||||
export function getRendererID(): number {
|
||||
if (global.agent == null) {
|
||||
throw Error('Agent unavailable.');
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
ComponentFilterHOC,
|
||||
ComponentFilterLocation,
|
||||
ComponentFilterEnvironmentName,
|
||||
ComponentFilterActivitySlice,
|
||||
ElementTypeClass,
|
||||
ElementTypeContext,
|
||||
ElementTypeFunction,
|
||||
@@ -53,7 +54,7 @@ import {
|
||||
renamePathInObject,
|
||||
setInObject,
|
||||
utfEncodeString,
|
||||
filterOutLocationComponentFilters,
|
||||
persistableComponentFilters,
|
||||
} from 'react-devtools-shared/src/utils';
|
||||
import {
|
||||
formatConsoleArgumentsToSingleString,
|
||||
@@ -85,6 +86,7 @@ import {
|
||||
TREE_OPERATION_SET_SUBTREE_MODE,
|
||||
TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS,
|
||||
TREE_OPERATION_UPDATE_TREE_BASE_DURATION,
|
||||
TREE_OPERATION_APPLIED_ACTIVITY_SLICE_CHANGE,
|
||||
SUSPENSE_TREE_OPERATION_ADD,
|
||||
SUSPENSE_TREE_OPERATION_REMOVE,
|
||||
SUSPENSE_TREE_OPERATION_REORDER_CHILDREN,
|
||||
@@ -170,6 +172,7 @@ import type {
|
||||
} from '../types';
|
||||
import type {
|
||||
ComponentFilter,
|
||||
ActivitySliceFilter,
|
||||
ElementType,
|
||||
Plugins,
|
||||
} from 'react-devtools-shared/src/frontend/types';
|
||||
@@ -301,6 +304,7 @@ type SuspenseNode = {
|
||||
rects: null | Array<Rect>, // The bounding rects of content children.
|
||||
suspendedBy: Map<ReactIOInfo, Set<DevToolsInstance>>, // Tracks which data we're suspended by and the children that suspend it.
|
||||
environments: Map<string, number>, // Tracks the Flight environment names that suspended this. I.e. if the server blocked this.
|
||||
endTime: number, // Track a short cut to the maximum end time value within the suspendedBy set.
|
||||
// Track whether any of the items in suspendedBy are unique this this Suspense boundaries or if they're all
|
||||
// also in the parent sets. This determine whether this could contribute in the loading sequence.
|
||||
hasUniqueSuspenders: boolean,
|
||||
@@ -330,6 +334,7 @@ function createSuspenseNode(
|
||||
rects: null,
|
||||
suspendedBy: new Map(),
|
||||
environments: new Map(),
|
||||
endTime: 0,
|
||||
hasUniqueSuspenders: false,
|
||||
hasUnknownSuspenders: false,
|
||||
});
|
||||
@@ -866,6 +871,9 @@ const idToDevToolsInstanceMap: Map<
|
||||
FiberInstance | VirtualInstance,
|
||||
> = new Map();
|
||||
|
||||
let focusedActivityID: null | FiberInstance['id'] = null;
|
||||
let focusedActivity: null | Fiber = null;
|
||||
|
||||
const idToSuspenseNodeMap: Map<FiberInstance['id'], SuspenseNode> = new Map();
|
||||
|
||||
// Map of canonical HostInstances to the nearest parent DevToolsInstance.
|
||||
@@ -1433,16 +1441,25 @@ export function attach(
|
||||
const hideElementsWithPaths: Set<RegExp> = new Set();
|
||||
const hideElementsWithTypes: Set<ElementType> = new Set();
|
||||
const hideElementsWithEnvs: Set<string> = new Set();
|
||||
let isInFocusedActivity: boolean = true;
|
||||
|
||||
// Highlight updates
|
||||
let traceUpdatesEnabled: boolean = false;
|
||||
const traceUpdatesForNodes: Set<HostInstance> = new Set();
|
||||
|
||||
function applyComponentFilters(componentFilters: Array<ComponentFilter>) {
|
||||
function applyComponentFilters(
|
||||
componentFilters: Array<ComponentFilter>,
|
||||
nextActivitySlice: null | Fiber,
|
||||
) {
|
||||
hideElementsWithTypes.clear();
|
||||
hideElementsWithDisplayNames.clear();
|
||||
hideElementsWithPaths.clear();
|
||||
hideElementsWithEnvs.clear();
|
||||
const previousFocusedActivityID = focusedActivityID;
|
||||
focusedActivityID = null;
|
||||
focusedActivity = null;
|
||||
// Consider everything in the slice by default
|
||||
isInFocusedActivity = true;
|
||||
|
||||
componentFilters.forEach(componentFilter => {
|
||||
if (!componentFilter.isEnabled) {
|
||||
@@ -1471,6 +1488,25 @@ export function attach(
|
||||
case ComponentFilterEnvironmentName:
|
||||
hideElementsWithEnvs.add(componentFilter.value);
|
||||
break;
|
||||
case ComponentFilterActivitySlice:
|
||||
if (
|
||||
nextActivitySlice !== null &&
|
||||
nextActivitySlice.tag === ActivityComponent
|
||||
) {
|
||||
focusedActivity = nextActivitySlice;
|
||||
isInFocusedActivity = false;
|
||||
if (componentFilter.rendererID !== rendererID) {
|
||||
// We filtered an Activity from another renderer.
|
||||
// We need to restore the instance ID since we won't be mounting it
|
||||
// in this renderer.
|
||||
focusedActivityID = previousFocusedActivityID;
|
||||
}
|
||||
} else {
|
||||
// We're not filtering by activity slice after all.
|
||||
// Don't mark the filter as disabled here.
|
||||
// Otherwise updateComponentFilters() will think no enabled filter was changed.
|
||||
}
|
||||
break;
|
||||
default:
|
||||
console.warn(
|
||||
`Invalid component filter type "${componentFilter.type}"`,
|
||||
@@ -1484,11 +1520,9 @@ export function attach(
|
||||
// because they are stored in localStorage within the context of the extension.
|
||||
// Instead it relies on the extension to pass filters through.
|
||||
if (window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ != null) {
|
||||
const componentFiltersWithoutLocationBasedOnes =
|
||||
filterOutLocationComponentFilters(
|
||||
window.__REACT_DEVTOOLS_COMPONENT_FILTERS__,
|
||||
);
|
||||
applyComponentFilters(componentFiltersWithoutLocationBasedOnes);
|
||||
const restoredComponentFilters: Array<ComponentFilter> =
|
||||
persistableComponentFilters(window.__REACT_DEVTOOLS_COMPONENT_FILTERS__);
|
||||
applyComponentFilters(restoredComponentFilters, null);
|
||||
} else {
|
||||
// Unfortunately this feature is not expected to work for React Native for now.
|
||||
// It would be annoying for us to spam YellowBox warnings with unactionable stuff,
|
||||
@@ -1496,7 +1530,7 @@ export function attach(
|
||||
//console.warn('⚛ DevTools: Could not locate saved component filters');
|
||||
|
||||
// Fallback to assuming the default filters in this case.
|
||||
applyComponentFilters(getDefaultComponentFilters());
|
||||
applyComponentFilters(getDefaultComponentFilters(), null);
|
||||
}
|
||||
|
||||
// If necessary, we can revisit optimizing this operation.
|
||||
@@ -1510,6 +1544,27 @@ export function attach(
|
||||
throw Error('Cannot modify filter preferences while profiling');
|
||||
}
|
||||
|
||||
const previousForcedFallbacks =
|
||||
forceFallbackForFibers.size > 0 ? new Set(forceFallbackForFibers) : null;
|
||||
const previousForcedErrors =
|
||||
forceErrorForFibers.size > 0 ? new Map(forceErrorForFibers) : null;
|
||||
|
||||
// The ID will be based on the old tree. We need to find the Fiber based on
|
||||
// that ID before we unmount everything. We set the activity slice ID once
|
||||
// we mount it again.
|
||||
let nextFocusedActivity: null | Fiber = null;
|
||||
let focusedActivityFilter: null | ActivitySliceFilter = null;
|
||||
for (let i = 0; i < componentFilters.length; i++) {
|
||||
const filter = componentFilters[i];
|
||||
if (filter.type === ComponentFilterActivitySlice && filter.isEnabled) {
|
||||
focusedActivityFilter = filter;
|
||||
const instance = idToDevToolsInstanceMap.get(filter.activityID);
|
||||
if (instance !== undefined && instance.kind === FIBER_INSTANCE) {
|
||||
nextFocusedActivity = instance.data;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively unmount all roots.
|
||||
hook.getFiberRoots(rendererID).forEach(root => {
|
||||
const rootInstance = rootToFiberInstanceMap.get(root);
|
||||
@@ -1521,15 +1576,59 @@ export function attach(
|
||||
currentRoot = rootInstance;
|
||||
unmountInstanceRecursively(rootInstance);
|
||||
rootToFiberInstanceMap.delete(root);
|
||||
flushPendingEvents();
|
||||
currentRoot = (null: any);
|
||||
});
|
||||
|
||||
applyComponentFilters(componentFilters);
|
||||
if (
|
||||
nextFocusedActivity !== focusedActivity &&
|
||||
(focusedActivityFilter === null ||
|
||||
focusedActivityFilter.rendererID === rendererID)
|
||||
) {
|
||||
// When we find the applied instance during mount we will send the actual ID.
|
||||
// Otherwise 0 will indicate that we unfocused the activity slice.
|
||||
pushOperation(TREE_OPERATION_APPLIED_ACTIVITY_SLICE_CHANGE);
|
||||
pushOperation(0);
|
||||
}
|
||||
applyComponentFilters(componentFilters, nextFocusedActivity);
|
||||
|
||||
// Reset pseudo counters so that new path selections will be persisted.
|
||||
rootDisplayNameCounter.clear();
|
||||
|
||||
// We just cleared all the forced states. Schedule updates on the affected Fibers
|
||||
// so that we get their initial states again according to the new filters.
|
||||
if (typeof scheduleUpdate === 'function') {
|
||||
if (previousForcedFallbacks !== null) {
|
||||
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
|
||||
for (const fiber of previousForcedFallbacks) {
|
||||
if (typeof scheduleRetry === 'function') {
|
||||
scheduleRetry(fiber);
|
||||
} else {
|
||||
scheduleUpdate(fiber);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (
|
||||
previousForcedErrors !== null &&
|
||||
typeof setErrorHandler === 'function'
|
||||
) {
|
||||
// Unlike for Suspense, disabling the forced error state requires setting
|
||||
// the status to false first. `shouldErrorFiberAccordingToMap` will clear
|
||||
// the Fibers later.
|
||||
setErrorHandler(shouldErrorFiberAccordingToMap);
|
||||
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
|
||||
for (const [fiber, shouldError] of previousForcedErrors) {
|
||||
forceErrorForFibers.set(fiber, false);
|
||||
if (shouldError) {
|
||||
if (typeof scheduleRetry === 'function') {
|
||||
scheduleRetry(fiber);
|
||||
} else {
|
||||
scheduleUpdate(fiber);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively re-mount all roots with new filter criteria applied.
|
||||
hook.getFiberRoots(rendererID).forEach(root => {
|
||||
const current = root.current;
|
||||
@@ -1546,10 +1645,16 @@ export function attach(
|
||||
currentRoot = newRoot;
|
||||
setRootPseudoKey(currentRoot.id, root.current);
|
||||
mountFiberRecursively(root.current, false);
|
||||
flushPendingEvents();
|
||||
currentRoot = (null: any);
|
||||
});
|
||||
|
||||
// We need to write back the new ID for the focused Fiber.
|
||||
// Otherwise subsequent filter applications will try to focus based on the old ID.
|
||||
// This is also relevant to filter across renderers.
|
||||
if (focusedActivityFilter !== null && focusedActivityID !== null) {
|
||||
focusedActivityFilter.activityID = focusedActivityID;
|
||||
}
|
||||
|
||||
flushPendingEvents();
|
||||
|
||||
needsToFlushComponentLogs = false;
|
||||
@@ -1579,6 +1684,10 @@ export function attach(
|
||||
data: ReactComponentInfo,
|
||||
secondaryEnv: null | string,
|
||||
): boolean {
|
||||
if (!isInFocusedActivity) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// For purposes of filtering Server Components are always Function Components.
|
||||
// Environment will be used to filter Server vs Client.
|
||||
// Technically they can be forwardRef and memo too but those filters will go away
|
||||
@@ -1614,6 +1723,11 @@ export function attach(
|
||||
function shouldFilterFiber(fiber: Fiber): boolean {
|
||||
const {tag, type, key} = fiber;
|
||||
|
||||
// It is never valid to filter the root element.
|
||||
if (tag !== HostRoot && !isInFocusedActivity) {
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (tag) {
|
||||
case DehydratedSuspenseComponent:
|
||||
// TODO: ideally we would show dehydrated Suspense immediately.
|
||||
@@ -2043,7 +2157,6 @@ export function attach(
|
||||
let pendingOperationsQueue: Array<OperationsArray> | null = [];
|
||||
const pendingStringTable: Map<string, StringTableEntry> = new Map();
|
||||
let pendingStringTableLength: number = 0;
|
||||
let pendingUnmountedRootID: FiberInstance['id'] | null = null;
|
||||
|
||||
function pushOperation(op: number): void {
|
||||
if (__DEV__) {
|
||||
@@ -2071,8 +2184,7 @@ export function attach(
|
||||
pendingOperations.length === 0 &&
|
||||
pendingRealUnmountedIDs.length === 0 &&
|
||||
pendingRealUnmountedSuspenseIDs.length === 0 &&
|
||||
pendingSuspenderChanges.size === 0 &&
|
||||
pendingUnmountedRootID === null
|
||||
pendingSuspenderChanges.size === 0
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2134,9 +2246,7 @@ export function attach(
|
||||
return;
|
||||
}
|
||||
|
||||
const numUnmountIDs =
|
||||
pendingRealUnmountedIDs.length +
|
||||
(pendingUnmountedRootID === null ? 0 : 1);
|
||||
const numUnmountIDs = pendingRealUnmountedIDs.length;
|
||||
const numUnmountSuspenseIDs = pendingRealUnmountedSuspenseIDs.length;
|
||||
const numSuspenderChanges = pendingSuspenderChanges.size;
|
||||
|
||||
@@ -2156,8 +2266,8 @@ export function attach(
|
||||
// Regular operations
|
||||
pendingOperations.length +
|
||||
// All suspender changes are batched in a single message.
|
||||
// [SUSPENSE_TREE_OPERATION_SUSPENDERS, suspenderChangesLength, ...[id, hasUniqueSuspenders, isSuspended]]
|
||||
(numSuspenderChanges > 0 ? 2 + numSuspenderChanges * 3 : 0),
|
||||
// [SUSPENSE_TREE_OPERATION_SUSPENDERS, suspenderChangesLength, ...[id, hasUniqueSuspenders, endTime, isSuspended]]
|
||||
(numSuspenderChanges > 0 ? 2 + numSuspenderChanges * 4 : 0),
|
||||
);
|
||||
|
||||
// Identify which renderer this update is coming from.
|
||||
@@ -2214,11 +2324,6 @@ export function attach(
|
||||
for (let j = 0; j < pendingRealUnmountedIDs.length; j++) {
|
||||
operations[i++] = pendingRealUnmountedIDs[j];
|
||||
}
|
||||
// The root ID should always be unmounted last.
|
||||
if (pendingUnmountedRootID !== null) {
|
||||
operations[i] = pendingUnmountedRootID;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
// Fill in pending operations.
|
||||
@@ -2242,6 +2347,7 @@ export function attach(
|
||||
}
|
||||
operations[i++] = fiberIdWithChanges;
|
||||
operations[i++] = suspense.hasUniqueSuspenders ? 1 : 0;
|
||||
operations[i++] = Math.round(suspense.endTime * 1000);
|
||||
const instance = suspense.instance;
|
||||
const isSuspended =
|
||||
// TODO: Track if other SuspenseNode like SuspenseList rows are suspended.
|
||||
@@ -2265,7 +2371,6 @@ export function attach(
|
||||
pendingRealUnmountedIDs.length = 0;
|
||||
pendingRealUnmountedSuspenseIDs.length = 0;
|
||||
pendingSuspenderChanges.clear();
|
||||
pendingUnmountedRootID = null;
|
||||
pendingStringTable.clear();
|
||||
pendingStringTableLength = 0;
|
||||
}
|
||||
@@ -2469,6 +2574,17 @@ export function attach(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const suspenseNode = fiberInstance.suspenseNode;
|
||||
if (suspenseNode !== null && fiber.memoizedState === null) {
|
||||
// We're reconnecting an unsuspended Suspense. Measure to see if anything changed.
|
||||
const prevRects = suspenseNode.rects;
|
||||
const nextRects = measureInstance(fiberInstance);
|
||||
if (!areEqualRects(prevRects, nextRects)) {
|
||||
suspenseNode.rects = nextRects;
|
||||
recordSuspenseResize(suspenseNode);
|
||||
}
|
||||
}
|
||||
|
||||
const {key} = fiber;
|
||||
const displayName = getDisplayNameForFiber(fiber);
|
||||
const elementType = getElementTypeForFiber(fiber);
|
||||
@@ -2740,7 +2856,6 @@ export function attach(
|
||||
// Already disconnected.
|
||||
return;
|
||||
}
|
||||
const fiber = fiberInstance.data;
|
||||
|
||||
if (trackedPathMatchInstance === fiberInstance) {
|
||||
// We're in the process of trying to restore previous selection.
|
||||
@@ -2750,17 +2865,7 @@ export function attach(
|
||||
}
|
||||
|
||||
const id = fiberInstance.id;
|
||||
const isRoot = fiber.tag === HostRoot;
|
||||
if (isRoot) {
|
||||
// Roots must be removed only after all children have been removed.
|
||||
// So we track it separately.
|
||||
pendingUnmountedRootID = id;
|
||||
} else {
|
||||
// To maintain child-first ordering,
|
||||
// we'll push it into one of these queues,
|
||||
// and later arrange them in the correct order.
|
||||
pendingRealUnmountedIDs.push(id);
|
||||
}
|
||||
pendingRealUnmountedIDs.push(id);
|
||||
}
|
||||
|
||||
function recordSuspenseResize(suspenseNode: SuspenseNode): void {
|
||||
@@ -2912,12 +3017,19 @@ export function attach(
|
||||
// like owner instances to link down into the tree.
|
||||
if (!suspendedBySet.has(parentInstance)) {
|
||||
suspendedBySet.add(parentInstance);
|
||||
const virtualEndTime = getVirtualEndTime(ioInfo);
|
||||
if (
|
||||
!parentSuspenseNode.hasUniqueSuspenders &&
|
||||
!ioExistsInSuspenseAncestor(parentSuspenseNode, ioInfo)
|
||||
) {
|
||||
// This didn't exist in the parent before, so let's mark this boundary as having a unique suspender.
|
||||
parentSuspenseNode.hasUniqueSuspenders = true;
|
||||
if (parentSuspenseNode.endTime < virtualEndTime) {
|
||||
parentSuspenseNode.endTime = virtualEndTime;
|
||||
}
|
||||
recordSuspenseSuspenders(parentSuspenseNode);
|
||||
} else if (parentSuspenseNode.endTime < virtualEndTime) {
|
||||
parentSuspenseNode.endTime = virtualEndTime;
|
||||
recordSuspenseSuspenders(parentSuspenseNode);
|
||||
}
|
||||
}
|
||||
@@ -2979,6 +3091,26 @@ export function attach(
|
||||
}
|
||||
}
|
||||
|
||||
function getVirtualEndTime(ioInfo: ReactIOInfo): number {
|
||||
if (ioInfo.env != null) {
|
||||
// Sort client side content first so that scripts and streams don't
|
||||
// cover up the effect of server time.
|
||||
return ioInfo.end + 1000000;
|
||||
}
|
||||
return ioInfo.end;
|
||||
}
|
||||
|
||||
function computeEndTime(suspenseNode: SuspenseNode) {
|
||||
let maxEndTime = 0;
|
||||
suspenseNode.suspendedBy.forEach((set, ioInfo) => {
|
||||
const virtualEndTime = getVirtualEndTime(ioInfo);
|
||||
if (virtualEndTime > maxEndTime) {
|
||||
maxEndTime = virtualEndTime;
|
||||
}
|
||||
});
|
||||
return maxEndTime;
|
||||
}
|
||||
|
||||
function removePreviousSuspendedBy(
|
||||
instance: DevToolsInstance,
|
||||
previousSuspendedBy: null | Array<ReactAsyncInfo>,
|
||||
@@ -2996,6 +3128,7 @@ export function attach(
|
||||
if (previousSuspendedBy !== null && suspenseNode !== null) {
|
||||
const nextSuspendedBy = instance.suspendedBy;
|
||||
let changedEnvironment = false;
|
||||
let mayHaveChangedEndTime = false;
|
||||
for (let i = 0; i < previousSuspendedBy.length; i++) {
|
||||
const asyncInfo = previousSuspendedBy[i];
|
||||
if (
|
||||
@@ -3009,6 +3142,11 @@ export function attach(
|
||||
const ioInfo = asyncInfo.awaited;
|
||||
const suspendedBySet = suspenseNode.suspendedBy.get(ioInfo);
|
||||
|
||||
if (suspenseNode.endTime === getVirtualEndTime(ioInfo)) {
|
||||
// This may be the only remaining entry at this end time. Recompute the end time.
|
||||
mayHaveChangedEndTime = true;
|
||||
}
|
||||
|
||||
if (
|
||||
suspendedBySet === undefined ||
|
||||
!suspendedBySet.delete(instance)
|
||||
@@ -3066,7 +3204,11 @@ export function attach(
|
||||
}
|
||||
}
|
||||
}
|
||||
if (changedEnvironment) {
|
||||
const newEndTime = mayHaveChangedEndTime
|
||||
? computeEndTime(suspenseNode)
|
||||
: suspenseNode.endTime;
|
||||
if (changedEnvironment || newEndTime !== suspenseNode.endTime) {
|
||||
suspenseNode.endTime = newEndTime;
|
||||
recordSuspenseSuspenders(suspenseNode);
|
||||
}
|
||||
}
|
||||
@@ -3940,11 +4082,23 @@ export function attach(
|
||||
fiber: Fiber,
|
||||
traceNearestHostComponentUpdate: boolean,
|
||||
): void {
|
||||
const isFocusedActivityEntry =
|
||||
focusedActivity !== null &&
|
||||
(fiber === focusedActivity || fiber.alternate === focusedActivity);
|
||||
if (isFocusedActivityEntry) {
|
||||
isInFocusedActivity = true;
|
||||
}
|
||||
|
||||
const shouldIncludeInTree = !shouldFilterFiber(fiber);
|
||||
let newInstance = null;
|
||||
let newSuspenseNode = null;
|
||||
if (shouldIncludeInTree) {
|
||||
newInstance = recordMount(fiber, reconcilingParent);
|
||||
if (isFocusedActivityEntry) {
|
||||
focusedActivityID = newInstance.id;
|
||||
pushOperation(TREE_OPERATION_APPLIED_ACTIVITY_SLICE_CHANGE);
|
||||
pushOperation(newInstance.id);
|
||||
}
|
||||
if (fiber.tag === SuspenseComponent || fiber.tag === HostRoot) {
|
||||
newSuspenseNode = createSuspenseNode(newInstance);
|
||||
// Measure this Suspense node. In general we shouldn't do this until we have
|
||||
@@ -4060,6 +4214,7 @@ export function attach(
|
||||
const stashedSuspenseParent = reconcilingParentSuspenseNode;
|
||||
const stashedSuspensePrevious = previouslyReconciledSiblingSuspenseNode;
|
||||
const stashedSuspenseRemaining = remainingReconcilingChildrenSuspenseNodes;
|
||||
const stashedIsInActivitySlice = isInFocusedActivity;
|
||||
if (newInstance !== null) {
|
||||
// Push a new DevTools instance parent while reconciling this subtree.
|
||||
reconcilingParent = newInstance;
|
||||
@@ -4073,6 +4228,17 @@ export function attach(
|
||||
remainingReconcilingChildrenSuspenseNodes = null;
|
||||
shouldPopSuspenseNode = true;
|
||||
}
|
||||
if (
|
||||
!isFocusedActivityEntry &&
|
||||
focusedActivity !== null &&
|
||||
fiber.tag === ActivityComponent
|
||||
) {
|
||||
// We're not filtering how Activity within the focused activity.
|
||||
// We cut of the bottom in the Frontend if we want to just show the
|
||||
// Activity slice instead of all Activity descendants.
|
||||
// The filtering in the backend only happens because filtering out
|
||||
// everything above the focused Activity is hard to implement in the frontend.
|
||||
}
|
||||
try {
|
||||
if (traceUpdatesEnabled) {
|
||||
if (traceNearestHostComponentUpdate) {
|
||||
@@ -4200,6 +4366,7 @@ export function attach(
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
isInFocusedActivity = stashedIsInActivitySlice;
|
||||
if (newInstance !== null) {
|
||||
reconcilingParent = stashedParent;
|
||||
previouslyReconciledSibling = stashedPrevious;
|
||||
@@ -4231,6 +4398,7 @@ export function attach(
|
||||
const stashedSuspenseParent = reconcilingParentSuspenseNode;
|
||||
const stashedSuspensePrevious = previouslyReconciledSiblingSuspenseNode;
|
||||
const stashedSuspenseRemaining = remainingReconcilingChildrenSuspenseNodes;
|
||||
const stashedIsInActivitySlice = isInFocusedActivity;
|
||||
const previousSuspendedBy = instance.suspendedBy;
|
||||
// Push a new DevTools instance parent while reconciling this subtree.
|
||||
reconcilingParent = instance;
|
||||
@@ -4249,6 +4417,19 @@ export function attach(
|
||||
shouldPopSuspenseNode = true;
|
||||
}
|
||||
|
||||
if (focusedActivity !== null) {
|
||||
if (instance.id === focusedActivityID) {
|
||||
isInFocusedActivity = true;
|
||||
} else if (
|
||||
instance.kind === FIBER_INSTANCE &&
|
||||
instance.data !== null &&
|
||||
instance.data.tag === ActivityComponent
|
||||
) {
|
||||
// Filtering nested Activity components inside the focused activity
|
||||
// is done in the frontend.
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Unmount the remaining set.
|
||||
if (
|
||||
@@ -4299,6 +4480,7 @@ export function attach(
|
||||
previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious;
|
||||
remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining;
|
||||
}
|
||||
isInFocusedActivity = stashedIsInActivitySlice;
|
||||
}
|
||||
if (instance.kind === FIBER_INSTANCE) {
|
||||
recordUnmount(instance);
|
||||
@@ -4979,6 +5161,7 @@ export function attach(
|
||||
const stashedSuspenseParent = reconcilingParentSuspenseNode;
|
||||
const stashedSuspensePrevious = previouslyReconciledSiblingSuspenseNode;
|
||||
const stashedSuspenseRemaining = remainingReconcilingChildrenSuspenseNodes;
|
||||
const stashedIsInActivitySlice = isInFocusedActivity;
|
||||
let updateFlags = NoUpdate;
|
||||
let shouldMeasureSuspenseNode = false;
|
||||
let shouldPopSuspenseNode = false;
|
||||
@@ -5018,6 +5201,15 @@ export function attach(
|
||||
shouldMeasureSuspenseNode = true;
|
||||
shouldPopSuspenseNode = true;
|
||||
}
|
||||
|
||||
if (focusedActivity !== null) {
|
||||
if (fiberInstance.id === focusedActivityID) {
|
||||
isInFocusedActivity = true;
|
||||
} else if (nextFiber.tag === ActivityComponent) {
|
||||
// Filtering nested Activity components inside the focused activity
|
||||
// is done in the frontend.
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
trackDebugInfoFromLazyType(nextFiber);
|
||||
@@ -5442,6 +5634,7 @@ export function attach(
|
||||
previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious;
|
||||
remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining;
|
||||
}
|
||||
isInFocusedActivity = stashedIsInActivitySlice;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5556,11 +5749,12 @@ export function attach(
|
||||
|
||||
mountFiberRecursively(root.current, false);
|
||||
|
||||
flushPendingEvents();
|
||||
|
||||
needsToFlushComponentLogs = false;
|
||||
currentRoot = (null: any);
|
||||
});
|
||||
|
||||
flushPendingEvents();
|
||||
|
||||
needsToFlushComponentLogs = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,66 @@ export default function setupHighlighter(
|
||||
bridge.addListener('shutdown', stopInspectingHost);
|
||||
bridge.addListener('startInspectingHost', startInspectingHost);
|
||||
bridge.addListener('stopInspectingHost', stopInspectingHost);
|
||||
bridge.addListener('scrollTo', scrollDocumentTo);
|
||||
bridge.addListener('requestScrollPosition', sendScroll);
|
||||
|
||||
let applyingScroll = false;
|
||||
|
||||
function scrollDocumentTo({
|
||||
left,
|
||||
top,
|
||||
right,
|
||||
bottom,
|
||||
}: {
|
||||
left: number,
|
||||
top: number,
|
||||
right: number,
|
||||
bottom: number,
|
||||
}) {
|
||||
if (
|
||||
left === Math.round(window.scrollX) &&
|
||||
top === Math.round(window.scrollY)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
applyingScroll = true;
|
||||
window.scrollTo({
|
||||
top: top,
|
||||
left: left,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}
|
||||
|
||||
let scrollTimer = null;
|
||||
function sendScroll() {
|
||||
if (scrollTimer) {
|
||||
clearTimeout(scrollTimer);
|
||||
scrollTimer = null;
|
||||
}
|
||||
if (applyingScroll) {
|
||||
return;
|
||||
}
|
||||
const left = window.scrollX;
|
||||
const top = window.scrollY;
|
||||
const right = left + window.innerWidth;
|
||||
const bottom = top + window.innerHeight;
|
||||
bridge.send('scrollTo', {left, top, right, bottom});
|
||||
}
|
||||
|
||||
function scrollEnd() {
|
||||
// Upon scrollend send it immediately.
|
||||
sendScroll();
|
||||
applyingScroll = false;
|
||||
}
|
||||
|
||||
document.addEventListener('scroll', () => {
|
||||
if (!scrollTimer) {
|
||||
// Periodically synchronize the scroll while scrolling.
|
||||
scrollTimer = setTimeout(sendScroll, 400);
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('scrollend', scrollEnd);
|
||||
|
||||
function startInspectingHost(onlySuspenseNodes: boolean) {
|
||||
inspectOnlySuspenseNodes = onlySuspenseNodes;
|
||||
|
||||
6
packages/react-devtools-shared/src/bridge.js
vendored
6
packages/react-devtools-shared/src/bridge.js
vendored
@@ -217,6 +217,7 @@ export type BackendEvents = {
|
||||
selectElement: [number],
|
||||
shutdown: [],
|
||||
stopInspectingHost: [boolean],
|
||||
scrollTo: [{left: number, top: number, right: number, bottom: number}],
|
||||
syncSelectionToBuiltinElementsPanel: [],
|
||||
unsupportedRendererVersion: [],
|
||||
|
||||
@@ -270,6 +271,8 @@ type FrontendEvents = {
|
||||
startProfiling: [StartProfilingParams],
|
||||
stopInspectingHost: [],
|
||||
scrollToHostInstance: [ScrollToHostInstance],
|
||||
scrollTo: [{left: number, top: number, right: number, bottom: number}],
|
||||
requestScrollPosition: [],
|
||||
stopProfiling: [],
|
||||
storeAsGlobal: [StoreAsGlobalParams],
|
||||
updateComponentFilters: [Array<ComponentFilter>],
|
||||
@@ -416,7 +419,8 @@ class Bridge<
|
||||
try {
|
||||
if (this._messageQueue.length) {
|
||||
for (let i = 0; i < this._messageQueue.length; i += 2) {
|
||||
this._wall.send(this._messageQueue[i], ...this._messageQueue[i + 1]);
|
||||
// This only supports one argument in practice but the types suggests it should support multiple.
|
||||
this._wall.send(this._messageQueue[i], this._messageQueue[i + 1][0]);
|
||||
}
|
||||
this._messageQueue.length = 0;
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ export const SUSPENSE_TREE_OPERATION_REMOVE = 9;
|
||||
export const SUSPENSE_TREE_OPERATION_REORDER_CHILDREN = 10;
|
||||
export const SUSPENSE_TREE_OPERATION_RESIZE = 11;
|
||||
export const SUSPENSE_TREE_OPERATION_SUSPENDERS = 12;
|
||||
export const TREE_OPERATION_APPLIED_ACTIVITY_SLICE_CHANGE = 13;
|
||||
|
||||
export const PROFILING_FLAG_BASIC_SUPPORT /*. */ = 0b001;
|
||||
export const PROFILING_FLAG_TIMELINE_SUPPORT /* */ = 0b010;
|
||||
|
||||
147
packages/react-devtools-shared/src/devtools/store.js
vendored
147
packages/react-devtools-shared/src/devtools/store.js
vendored
@@ -21,13 +21,18 @@ import {
|
||||
TREE_OPERATION_SET_SUBTREE_MODE,
|
||||
TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS,
|
||||
TREE_OPERATION_UPDATE_TREE_BASE_DURATION,
|
||||
TREE_OPERATION_APPLIED_ACTIVITY_SLICE_CHANGE,
|
||||
SUSPENSE_TREE_OPERATION_ADD,
|
||||
SUSPENSE_TREE_OPERATION_REMOVE,
|
||||
SUSPENSE_TREE_OPERATION_REORDER_CHILDREN,
|
||||
SUSPENSE_TREE_OPERATION_RESIZE,
|
||||
SUSPENSE_TREE_OPERATION_SUSPENDERS,
|
||||
} from '../constants';
|
||||
import {ElementTypeRoot} from '../frontend/types';
|
||||
import {
|
||||
ElementTypeRoot,
|
||||
ElementTypeActivity,
|
||||
ComponentFilterActivitySlice,
|
||||
} from '../frontend/types';
|
||||
import {
|
||||
getSavedComponentFilters,
|
||||
setSavedComponentFilters,
|
||||
@@ -144,7 +149,13 @@ export default class Store extends EventEmitter<{
|
||||
hookSettings: [$ReadOnly<DevToolsHookSettings>],
|
||||
hostInstanceSelected: [Element['id']],
|
||||
settingsUpdated: [$ReadOnly<DevToolsHookSettings>],
|
||||
mutated: [[Array<Element['id']>, Map<Element['id'], Element['id']>]],
|
||||
mutated: [
|
||||
[
|
||||
Array<Element['id']>,
|
||||
Map<Element['id'], Element['id']>,
|
||||
Element['id'] | null,
|
||||
],
|
||||
],
|
||||
recordChangeDescriptions: [],
|
||||
roots: [],
|
||||
rootSupportsBasicProfiling: [],
|
||||
@@ -658,6 +669,10 @@ export default class Store extends EventEmitter<{
|
||||
return element;
|
||||
}
|
||||
|
||||
containsSuspense(id: SuspenseNode['id']): boolean {
|
||||
return this._idToSuspense.has(id);
|
||||
}
|
||||
|
||||
getSuspenseByID(id: SuspenseNode['id']): SuspenseNode | null {
|
||||
const suspense = this._idToSuspense.get(id);
|
||||
if (suspense === undefined) {
|
||||
@@ -925,7 +940,7 @@ export default class Store extends EventEmitter<{
|
||||
*/
|
||||
getSuspendableDocumentOrderSuspense(
|
||||
uniqueSuspendersOnly: boolean,
|
||||
): $ReadOnlyArray<SuspenseTimelineStep> {
|
||||
): Array<SuspenseTimelineStep> {
|
||||
const target: Array<SuspenseTimelineStep> = [];
|
||||
const roots = this.roots;
|
||||
let rootStep: null | SuspenseTimelineStep = null;
|
||||
@@ -949,17 +964,25 @@ export default class Store extends EventEmitter<{
|
||||
rootStep = {
|
||||
id: suspense.id,
|
||||
environment: environmentName,
|
||||
endTime: suspense.endTime,
|
||||
};
|
||||
target.push(rootStep);
|
||||
} else if (rootStep.environment === null) {
|
||||
// If any root has an environment name, then let's use it.
|
||||
rootStep.environment = environmentName;
|
||||
} else {
|
||||
if (rootStep.environment === null) {
|
||||
// If any root has an environment name, then let's use it.
|
||||
rootStep.environment = environmentName;
|
||||
}
|
||||
if (suspense.endTime > rootStep.endTime) {
|
||||
// If any root has a higher end time, let's use that.
|
||||
rootStep.endTime = suspense.endTime;
|
||||
}
|
||||
}
|
||||
this.pushTimelineStepsInDocumentOrder(
|
||||
suspense.children,
|
||||
target,
|
||||
uniqueSuspendersOnly,
|
||||
environments,
|
||||
0, // Don't pass a minimum end time at the root. The root is always first so doesn't matter.
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -972,6 +995,7 @@ export default class Store extends EventEmitter<{
|
||||
target: Array<SuspenseTimelineStep>,
|
||||
uniqueSuspendersOnly: boolean,
|
||||
parentEnvironments: Array<string>,
|
||||
parentEndTime: number,
|
||||
): void {
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const child = this.getSuspenseByID(children[i]);
|
||||
@@ -996,10 +1020,15 @@ export default class Store extends EventEmitter<{
|
||||
unionEnvironments.length > 0
|
||||
? unionEnvironments[unionEnvironments.length - 1]
|
||||
: null;
|
||||
// The end time of a child boundary can in effect never be earlier than its parent even if
|
||||
// everything unsuspended before that.
|
||||
const maxEndTime =
|
||||
parentEndTime > child.endTime ? parentEndTime : child.endTime;
|
||||
if (hasRects && (!uniqueSuspendersOnly || child.hasUniqueSuspenders)) {
|
||||
target.push({
|
||||
id: child.id,
|
||||
environment: environmentName,
|
||||
endTime: maxEndTime,
|
||||
});
|
||||
}
|
||||
this.pushTimelineStepsInDocumentOrder(
|
||||
@@ -1007,10 +1036,28 @@ export default class Store extends EventEmitter<{
|
||||
target,
|
||||
uniqueSuspendersOnly,
|
||||
unionEnvironments,
|
||||
maxEndTime,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
getEndTimeOrDocumentOrderSuspense(
|
||||
uniqueSuspendersOnly: boolean,
|
||||
): $ReadOnlyArray<SuspenseTimelineStep> {
|
||||
const timeline =
|
||||
this.getSuspendableDocumentOrderSuspense(uniqueSuspendersOnly);
|
||||
if (timeline.length === 0) {
|
||||
return timeline;
|
||||
}
|
||||
const root = timeline[0];
|
||||
// We mutate in place since we assume we've got a fresh array.
|
||||
timeline.sort((a, b) => {
|
||||
// Root is always first
|
||||
return a === root ? -1 : b === root ? 1 : a.endTime - b.endTime;
|
||||
});
|
||||
return timeline;
|
||||
}
|
||||
|
||||
getRendererIDForElement(id: number): number | null {
|
||||
let current = this._idToElement.get(id);
|
||||
while (current !== undefined) {
|
||||
@@ -1124,7 +1171,7 @@ export default class Store extends EventEmitter<{
|
||||
// The Tree context's search reducer expects an explicit list of ids for nodes that were added or removed.
|
||||
// In this case, we can pass it empty arrays since nodes in a collapsed tree are still there (just hidden).
|
||||
// Updating the selected search index later may require auto-expanding a collapsed subtree though.
|
||||
this.emit('mutated', [[], new Map()]);
|
||||
this.emit('mutated', [[], new Map(), null]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1193,10 +1240,11 @@ export default class Store extends EventEmitter<{
|
||||
|
||||
const addedElementIDs: Array<number> = [];
|
||||
// This is a mapping of removed ID -> parent ID:
|
||||
// We'll use the parent ID to adjust selection if it gets deleted.
|
||||
const removedElementIDs: Map<number, number> = new Map();
|
||||
const removedSuspenseIDs: Map<SuspenseNode['id'], SuspenseNode['id']> =
|
||||
new Map();
|
||||
// We'll use the parent ID to adjust selection if it gets deleted.
|
||||
let nextActivitySliceID = null;
|
||||
|
||||
let i = 2;
|
||||
|
||||
@@ -1688,6 +1736,7 @@ export default class Store extends EventEmitter<{
|
||||
hasUniqueSuspenders: false,
|
||||
isSuspended: isSuspended,
|
||||
environments: [],
|
||||
endTime: 0,
|
||||
});
|
||||
|
||||
hasSuspenseTreeChanged = true;
|
||||
@@ -1884,6 +1933,7 @@ export default class Store extends EventEmitter<{
|
||||
for (let changeIndex = 0; changeIndex < changeLength; changeIndex++) {
|
||||
const id = operations[i++];
|
||||
const hasUniqueSuspenders = operations[i++] === 1;
|
||||
const endTime = operations[i++] / 1000;
|
||||
const isSuspended = operations[i++] === 1;
|
||||
const environmentNamesLength = operations[i++];
|
||||
const environmentNames = [];
|
||||
@@ -1919,6 +1969,7 @@ export default class Store extends EventEmitter<{
|
||||
}
|
||||
|
||||
suspense.hasUniqueSuspenders = hasUniqueSuspenders;
|
||||
suspense.endTime = endTime;
|
||||
suspense.isSuspended = isSuspended;
|
||||
suspense.environments = environmentNames;
|
||||
}
|
||||
@@ -1927,6 +1978,11 @@ export default class Store extends EventEmitter<{
|
||||
|
||||
break;
|
||||
}
|
||||
case TREE_OPERATION_APPLIED_ACTIVITY_SLICE_CHANGE: {
|
||||
i++;
|
||||
nextActivitySliceID = operations[i++];
|
||||
break;
|
||||
}
|
||||
default:
|
||||
this._throwAndEmitError(
|
||||
new UnsupportedBridgeOperationError(
|
||||
@@ -2025,9 +2081,80 @@ export default class Store extends EventEmitter<{
|
||||
console.groupEnd();
|
||||
}
|
||||
|
||||
this.emit('mutated', [addedElementIDs, removedElementIDs]);
|
||||
if (nextActivitySliceID !== null && nextActivitySliceID !== 0) {
|
||||
let didCollapse = false;
|
||||
// The backend filtered everything above the Activity slice.
|
||||
// We need to hide everything below the Activity slice by collapsing
|
||||
// the Activities that are descendants of the next Activity slice.
|
||||
const nextActivitySlice = this._idToElement.get(nextActivitySliceID);
|
||||
if (nextActivitySlice === undefined) {
|
||||
throw new Error('Next Activity slice not found in Store.');
|
||||
}
|
||||
|
||||
for (let j = 0; j < nextActivitySlice.children.length; j++) {
|
||||
didCollapse ||= this._collapseActivitiesRecursively(
|
||||
nextActivitySlice.children[j],
|
||||
);
|
||||
}
|
||||
|
||||
if (didCollapse) {
|
||||
let weightAcrossRoots = 0;
|
||||
this._roots.forEach(rootID => {
|
||||
const {weight} = ((this.getElementByID(rootID): any): Element);
|
||||
weightAcrossRoots += weight;
|
||||
});
|
||||
this._weightAcrossRoots = weightAcrossRoots;
|
||||
}
|
||||
}
|
||||
|
||||
for (let j = 0; j < this._componentFilters.length; j++) {
|
||||
const filter = this._componentFilters[j];
|
||||
// If we're focusing an Activity, IDs may have changed.
|
||||
if (filter.type === ComponentFilterActivitySlice) {
|
||||
if (nextActivitySliceID === null || nextActivitySliceID === 0) {
|
||||
filter.isValid = false;
|
||||
} else {
|
||||
filter.activityID = nextActivitySliceID;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.emit('mutated', [
|
||||
addedElementIDs,
|
||||
removedElementIDs,
|
||||
nextActivitySliceID,
|
||||
]);
|
||||
};
|
||||
|
||||
_collapseActivitiesRecursively(elementID: number): boolean {
|
||||
let didMutate = false;
|
||||
const element = this._idToElement.get(elementID);
|
||||
if (element === undefined) {
|
||||
throw new Error('Element not found in Store.');
|
||||
}
|
||||
|
||||
if (element.type === ElementTypeActivity) {
|
||||
if (!element.isCollapsed) {
|
||||
element.isCollapsed = true;
|
||||
|
||||
const weightDelta = 1 - element.weight;
|
||||
|
||||
let parentElement = this._idToElement.get(element.parentID);
|
||||
while (parentElement !== undefined) {
|
||||
parentElement.weight += weightDelta;
|
||||
parentElement = this._idToElement.get(parentElement.parentID);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i < element.children.length; i++) {
|
||||
didMutate ||= this._collapseActivitiesRecursively(element.children[i]);
|
||||
}
|
||||
return didMutate;
|
||||
}
|
||||
|
||||
// Certain backends save filters on a per-domain basis.
|
||||
// In order to prevent filter preferences and applied filters from being out of sync,
|
||||
// this message enables the backend to override the frontend's current ("saved") filters.
|
||||
@@ -2193,7 +2320,7 @@ export default class Store extends EventEmitter<{
|
||||
|
||||
if (previousStatus !== status) {
|
||||
// Propagate to subscribers, although tree state has not changed
|
||||
this.emit('mutated', [[], new Map()]);
|
||||
this.emit('mutated', [[], new Map(), null]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
.ActivitySlice {
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ActivitySliceButton {
|
||||
color: var(--color-button-active);
|
||||
font-family: var(--font-family-monospace);
|
||||
font-size: var(--font-size-monospace-normal);
|
||||
}
|
||||
|
||||
.Bar {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.VRule {
|
||||
flex: 0 0 auto;
|
||||
height: 20px;
|
||||
width: 1px;
|
||||
background-color: var(--color-border);
|
||||
margin: 0 0.5rem;
|
||||
}
|
||||
52
packages/react-devtools-shared/src/devtools/views/Components/ActivitySlice.js
vendored
Normal file
52
packages/react-devtools-shared/src/devtools/views/Components/ActivitySlice.js
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
import * as React from 'react';
|
||||
import {startTransition, useContext} from 'react';
|
||||
import Button from '../Button';
|
||||
import ButtonIcon from '../ButtonIcon';
|
||||
import {StoreContext} from '../context';
|
||||
import {useChangeActivitySliceAction} from '../SuspenseTab/ActivityList';
|
||||
import {TreeDispatcherContext, TreeStateContext} from './TreeContext';
|
||||
import styles from './ActivitySlice.css';
|
||||
|
||||
export default function ActivitySlice(): React.Node {
|
||||
const dispatch = useContext(TreeDispatcherContext);
|
||||
const {activityID} = useContext(TreeStateContext);
|
||||
const store = useContext(StoreContext);
|
||||
|
||||
const activity =
|
||||
activityID === null ? null : store.getElementByID(activityID);
|
||||
const name = activity ? activity.nameProp : null;
|
||||
|
||||
const changeActivitySliceAction = useChangeActivitySliceAction();
|
||||
|
||||
return (
|
||||
<div className={styles.ActivitySlice}>
|
||||
<div className={styles.Bar}>
|
||||
<Button
|
||||
className={styles.ActivitySliceButton}
|
||||
onClick={dispatch.bind(null, {
|
||||
type: 'SELECT_ELEMENT_BY_ID',
|
||||
payload: activityID,
|
||||
})}>
|
||||
"{name || 'Unknown'}"
|
||||
</Button>
|
||||
</div>
|
||||
<div className={styles.VRule} />
|
||||
<Button
|
||||
onClick={startTransition.bind(
|
||||
null,
|
||||
changeActivitySliceAction.bind(null, null),
|
||||
)}
|
||||
title="Back to tree view">
|
||||
<ButtonIcon type="close" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -8,8 +8,9 @@
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import {Fragment, useContext, useMemo, useState} from 'react';
|
||||
import {Fragment, startTransition, useContext, useMemo, useState} from 'react';
|
||||
import Store from 'react-devtools-shared/src/devtools/store';
|
||||
import {ElementTypeActivity} from 'react-devtools-shared/src/frontend/types';
|
||||
import ButtonIcon from '../ButtonIcon';
|
||||
import {TreeDispatcherContext, TreeStateContext} from './TreeContext';
|
||||
import {StoreContext} from '../context';
|
||||
@@ -25,6 +26,7 @@ import styles from './Element.css';
|
||||
import Icon from '../Icon';
|
||||
import {useChangeOwnerAction} from './OwnersListContext';
|
||||
import Tooltip from './reach-ui/tooltip';
|
||||
import {useChangeActivitySliceAction} from '../SuspenseTab/ActivityList';
|
||||
|
||||
type Props = {
|
||||
data: ItemData,
|
||||
@@ -65,6 +67,7 @@ export default function Element({data, index, style}: Props): React.Node {
|
||||
}>(errorsAndWarningsSubscription);
|
||||
|
||||
const changeOwnerAction = useChangeOwnerAction();
|
||||
const changeActivitySliceAction = useChangeActivitySliceAction();
|
||||
|
||||
// Handle elements that are removed from the tree while an async render is in progress.
|
||||
if (element == null) {
|
||||
@@ -75,9 +78,13 @@ export default function Element({data, index, style}: Props): React.Node {
|
||||
}
|
||||
|
||||
const handleDoubleClick = () => {
|
||||
if (id !== null) {
|
||||
changeOwnerAction(id);
|
||||
}
|
||||
startTransition(() => {
|
||||
if (element.type === ElementTypeActivity) {
|
||||
changeActivitySliceAction(element.id);
|
||||
} else {
|
||||
changeOwnerAction(element.id);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// $FlowFixMe[missing-local-annot]
|
||||
|
||||
@@ -11,6 +11,7 @@ import * as React from 'react';
|
||||
import {
|
||||
Fragment,
|
||||
Suspense,
|
||||
startTransition,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
@@ -37,7 +38,10 @@ import ButtonIcon from '../ButtonIcon';
|
||||
import Button from '../Button';
|
||||
import {logEvent} from 'react-devtools-shared/src/Logger';
|
||||
import {useExtensionComponentsPanelVisibility} from 'react-devtools-shared/src/frontend/hooks/useExtensionComponentsPanelVisibility';
|
||||
import {ElementTypeActivity} from 'react-devtools-shared/src/frontend/types';
|
||||
import {useChangeOwnerAction} from './OwnersListContext';
|
||||
import {useChangeActivitySliceAction} from '../SuspenseTab/ActivityList';
|
||||
import ActivitySlice from './ActivitySlice';
|
||||
|
||||
// Indent for each node at level N, compared to node at level N - 1.
|
||||
const INDENTATION_SIZE = 10;
|
||||
@@ -72,6 +76,7 @@ function calculateInitialScrollOffset(
|
||||
export default function Tree(): React.Node {
|
||||
const dispatch = useContext(TreeDispatcherContext);
|
||||
const {
|
||||
activityID,
|
||||
numElements,
|
||||
ownerID,
|
||||
searchIndex,
|
||||
@@ -302,6 +307,7 @@ export default function Tree(): React.Node {
|
||||
const handleBlur = useCallback(() => setTreeFocused(false), []);
|
||||
const handleFocus = useCallback(() => setTreeFocused(true), []);
|
||||
|
||||
const changeActivitySliceAction = useChangeActivitySliceAction();
|
||||
const changeOwnerAction = useChangeOwnerAction();
|
||||
const handleKeyPress = useCallback(
|
||||
(event: $FlowFixMe) => {
|
||||
@@ -309,7 +315,17 @@ export default function Tree(): React.Node {
|
||||
case 'Enter':
|
||||
case ' ':
|
||||
if (inspectedElementID !== null) {
|
||||
changeOwnerAction(inspectedElementID);
|
||||
const inspectedElement = store.getElementByID(inspectedElementID);
|
||||
startTransition(() => {
|
||||
if (
|
||||
inspectedElement !== null &&
|
||||
inspectedElement.type === ElementTypeActivity
|
||||
) {
|
||||
changeActivitySliceAction(inspectedElementID);
|
||||
} else {
|
||||
changeOwnerAction(inspectedElementID);
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
default:
|
||||
@@ -444,7 +460,13 @@ export default function Tree(): React.Node {
|
||||
</Fragment>
|
||||
)}
|
||||
<Suspense fallback={<Loading />}>
|
||||
{ownerID !== null ? <OwnersStack /> : <ComponentSearchInput />}
|
||||
{ownerID !== null ? (
|
||||
<OwnersStack />
|
||||
) : activityID !== null ? (
|
||||
<ActivitySlice />
|
||||
) : (
|
||||
<ComponentSearchInput />
|
||||
)}
|
||||
</Suspense>
|
||||
{ownerID === null && (errors > 0 || warnings > 0) && (
|
||||
<React.Fragment>
|
||||
|
||||
@@ -57,6 +57,9 @@ export type StateContext = {
|
||||
ownerID: number | null,
|
||||
ownerFlatTree: Array<Element> | null,
|
||||
|
||||
// Activity slice
|
||||
activityID: Element['id'] | null,
|
||||
|
||||
// Inspection element panel
|
||||
inspectedElementID: number | null,
|
||||
inspectedElementIndex: number | null,
|
||||
@@ -70,7 +73,7 @@ type ACTION_GO_TO_PREVIOUS_SEARCH_RESULT = {
|
||||
};
|
||||
type ACTION_HANDLE_STORE_MUTATION = {
|
||||
type: 'HANDLE_STORE_MUTATION',
|
||||
payload: [Array<number>, Map<number, number>],
|
||||
payload: [Array<number>, Map<number, number>, null | Element['id']],
|
||||
};
|
||||
type ACTION_RESET_OWNER_STACK = {
|
||||
type: 'RESET_OWNER_STACK',
|
||||
@@ -167,6 +170,9 @@ type State = {
|
||||
ownerID: number | null,
|
||||
ownerFlatTree: Array<Element> | null,
|
||||
|
||||
// Activity slice
|
||||
activityID: Element['id'] | null,
|
||||
|
||||
// Inspection element panel
|
||||
inspectedElementID: number | null,
|
||||
inspectedElementIndex: number | null,
|
||||
@@ -794,6 +800,33 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
|
||||
};
|
||||
}
|
||||
|
||||
function reduceActivityState(
|
||||
store: Store,
|
||||
state: State,
|
||||
action: Action,
|
||||
): State {
|
||||
switch (action.type) {
|
||||
case 'HANDLE_STORE_MUTATION':
|
||||
let {activityID} = state;
|
||||
const [, , activitySliceIDChange] = action.payload;
|
||||
if (activitySliceIDChange === 0 && activityID !== null) {
|
||||
activityID = null;
|
||||
} else if (
|
||||
activitySliceIDChange !== null &&
|
||||
activitySliceIDChange !== activityID
|
||||
) {
|
||||
activityID = activitySliceIDChange;
|
||||
}
|
||||
if (activityID !== state.activityID) {
|
||||
return {
|
||||
...state,
|
||||
activityID,
|
||||
};
|
||||
}
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
type Props = {
|
||||
children: React$Node,
|
||||
|
||||
@@ -828,6 +861,9 @@ function getInitialState({
|
||||
ownerID: defaultOwnerID == null ? null : defaultOwnerID,
|
||||
ownerFlatTree: null,
|
||||
|
||||
// Activity slice
|
||||
activityID: null,
|
||||
|
||||
// Inspection element panel
|
||||
inspectedElementID:
|
||||
defaultInspectedElementID != null
|
||||
@@ -882,6 +918,7 @@ function TreeContextController({
|
||||
state = reduceTreeState(store, state, action);
|
||||
state = reduceSearchState(store, state, action);
|
||||
state = reduceOwnersState(store, state, action);
|
||||
state = reduceActivityState(store, state, action);
|
||||
|
||||
// TODO(hoxyq): review
|
||||
// If the selected ID is in a collapsed subtree, reset the selected index to null.
|
||||
@@ -950,13 +987,14 @@ function TreeContextController({
|
||||
|
||||
// Mutations to the underlying tree may impact this context (e.g. search results, selection state).
|
||||
useEffect(() => {
|
||||
const handleStoreMutated = ([addedElementIDs, removedElementIDs]: [
|
||||
Array<number>,
|
||||
Map<number, number>,
|
||||
]) => {
|
||||
const handleStoreMutated = ([
|
||||
addedElementIDs,
|
||||
removedElementIDs,
|
||||
activitySliceIDChange,
|
||||
]: [Array<number>, Map<number, number>, null | Element['id']]) => {
|
||||
dispatch({
|
||||
type: 'HANDLE_STORE_MUTATION',
|
||||
payload: [addedElementIDs, removedElementIDs],
|
||||
payload: [addedElementIDs, removedElementIDs, activitySliceIDChange],
|
||||
});
|
||||
};
|
||||
|
||||
@@ -967,7 +1005,7 @@ function TreeContextController({
|
||||
// It would only impact the search state, which is unlikely to exist yet at this point.
|
||||
dispatch({
|
||||
type: 'HANDLE_STORE_MUTATION',
|
||||
payload: [[], new Map()],
|
||||
payload: [[], new Map(), null],
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
TREE_OPERATION_SET_SUBTREE_MODE,
|
||||
TREE_OPERATION_UPDATE_TREE_BASE_DURATION,
|
||||
TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS,
|
||||
TREE_OPERATION_APPLIED_ACTIVITY_SLICE_CHANGE,
|
||||
SUSPENSE_TREE_OPERATION_ADD,
|
||||
SUSPENSE_TREE_OPERATION_REMOVE,
|
||||
SUSPENSE_TREE_OPERATION_REORDER_CHILDREN,
|
||||
@@ -460,13 +461,14 @@ function updateTree(
|
||||
for (let changeIndex = 0; changeIndex < changeLength; changeIndex++) {
|
||||
const suspenseNodeId = operations[i++];
|
||||
const hasUniqueSuspenders = operations[i++] === 1;
|
||||
const endTime = operations[i++] / 1000;
|
||||
const isSuspended = operations[i++] === 1;
|
||||
const environmentNamesLength = operations[i++];
|
||||
i += environmentNamesLength;
|
||||
if (__DEBUG__) {
|
||||
debug(
|
||||
'Suspender changes',
|
||||
`Suspense node ${suspenseNodeId} unique suspenders set to ${String(hasUniqueSuspenders)} is suspended set to ${String(isSuspended)} with ${String(environmentNamesLength)} environments`,
|
||||
`Suspense node ${suspenseNodeId} unique suspenders set to ${String(hasUniqueSuspenders)} ending at ${String(endTime)} is suspended set to ${String(isSuspended)} with ${String(environmentNamesLength)} environments`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -474,6 +476,20 @@ function updateTree(
|
||||
break;
|
||||
}
|
||||
|
||||
case TREE_OPERATION_APPLIED_ACTIVITY_SLICE_CHANGE: {
|
||||
i++;
|
||||
const activitySliceIDChange = operations[i++];
|
||||
if (__DEBUG__) {
|
||||
debug(
|
||||
'Applied activity slice change',
|
||||
activitySliceIDChange === 0
|
||||
? 'Reset applied activity slice'
|
||||
: `Changed to activity slice ID ${activitySliceIDChange}`,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
throw Error(`Unsupported Bridge operation "${operation}"`);
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
ComponentFilterHOC,
|
||||
ComponentFilterLocation,
|
||||
ComponentFilterEnvironmentName,
|
||||
ComponentFilterActivitySlice,
|
||||
ElementTypeClass,
|
||||
ElementTypeContext,
|
||||
ElementTypeFunction,
|
||||
@@ -171,6 +172,8 @@ export default function ComponentsSettings({
|
||||
isValid: true,
|
||||
value: 'Client',
|
||||
};
|
||||
} else if (type === ComponentFilterActivitySlice) {
|
||||
// TODO: Allow changing type
|
||||
}
|
||||
}
|
||||
return cloned;
|
||||
@@ -364,34 +367,39 @@ export default function ComponentsSettings({
|
||||
{componentFilters.map((componentFilter, index) => (
|
||||
<tr className={styles.TableRow} key={index}>
|
||||
<td className={styles.TableCell}>
|
||||
<Toggle
|
||||
className={
|
||||
componentFilter.isValid !== false
|
||||
? ''
|
||||
: styles.InvalidRegExp
|
||||
}
|
||||
isChecked={componentFilter.isEnabled}
|
||||
onChange={isEnabled =>
|
||||
toggleFilterIsEnabled(componentFilter, isEnabled)
|
||||
}
|
||||
title={
|
||||
componentFilter.isValid === false
|
||||
? 'Filter invalid'
|
||||
: componentFilter.isEnabled
|
||||
? 'Filter enabled'
|
||||
: 'Filter disabled'
|
||||
}>
|
||||
<ToggleIcon
|
||||
isEnabled={componentFilter.isEnabled}
|
||||
isValid={
|
||||
componentFilter.isValid == null ||
|
||||
componentFilter.isValid === true
|
||||
{componentFilter.type !== ComponentFilterActivitySlice && (
|
||||
<Toggle
|
||||
className={
|
||||
componentFilter.isValid !== false
|
||||
? ''
|
||||
: styles.InvalidRegExp
|
||||
}
|
||||
/>
|
||||
</Toggle>
|
||||
isChecked={componentFilter.isEnabled}
|
||||
onChange={isEnabled =>
|
||||
toggleFilterIsEnabled(componentFilter, isEnabled)
|
||||
}
|
||||
title={
|
||||
componentFilter.isValid === false
|
||||
? 'Filter invalid'
|
||||
: componentFilter.isEnabled
|
||||
? 'Filter enabled'
|
||||
: 'Filter disabled'
|
||||
}>
|
||||
<ToggleIcon
|
||||
isEnabled={componentFilter.isEnabled}
|
||||
isValid={
|
||||
componentFilter.isValid == null ||
|
||||
componentFilter.isValid === true
|
||||
}
|
||||
/>
|
||||
</Toggle>
|
||||
)}
|
||||
</td>
|
||||
<td className={styles.TableCell}>
|
||||
<select
|
||||
disabled={
|
||||
componentFilter.type === ComponentFilterActivitySlice
|
||||
}
|
||||
value={componentFilter.type}
|
||||
onChange={({currentTarget}) =>
|
||||
changeFilterType(
|
||||
@@ -413,6 +421,11 @@ export default function ComponentsSettings({
|
||||
environment
|
||||
</option>
|
||||
)}
|
||||
{componentFilter.type === ComponentFilterActivitySlice && (
|
||||
<option value={ComponentFilterActivitySlice}>
|
||||
component
|
||||
</option>
|
||||
)}
|
||||
</select>
|
||||
</td>
|
||||
<td className={styles.TableCell}>
|
||||
@@ -422,6 +435,8 @@ export default function ComponentsSettings({
|
||||
{(componentFilter.type === ComponentFilterLocation ||
|
||||
componentFilter.type === ComponentFilterDisplayName) &&
|
||||
'matches'}
|
||||
{componentFilter.type === ComponentFilterActivitySlice &&
|
||||
'within'}
|
||||
</td>
|
||||
<td className={styles.TableCell}>
|
||||
{componentFilter.type === ComponentFilterElementType && (
|
||||
@@ -487,6 +502,9 @@ export default function ComponentsSettings({
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
{componentFilter.type === ComponentFilterActivitySlice && (
|
||||
<span>Activity Slice</span>
|
||||
)}
|
||||
</td>
|
||||
<td className={styles.TableCell}>
|
||||
<Button
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
.ActivityList {
|
||||
cursor: default;
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.ActivityList[data-pending-activity-slice-selection="true"] {
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
.ActivityList:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.ActivityListItem {
|
||||
color: var(--color-component-name);
|
||||
padding: 0 0.25rem;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.ActivityListItem:hover {
|
||||
background-color: var(--color-background-hover);
|
||||
}
|
||||
|
||||
.ActivityListItem[aria-selected="true"] {
|
||||
background-color: var(--color-background-inactive);
|
||||
}
|
||||
|
||||
.ActivityList:focus .ActivityListItem[aria-selected="true"] {
|
||||
background-color: var(--color-background-selected);
|
||||
color: var(--color-text-selected);
|
||||
|
||||
/* Invert colors */
|
||||
--color-component-name: var(--color-component-name-inverted);
|
||||
--color-text: var(--color-text-selected);
|
||||
--color-component-badge-background: var(
|
||||
--color-component-badge-background-inverted
|
||||
);
|
||||
--color-forget-badge-background: var(--color-forget-badge-background-inverted);
|
||||
--color-component-badge-count: var(--color-component-badge-count-inverted);
|
||||
--color-attribute-name: var(--color-attribute-name-inverted);
|
||||
--color-attribute-value: var(--color-attribute-value-inverted);
|
||||
--color-expand-collapse-toggle: var(--color-component-name-inverted);
|
||||
}
|
||||
173
packages/react-devtools-shared/src/devtools/views/SuspenseTab/ActivityList.js
vendored
Normal file
173
packages/react-devtools-shared/src/devtools/views/SuspenseTab/ActivityList.js
vendored
Normal file
@@ -0,0 +1,173 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
import type {
|
||||
Element,
|
||||
ActivitySliceFilter,
|
||||
ComponentFilter,
|
||||
} from 'react-devtools-shared/src/frontend/types';
|
||||
import typeof {
|
||||
SyntheticMouseEvent,
|
||||
SyntheticKeyboardEvent,
|
||||
} from 'react-dom-bindings/src/events/SyntheticEvent';
|
||||
|
||||
import * as React from 'react';
|
||||
import {useContext, useTransition} from 'react';
|
||||
import {ComponentFilterActivitySlice} from 'react-devtools-shared/src/frontend/types';
|
||||
import styles from './ActivityList.css';
|
||||
import {
|
||||
TreeStateContext,
|
||||
TreeDispatcherContext,
|
||||
} from '../Components/TreeContext';
|
||||
import {useHighlightHostInstance} from '../hooks';
|
||||
import {StoreContext} from '../context';
|
||||
|
||||
export function useChangeActivitySliceAction(): (
|
||||
id: Element['id'] | null,
|
||||
) => void {
|
||||
const store = useContext(StoreContext);
|
||||
|
||||
function changeActivitySliceAction(activityID: Element['id'] | null) {
|
||||
const nextFilters: ComponentFilter[] = [];
|
||||
// Remove any existing activity slice filter
|
||||
for (let i = 0; i < store.componentFilters.length; i++) {
|
||||
const filter = store.componentFilters[i];
|
||||
if (filter.type !== ComponentFilterActivitySlice) {
|
||||
nextFilters.push(filter);
|
||||
}
|
||||
}
|
||||
|
||||
if (activityID !== null) {
|
||||
const rendererID = store.getRendererIDForElement(activityID);
|
||||
if (rendererID === null) {
|
||||
throw new Error('Expected to find renderer.');
|
||||
}
|
||||
const activityFilter: ActivitySliceFilter = {
|
||||
type: ComponentFilterActivitySlice,
|
||||
activityID,
|
||||
rendererID,
|
||||
isValid: true,
|
||||
isEnabled: true,
|
||||
};
|
||||
nextFilters.push(activityFilter);
|
||||
}
|
||||
store.componentFilters = nextFilters;
|
||||
}
|
||||
|
||||
return changeActivitySliceAction;
|
||||
}
|
||||
|
||||
export default function ActivityList({
|
||||
activities,
|
||||
}: {
|
||||
activities: $ReadOnlyArray<Element>,
|
||||
}): React$Node {
|
||||
const {inspectedElementID} = useContext(TreeStateContext);
|
||||
const treeDispatch = useContext(TreeDispatcherContext);
|
||||
// TODO: Derive from inspected element
|
||||
const selectedActivityID = inspectedElementID;
|
||||
const {highlightHostInstance, clearHighlightHostInstance} =
|
||||
useHighlightHostInstance();
|
||||
|
||||
const [isPendingActivitySliceSelection, startActivitySliceSelection] =
|
||||
useTransition();
|
||||
const changeActivitySliceAction = useChangeActivitySliceAction();
|
||||
|
||||
function handleKeyDown(event: SyntheticKeyboardEvent) {
|
||||
// TODO: Implement keyboard navigation
|
||||
switch (event.key) {
|
||||
case 'Enter':
|
||||
case ' ':
|
||||
if (inspectedElementID !== null) {
|
||||
startActivitySliceSelection(() => {
|
||||
changeActivitySliceAction(inspectedElementID);
|
||||
});
|
||||
}
|
||||
event.preventDefault();
|
||||
break;
|
||||
case 'Home':
|
||||
treeDispatch({type: 'SELECT_ELEMENT_BY_ID', payload: activities[0].id});
|
||||
event.preventDefault();
|
||||
break;
|
||||
case 'End':
|
||||
treeDispatch({
|
||||
type: 'SELECT_ELEMENT_BY_ID',
|
||||
payload: activities[activities.length - 1].id,
|
||||
});
|
||||
event.preventDefault();
|
||||
break;
|
||||
case 'ArrowUp': {
|
||||
const currentIndex = activities.findIndex(
|
||||
activity => activity.id === selectedActivityID,
|
||||
);
|
||||
if (currentIndex !== undefined) {
|
||||
const nextIndex =
|
||||
(currentIndex + activities.length - 1) % activities.length;
|
||||
|
||||
treeDispatch({
|
||||
type: 'SELECT_ELEMENT_BY_ID',
|
||||
payload: activities[nextIndex].id,
|
||||
});
|
||||
}
|
||||
event.preventDefault();
|
||||
break;
|
||||
}
|
||||
case 'ArrowDown': {
|
||||
const currentIndex = activities.findIndex(
|
||||
activity => activity.id === selectedActivityID,
|
||||
);
|
||||
if (currentIndex !== undefined) {
|
||||
const nextIndex = (currentIndex + 1) % activities.length;
|
||||
|
||||
treeDispatch({
|
||||
type: 'SELECT_ELEMENT_BY_ID',
|
||||
payload: activities[nextIndex].id,
|
||||
});
|
||||
}
|
||||
event.preventDefault();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function handleClick(id: Element['id'], event: SyntheticMouseEvent) {
|
||||
event.preventDefault();
|
||||
treeDispatch({type: 'SELECT_ELEMENT_BY_ID', payload: id});
|
||||
}
|
||||
|
||||
function handleDoubleClick() {
|
||||
if (inspectedElementID !== null) {
|
||||
changeActivitySliceAction(inspectedElementID);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ol
|
||||
role="listbox"
|
||||
className={styles.ActivityList}
|
||||
data-pending-activity-slice-selection={isPendingActivitySliceSelection}
|
||||
tabIndex={0}
|
||||
onKeyDown={handleKeyDown}>
|
||||
{activities.map(activity => (
|
||||
<li
|
||||
key={activity.id}
|
||||
role="option"
|
||||
aria-selected={activity.id === selectedActivityID ? 'true' : 'false'}
|
||||
className={styles.ActivityListItem}
|
||||
onClick={handleClick.bind(null, activity.id)}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
onPointerOver={highlightHostInstance.bind(null, activity.id, false)}
|
||||
onPointerLeave={clearHighlightHostInstance}>
|
||||
{activity.nameProp}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
);
|
||||
}
|
||||
@@ -6,18 +6,17 @@
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.SuspenseRectsContainer[data-highlighted='true'] {
|
||||
outline-color: var(--color-transition);
|
||||
outline-style: solid;
|
||||
outline-width: 4px;
|
||||
}
|
||||
|
||||
.SuspenseRectsRoot {
|
||||
cursor: pointer;
|
||||
outline-color: var(--color-transition);
|
||||
background-color: color-mix(in srgb, var(--color-transition) 5%, transparent);
|
||||
}
|
||||
|
||||
.SuspenseRectsRootOutline {
|
||||
outline-width: 4px;
|
||||
border-radius: 0.125rem;
|
||||
}
|
||||
|
||||
.SuspenseRectsRoot[data-hovered='true'] {
|
||||
background-color: color-mix(in srgb, var(--color-transition) 15%, transparent);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import typeof {
|
||||
} from 'react-dom-bindings/src/events/SyntheticEvent';
|
||||
|
||||
import * as React from 'react';
|
||||
import {createContext, useContext} from 'react';
|
||||
import {createContext, useContext, useLayoutEffect} from 'react';
|
||||
import {
|
||||
TreeDispatcherContext,
|
||||
TreeStateContext,
|
||||
@@ -236,13 +236,6 @@ function SuspenseRects({
|
||||
<span>{suspense.name}</span>
|
||||
</ScaledRect>
|
||||
) : null}
|
||||
{selected && visible ? (
|
||||
<ScaledRect
|
||||
className={styles.SuspenseRectOutline}
|
||||
rect={boundingBox}
|
||||
adjust={true}
|
||||
/>
|
||||
) : null}
|
||||
</ViewBox.Provider>
|
||||
</ScaledRect>
|
||||
);
|
||||
@@ -435,7 +428,11 @@ function SuspenseRectsRoot({rootID}: {rootID: SuspenseNode['id']}): React$Node {
|
||||
|
||||
const ViewBox = createContext<Rect>((null: any));
|
||||
|
||||
function SuspenseRectsContainer(): React$Node {
|
||||
function SuspenseRectsContainer({
|
||||
scaleRef,
|
||||
}: {
|
||||
scaleRef: {current: number},
|
||||
}): React$Node {
|
||||
const store = useContext(StoreContext);
|
||||
const {inspectedElementID} = useContext(TreeStateContext);
|
||||
const treeDispatch = useContext(TreeDispatcherContext);
|
||||
@@ -505,17 +502,47 @@ function SuspenseRectsContainer(): React$Node {
|
||||
const rootEnvironment =
|
||||
timeline.length === 0 ? null : timeline[0].environment;
|
||||
|
||||
useLayoutEffect(() => {
|
||||
// 100% of the width represents this many pixels in the real document.
|
||||
scaleRef.current = boundingBoxWidth;
|
||||
}, [boundingBoxWidth]);
|
||||
|
||||
let selectedBoundingBox = null;
|
||||
let selectedEnvironment = null;
|
||||
if (isRootSelected) {
|
||||
selectedEnvironment = rootEnvironment;
|
||||
} else if (
|
||||
inspectedElementID !== null &&
|
||||
// TODO: Separate inspected element and inspected Suspense and use the inspected Suspense ID here.
|
||||
store.containsSuspense(inspectedElementID)
|
||||
) {
|
||||
const selectedSuspenseNode = store.getSuspenseByID(inspectedElementID);
|
||||
if (
|
||||
selectedSuspenseNode !== null &&
|
||||
(selectedSuspenseNode.hasUniqueSuspenders || !uniqueSuspendersOnly)
|
||||
) {
|
||||
selectedBoundingBox = getBoundingBox(selectedSuspenseNode.rects);
|
||||
for (let i = 0; i < timeline.length; i++) {
|
||||
const timelineStep = timeline[i];
|
||||
if (timelineStep.id === inspectedElementID) {
|
||||
selectedEnvironment = timelineStep.environment;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
styles.SuspenseRectsContainer +
|
||||
(hasRootSuspenders ? ' ' + styles.SuspenseRectsRoot : '') +
|
||||
(isRootSelected ? ' ' + styles.SuspenseRectsRootOutline : '') +
|
||||
' ' +
|
||||
getClassNameForEnvironment(rootEnvironment)
|
||||
}
|
||||
onClick={handleClick}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
data-highlighted={isRootSelected}
|
||||
data-hovered={isRootHovered}>
|
||||
<ViewBox.Provider value={boundingBox}>
|
||||
<div
|
||||
@@ -524,6 +551,17 @@ function SuspenseRectsContainer(): React$Node {
|
||||
{roots.map(rootID => {
|
||||
return <SuspenseRectsRoot key={rootID} rootID={rootID} />;
|
||||
})}
|
||||
{selectedBoundingBox !== null ? (
|
||||
<ScaledRect
|
||||
className={
|
||||
styles.SuspenseRectOutline +
|
||||
' ' +
|
||||
getClassNameForEnvironment(selectedEnvironment)
|
||||
}
|
||||
rect={selectedBoundingBox}
|
||||
adjust={true}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</ViewBox.Provider>
|
||||
</div>
|
||||
|
||||
@@ -91,10 +91,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
.TreeList {
|
||||
.ActivityList {
|
||||
flex: 0 0 var(--horizontal-resize-tree-list-percentage);
|
||||
border-right: 1px solid var(--color-border);
|
||||
padding: 0.25rem;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
@@ -142,4 +141,4 @@
|
||||
|
||||
.SuspenseTreeViewFooterButtons {
|
||||
padding: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,12 +6,14 @@
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
import type {Element} from 'react-devtools-shared/src/frontend/types';
|
||||
|
||||
import * as React from 'react';
|
||||
import {
|
||||
useContext,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useReducer,
|
||||
useRef,
|
||||
Fragment,
|
||||
@@ -30,12 +32,12 @@ import styles from './SuspenseTab.css';
|
||||
import SuspenseBreadcrumbs from './SuspenseBreadcrumbs';
|
||||
import SuspenseRects from './SuspenseRects';
|
||||
import SuspenseTimeline from './SuspenseTimeline';
|
||||
import SuspenseTreeList from './SuspenseTreeList';
|
||||
import ActivityList from './ActivityList';
|
||||
import {
|
||||
SuspenseTreeDispatcherContext,
|
||||
SuspenseTreeStateContext,
|
||||
} from './SuspenseTreeContext';
|
||||
import {StoreContext, OptionsContext} from '../context';
|
||||
import {BridgeContext, StoreContext, OptionsContext} from '../context';
|
||||
import Button from '../Button';
|
||||
import Toggle from '../Toggle';
|
||||
import typeof {SyntheticPointerEvent} from 'react-dom-bindings/src/events/SyntheticEvent';
|
||||
@@ -74,7 +76,7 @@ function ToggleUniqueSuspenders() {
|
||||
function handleToggleUniqueSuspenders() {
|
||||
const nextUniqueSuspendersOnly = !uniqueSuspendersOnly;
|
||||
// TODO: Handle different timeline modes (e.g. random order)
|
||||
const nextTimeline = store.getSuspendableDocumentOrderSuspense(
|
||||
const nextTimeline = store.getEndTimeOrDocumentOrderSuspense(
|
||||
nextUniqueSuspendersOnly,
|
||||
);
|
||||
suspenseTreeDispatch({
|
||||
@@ -157,6 +159,130 @@ function ToggleInspectedElement({
|
||||
);
|
||||
}
|
||||
|
||||
function SynchronizedScrollContainer({
|
||||
className,
|
||||
children,
|
||||
scaleRef,
|
||||
}: {
|
||||
className?: string,
|
||||
children?: React.Node,
|
||||
scaleRef: {current: number},
|
||||
}) {
|
||||
const bridge = useContext(BridgeContext);
|
||||
const ref = useRef(null);
|
||||
const applyingScrollRef = useRef(false);
|
||||
|
||||
// TODO: useEffectEvent
|
||||
function scrollContainerTo({
|
||||
left,
|
||||
top,
|
||||
right,
|
||||
bottom,
|
||||
}: {
|
||||
left: number,
|
||||
top: number,
|
||||
right: number,
|
||||
bottom: number,
|
||||
}): void {
|
||||
const element = ref.current;
|
||||
if (element === null) {
|
||||
return;
|
||||
}
|
||||
const scale = scaleRef.current / element.clientWidth;
|
||||
const targetLeft = Math.round(left / scale);
|
||||
const targetTop = Math.round(top / scale);
|
||||
if (
|
||||
targetLeft !== Math.round(element.scrollLeft) ||
|
||||
targetTop !== Math.round(element.scrollTop)
|
||||
) {
|
||||
// Disable scroll events until we've applied the new scroll position.
|
||||
applyingScrollRef.current = true;
|
||||
element.scrollTo({
|
||||
left: targetLeft,
|
||||
top: targetTop,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const callback = scrollContainerTo;
|
||||
bridge.addListener('scrollTo', callback);
|
||||
// Ask for the current scroll position when we mount so we can attach ourselves to it.
|
||||
bridge.send('requestScrollPosition');
|
||||
return () => bridge.removeListener('scrollTo', callback);
|
||||
}, [bridge]);
|
||||
|
||||
const scrollTimer = useRef<null | TimeoutID>(null);
|
||||
|
||||
// TODO: useEffectEvent
|
||||
function sendScroll() {
|
||||
if (scrollTimer.current) {
|
||||
clearTimeout(scrollTimer.current);
|
||||
scrollTimer.current = null;
|
||||
}
|
||||
if (applyingScrollRef.current) {
|
||||
return;
|
||||
}
|
||||
const element = ref.current;
|
||||
if (element === null) {
|
||||
return;
|
||||
}
|
||||
const scale = scaleRef.current / element.clientWidth;
|
||||
const left = element.scrollLeft * scale;
|
||||
const top = element.scrollTop * scale;
|
||||
const right = left + element.clientWidth * scale;
|
||||
const bottom = top + element.clientHeight * scale;
|
||||
bridge.send('scrollTo', {left, top, right, bottom});
|
||||
}
|
||||
|
||||
// TODO: useEffectEvent
|
||||
function throttleScroll() {
|
||||
if (!scrollTimer.current) {
|
||||
// Periodically synchronize the scroll while scrolling.
|
||||
scrollTimer.current = setTimeout(sendScroll, 400);
|
||||
}
|
||||
}
|
||||
|
||||
function scrollEnd() {
|
||||
// Upon scrollend send it immediately.
|
||||
sendScroll();
|
||||
applyingScrollRef.current = false;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const element = ref.current;
|
||||
if (element === null) {
|
||||
return;
|
||||
}
|
||||
const scrollCallback = throttleScroll;
|
||||
const scrollEndCallback = scrollEnd;
|
||||
element.addEventListener('scroll', scrollCallback);
|
||||
element.addEventListener('scrollend', scrollEndCallback);
|
||||
return () => {
|
||||
element.removeEventListener('scroll', scrollCallback);
|
||||
element.removeEventListener('scrollend', scrollEndCallback);
|
||||
};
|
||||
}, [ref]);
|
||||
|
||||
return (
|
||||
<div className={className} ref={ref}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: Get this from the store directly.
|
||||
// The backend needs to keep a separate tree so that resuspending keeps Activity around.
|
||||
function useActivities(): $ReadOnlyArray<Element> {
|
||||
const activities = useMemo(() => {
|
||||
const items: Array<Element> = [];
|
||||
return items;
|
||||
}, []);
|
||||
|
||||
return activities;
|
||||
}
|
||||
|
||||
function SuspenseTab(_: {}) {
|
||||
const store = useContext(StoreContext);
|
||||
const {hideSettings} = useContext(OptionsContext);
|
||||
@@ -166,10 +292,10 @@ function SuspenseTab(_: {}) {
|
||||
initLayoutState,
|
||||
);
|
||||
|
||||
const activities = useActivities();
|
||||
// If there are no named Activity boundaries, we don't have any tree list and we should hide
|
||||
// both the panel and the button to toggle it. Since we currently don't support it yet, it's
|
||||
// always disabled.
|
||||
const treeListDisabled = true;
|
||||
// both the panel and the button to toggle it.
|
||||
const treeListDisabled = activities.length === 0;
|
||||
|
||||
const wrapperTreeRef = useRef<null | HTMLElement>(null);
|
||||
const resizeTreeRef = useRef<null | HTMLElement>(null);
|
||||
@@ -341,16 +467,18 @@ function SuspenseTab(_: {}) {
|
||||
}
|
||||
};
|
||||
|
||||
const scaleRef = useRef(0);
|
||||
|
||||
return (
|
||||
<SettingsModalContextController>
|
||||
<div className={styles.SuspenseTab} ref={wrapperTreeRef}>
|
||||
<div className={styles.TreeWrapper} ref={resizeTreeRef}>
|
||||
{treeListDisabled ? null : (
|
||||
<div
|
||||
className={styles.TreeList}
|
||||
className={styles.ActivityList}
|
||||
hidden={treeListHidden}
|
||||
ref={resizeTreeListRef}>
|
||||
<SuspenseTreeList />
|
||||
<ActivityList activities={activities} />
|
||||
</div>
|
||||
)}
|
||||
{treeListDisabled ? null : (
|
||||
@@ -388,9 +516,11 @@ function SuspenseTab(_: {}) {
|
||||
orientation="horizontal"
|
||||
/>
|
||||
</header>
|
||||
<div className={styles.Rects}>
|
||||
<SuspenseRects />
|
||||
</div>
|
||||
<SynchronizedScrollContainer
|
||||
className={styles.Rects}
|
||||
scaleRef={scaleRef}>
|
||||
<SuspenseRects scaleRef={scaleRef} />
|
||||
</SynchronizedScrollContainer>
|
||||
<footer className={styles.SuspenseTreeViewFooter}>
|
||||
<SuspenseTimeline />
|
||||
<div className={styles.SuspenseTreeViewFooterButtons}>
|
||||
|
||||
@@ -111,7 +111,7 @@ type Props = {
|
||||
function getInitialState(store: Store): SuspenseTreeState {
|
||||
const uniqueSuspendersOnly = true;
|
||||
const timeline =
|
||||
store.getSuspendableDocumentOrderSuspense(uniqueSuspendersOnly);
|
||||
store.getEndTimeOrDocumentOrderSuspense(uniqueSuspendersOnly);
|
||||
const timelineIndex = timeline.length - 1;
|
||||
const selectedSuspenseID =
|
||||
timelineIndex === -1 ? null : timeline[timelineIndex].id;
|
||||
@@ -182,7 +182,7 @@ function SuspenseTreeContextController({children}: Props): React.Node {
|
||||
}
|
||||
|
||||
// TODO: Handle different timeline modes (e.g. random order)
|
||||
const nextTimeline = store.getSuspendableDocumentOrderSuspense(
|
||||
const nextTimeline = store.getEndTimeOrDocumentOrderSuspense(
|
||||
state.uniqueSuspendersOnly,
|
||||
);
|
||||
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
export default function SuspenseTreeList(_: {}): React$Node {
|
||||
return <div>Activity slices not implemented yet</div>;
|
||||
}
|
||||
@@ -82,8 +82,9 @@ export const ComponentFilterDisplayName = 2;
|
||||
export const ComponentFilterLocation = 3;
|
||||
export const ComponentFilterHOC = 4;
|
||||
export const ComponentFilterEnvironmentName = 5;
|
||||
export const ComponentFilterActivitySlice = 6;
|
||||
|
||||
export type ComponentFilterType = 1 | 2 | 3 | 4 | 5;
|
||||
export type ComponentFilterType = 1 | 2 | 3 | 4 | 5 | 6;
|
||||
|
||||
// Hide all elements of types in this Set.
|
||||
// We hide host components only by default.
|
||||
@@ -115,11 +116,20 @@ export type EnvironmentNameComponentFilter = {
|
||||
value: string,
|
||||
};
|
||||
|
||||
export type ActivitySliceFilter = {
|
||||
type: 6,
|
||||
activityID: Element['id'],
|
||||
rendererID: number,
|
||||
isValid: boolean,
|
||||
isEnabled: boolean,
|
||||
};
|
||||
|
||||
export type ComponentFilter =
|
||||
| BooleanComponentFilter
|
||||
| ElementTypeComponentFilter
|
||||
| RegExpComponentFilter
|
||||
| EnvironmentNameComponentFilter;
|
||||
| EnvironmentNameComponentFilter
|
||||
| ActivitySliceFilter;
|
||||
|
||||
export type HookName = string | null;
|
||||
// Map of hook source ("<filename>:<line-number>:<column-number>") to name.
|
||||
@@ -196,6 +206,7 @@ export type Rect = {
|
||||
export type SuspenseTimelineStep = {
|
||||
id: SuspenseNode['id'], // TODO: Will become a group.
|
||||
environment: null | string,
|
||||
endTime: number,
|
||||
};
|
||||
|
||||
export type SuspenseNode = {
|
||||
@@ -207,6 +218,7 @@ export type SuspenseNode = {
|
||||
hasUniqueSuspenders: boolean,
|
||||
isSuspended: boolean,
|
||||
environments: Array<string>,
|
||||
endTime: number,
|
||||
};
|
||||
|
||||
// Serialized version of ReactIOInfo
|
||||
|
||||
39
packages/react-devtools-shared/src/utils.js
vendored
39
packages/react-devtools-shared/src/utils.js
vendored
@@ -33,6 +33,7 @@ import {
|
||||
TREE_OPERATION_SET_SUBTREE_MODE,
|
||||
TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS,
|
||||
TREE_OPERATION_UPDATE_TREE_BASE_DURATION,
|
||||
TREE_OPERATION_APPLIED_ACTIVITY_SLICE_CHANGE,
|
||||
LOCAL_STORAGE_COMPONENT_FILTER_PREFERENCES_KEY,
|
||||
LOCAL_STORAGE_OPEN_IN_EDITOR_URL,
|
||||
LOCAL_STORAGE_OPEN_IN_EDITOR_URL_PRESET,
|
||||
@@ -47,6 +48,7 @@ import {
|
||||
SUSPENSE_TREE_OPERATION_SUSPENDERS,
|
||||
} from './constants';
|
||||
import {
|
||||
ComponentFilterActivitySlice,
|
||||
ComponentFilterElementType,
|
||||
ComponentFilterLocation,
|
||||
ElementTypeHostComponent,
|
||||
@@ -432,16 +434,27 @@ export function printOperationsArray(operations: Array<number>) {
|
||||
for (let changeIndex = 0; changeIndex < changeLength; changeIndex++) {
|
||||
const id = operations[i++];
|
||||
const hasUniqueSuspenders = operations[i++] === 1;
|
||||
const endTime = operations[i++] / 1000;
|
||||
const isSuspended = operations[i++] === 1;
|
||||
const environmentNamesLength = operations[i++];
|
||||
i += environmentNamesLength;
|
||||
logs.push(
|
||||
`Suspense node ${id} unique suspenders set to ${String(hasUniqueSuspenders)} is suspended set to ${String(isSuspended)} with ${String(environmentNamesLength)} environments`,
|
||||
`Suspense node ${id} unique suspenders set to ${String(hasUniqueSuspenders)} ending at ${String(endTime)} is suspended set to ${String(isSuspended)} with ${String(environmentNamesLength)} environments`,
|
||||
);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case TREE_OPERATION_APPLIED_ACTIVITY_SLICE_CHANGE: {
|
||||
i++;
|
||||
const activitySliceIDChange = operations[i + 1];
|
||||
logs.push(
|
||||
activitySliceIDChange === 0
|
||||
? 'Reset applied activity slice'
|
||||
: 'Applied activity slice change to ' + activitySliceIDChange,
|
||||
);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw Error(`Unsupported Bridge operation "${operation}"`);
|
||||
}
|
||||
@@ -467,7 +480,7 @@ export function getSavedComponentFilters(): Array<ComponentFilter> {
|
||||
);
|
||||
if (raw != null) {
|
||||
const parsedFilters: Array<ComponentFilter> = JSON.parse(raw);
|
||||
return filterOutLocationComponentFilters(parsedFilters);
|
||||
return persistableComponentFilters(parsedFilters);
|
||||
}
|
||||
} catch (error) {}
|
||||
return getDefaultComponentFilters();
|
||||
@@ -478,16 +491,11 @@ export function setSavedComponentFilters(
|
||||
): void {
|
||||
localStorageSetItem(
|
||||
LOCAL_STORAGE_COMPONENT_FILTER_PREFERENCES_KEY,
|
||||
JSON.stringify(filterOutLocationComponentFilters(componentFilters)),
|
||||
JSON.stringify(persistableComponentFilters(componentFilters)),
|
||||
);
|
||||
}
|
||||
|
||||
// Following __debugSource removal from Fiber, the new approach for finding the source location
|
||||
// of a component, represented by the Fiber, is based on lazily generating and parsing component stack frames
|
||||
// To find the original location, React DevTools will perform symbolication, source maps are required for that.
|
||||
// In order to start filtering Fibers, we need to find location for all of them, which can't be done lazily.
|
||||
// Eager symbolication can become quite expensive for large applications.
|
||||
export function filterOutLocationComponentFilters(
|
||||
export function persistableComponentFilters(
|
||||
componentFilters: Array<ComponentFilter>,
|
||||
): Array<ComponentFilter> {
|
||||
// This is just an additional check to preserve the previous state
|
||||
@@ -496,7 +504,18 @@ export function filterOutLocationComponentFilters(
|
||||
return componentFilters;
|
||||
}
|
||||
|
||||
return componentFilters.filter(f => f.type !== ComponentFilterLocation);
|
||||
return componentFilters.filter(f => {
|
||||
return (
|
||||
// Following __debugSource removal from Fiber, the new approach for finding the source location
|
||||
// of a component, represented by the Fiber, is based on lazily generating and parsing component stack frames
|
||||
// To find the original location, React DevTools will perform symbolication, source maps are required for that.
|
||||
// In order to start filtering Fibers, we need to find location for all of them, which can't be done lazily.
|
||||
// Eager symbolication can become quite expensive for large applications.
|
||||
f.type !== ComponentFilterLocation &&
|
||||
// Activity slice filters are based on DevTools instance IDs which do not persist across sessions.
|
||||
f.type !== ComponentFilterActivitySlice
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const vscodeFilepath = 'vscode://file/{path}:{line}:{column}';
|
||||
|
||||
96
packages/react-devtools-shell/src/app/Segments/index.js
vendored
Normal file
96
packages/react-devtools-shell/src/app/Segments/index.js
vendored
Normal file
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
function deferred<T>(
|
||||
timeoutMS: number,
|
||||
resolvedValue: T,
|
||||
displayName: string,
|
||||
): Promise<T> {
|
||||
const promise = new Promise<T>(resolve => {
|
||||
setTimeout(() => resolve(resolvedValue), timeoutMS);
|
||||
});
|
||||
(promise as any).displayName = displayName;
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
const title = deferred(100, 'Segmented Page Title', 'title');
|
||||
const content = deferred(
|
||||
400,
|
||||
'This is the content of a segmented page. It loads in multiple parts.',
|
||||
'content',
|
||||
);
|
||||
function Page(): React.Node {
|
||||
return (
|
||||
<article>
|
||||
<h1>{title}</h1>
|
||||
<p>{content}</p>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
function InnerSegment({children}: {children: React.Node}): React.Node {
|
||||
return (
|
||||
<>
|
||||
<h3>Inner Segment</h3>
|
||||
<React.Suspense name="InnerSegment" fallback={<p>Loading...</p>}>
|
||||
<section>{children}</section>
|
||||
<p>After inner</p>
|
||||
</React.Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const cookies = deferred(200, 'Cookies: 🍪🍪🍪', 'cookies');
|
||||
function OuterSegment({children}: {children: React.Node}): React.Node {
|
||||
return (
|
||||
<>
|
||||
<h2>Outer Segment</h2>
|
||||
<React.Suspense name="OuterSegment" fallback={<p>Loading outer</p>}>
|
||||
<p>{cookies}</p>
|
||||
<div>{children}</div>
|
||||
<p>After outer</p>
|
||||
</React.Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Root({children}: {children: React.Node}): React.Node {
|
||||
return (
|
||||
<>
|
||||
<h1>Root Segment</h1>
|
||||
<React.Suspense name="Root" fallback={<p>Loading root</p>}>
|
||||
<main>{children}</main>
|
||||
<footer>After root</footer>
|
||||
</React.Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Segments(): React.Node {
|
||||
return (
|
||||
<React.Activity name="/" mode="visible">
|
||||
<Root>
|
||||
<React.Activity name="/outer/" mode="visible">
|
||||
<OuterSegment>
|
||||
<React.Activity name="/outer/inner" mode="visible">
|
||||
<InnerSegment>
|
||||
<React.Activity name="/outer/inner/page" mode="visible">
|
||||
<Page />
|
||||
</React.Activity>
|
||||
</InnerSegment>
|
||||
</React.Activity>
|
||||
</OuterSegment>
|
||||
</React.Activity>
|
||||
</Root>
|
||||
</React.Activity>
|
||||
);
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import ToDoList from './ToDoList';
|
||||
import Toggle from './Toggle';
|
||||
import ErrorBoundaries from './ErrorBoundaries';
|
||||
import PartiallyStrictApp from './PartiallyStrictApp';
|
||||
import Segments from './Segments';
|
||||
import SuspenseTree from './SuspenseTree';
|
||||
import TraceUpdatesTest from './TraceUpdatesTest';
|
||||
import {ignoreErrors, ignoreLogs, ignoreWarnings} from './console';
|
||||
@@ -114,6 +115,7 @@ function mountTestApp() {
|
||||
mountApp(DeeplyNestedComponents);
|
||||
mountApp(Iframe);
|
||||
mountApp(TraceUpdatesTest);
|
||||
mountApp(Segments);
|
||||
|
||||
if (shouldRenderLegacy) {
|
||||
mountLegacyApp(PartiallyStrictApp);
|
||||
|
||||
@@ -27,6 +27,6 @@
|
||||
"internal-ip": "^6.2.0",
|
||||
"minimist": "^1.2.3",
|
||||
"react-devtools-core": "7.0.1",
|
||||
"update-notifier": "^2.1.0"
|
||||
"update-notifier": "^5.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,8 @@ import {getParentHydrationBoundary} from './ReactFiberConfigDOM';
|
||||
|
||||
import {enableScopeAPI} from 'shared/ReactFeatureFlags';
|
||||
|
||||
import {enableInternalInstanceMap} from 'shared/ReactFeatureFlags';
|
||||
|
||||
const randomKey = Math.random().toString(36).slice(2);
|
||||
const internalInstanceKey = '__reactFiber$' + randomKey;
|
||||
const internalPropsKey = '__reactProps$' + randomKey;
|
||||
@@ -49,7 +51,32 @@ const internalRootNodeResourcesKey = '__reactResources$' + randomKey;
|
||||
const internalHoistableMarker = '__reactMarker$' + randomKey;
|
||||
const internalScrollTimer = '__reactScroll$' + randomKey;
|
||||
|
||||
type InstanceUnion =
|
||||
| Instance
|
||||
| TextInstance
|
||||
| SuspenseInstance
|
||||
| ActivityInstance
|
||||
| ReactScopeInstance
|
||||
| Container;
|
||||
|
||||
const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
|
||||
const internalInstanceMap:
|
||||
| WeakMap<InstanceUnion, Fiber>
|
||||
| Map<InstanceUnion, Fiber> = new PossiblyWeakMap();
|
||||
const internalPropsMap:
|
||||
| WeakMap<InstanceUnion, Props>
|
||||
| Map<InstanceUnion, Props> = new PossiblyWeakMap();
|
||||
|
||||
export function detachDeletedInstance(node: Instance): void {
|
||||
if (enableInternalInstanceMap) {
|
||||
internalInstanceMap.delete(node);
|
||||
internalPropsMap.delete(node);
|
||||
delete (node: any)[internalEventHandlersKey];
|
||||
delete (node: any)[internalEventHandlerListenersKey];
|
||||
delete (node: any)[internalEventHandlesSetKey];
|
||||
delete (node: any)[internalRootNodeResourcesKey];
|
||||
return;
|
||||
}
|
||||
// TODO: This function is only called on host components. I don't think all of
|
||||
// these fields are relevant.
|
||||
delete (node: any)[internalInstanceKey];
|
||||
@@ -68,6 +95,10 @@ export function precacheFiberNode(
|
||||
| ActivityInstance
|
||||
| ReactScopeInstance,
|
||||
): void {
|
||||
if (enableInternalInstanceMap) {
|
||||
internalInstanceMap.set(node, hostInst);
|
||||
return;
|
||||
}
|
||||
(node: any)[internalInstanceKey] = hostInst;
|
||||
}
|
||||
|
||||
@@ -95,7 +126,12 @@ export function isContainerMarkedAsRoot(node: Container): boolean {
|
||||
// HostRoot back. To get to the HostRoot, you need to pass a child of it.
|
||||
// The same thing applies to Suspense and Activity boundaries.
|
||||
export function getClosestInstanceFromNode(targetNode: Node): null | Fiber {
|
||||
let targetInst = (targetNode: any)[internalInstanceKey];
|
||||
let targetInst: void | Fiber;
|
||||
if (enableInternalInstanceMap) {
|
||||
targetInst = internalInstanceMap.get(((targetNode: any): InstanceUnion));
|
||||
} else {
|
||||
targetInst = (targetNode: any)[internalInstanceKey];
|
||||
}
|
||||
if (targetInst) {
|
||||
// Don't return HostRoot, SuspenseComponent or ActivityComponent here.
|
||||
return targetInst;
|
||||
@@ -112,9 +148,15 @@ export function getClosestInstanceFromNode(targetNode: Node): null | Fiber {
|
||||
// itself because the fibers are conceptually between the container
|
||||
// node and the first child. It isn't surrounding the container node.
|
||||
// If it's not a container, we check if it's an instance.
|
||||
targetInst =
|
||||
(parentNode: any)[internalContainerInstanceKey] ||
|
||||
(parentNode: any)[internalInstanceKey];
|
||||
if (enableInternalInstanceMap) {
|
||||
targetInst =
|
||||
(parentNode: any)[internalContainerInstanceKey] ||
|
||||
internalInstanceMap.get(((parentNode: any): InstanceUnion));
|
||||
} else {
|
||||
targetInst =
|
||||
(parentNode: any)[internalContainerInstanceKey] ||
|
||||
(parentNode: any)[internalInstanceKey];
|
||||
}
|
||||
if (targetInst) {
|
||||
// Since this wasn't the direct target of the event, we might have
|
||||
// stepped past dehydrated DOM nodes to get here. However they could
|
||||
@@ -147,8 +189,10 @@ export function getClosestInstanceFromNode(targetNode: Node): null | Fiber {
|
||||
// have had an internalInstanceKey on it.
|
||||
// Let's get the fiber associated with the SuspenseComponent
|
||||
// as the deepest instance.
|
||||
// $FlowFixMe[prop-missing]
|
||||
const targetFiber = hydrationInstance[internalInstanceKey];
|
||||
const targetFiber = enableInternalInstanceMap
|
||||
? internalInstanceMap.get(hydrationInstance)
|
||||
: // $FlowFixMe[prop-missing]
|
||||
hydrationInstance[internalInstanceKey];
|
||||
if (targetFiber) {
|
||||
return targetFiber;
|
||||
}
|
||||
@@ -175,9 +219,16 @@ export function getClosestInstanceFromNode(targetNode: Node): null | Fiber {
|
||||
* instance, or null if the node was not rendered by this React.
|
||||
*/
|
||||
export function getInstanceFromNode(node: Node): Fiber | null {
|
||||
const inst =
|
||||
(node: any)[internalInstanceKey] ||
|
||||
(node: any)[internalContainerInstanceKey];
|
||||
let inst: void | null | Fiber;
|
||||
if (enableInternalInstanceMap) {
|
||||
inst =
|
||||
internalInstanceMap.get(((node: any): InstanceUnion)) ||
|
||||
(node: any)[internalContainerInstanceKey];
|
||||
} else {
|
||||
inst =
|
||||
(node: any)[internalInstanceKey] ||
|
||||
(node: any)[internalContainerInstanceKey];
|
||||
}
|
||||
if (inst) {
|
||||
const tag = inst.tag;
|
||||
if (
|
||||
@@ -226,16 +277,25 @@ export function getFiberCurrentPropsFromNode(
|
||||
| TextInstance
|
||||
| SuspenseInstance
|
||||
| ActivityInstance,
|
||||
): Props {
|
||||
): Props | null {
|
||||
if (enableInternalInstanceMap) {
|
||||
return internalPropsMap.get(node) || null;
|
||||
}
|
||||
return (node: any)[internalPropsKey] || null;
|
||||
}
|
||||
|
||||
export function updateFiberProps(node: Instance, props: Props): void {
|
||||
if (enableInternalInstanceMap) {
|
||||
internalPropsMap.set(node, props);
|
||||
return;
|
||||
}
|
||||
(node: any)[internalPropsKey] = props;
|
||||
}
|
||||
|
||||
export function getEventListenerSet(node: EventTarget): Set<string> {
|
||||
let elementListenerSet = (node: any)[internalEventHandlersKey];
|
||||
let elementListenerSet: Set<string> | void = (node: any)[
|
||||
internalEventHandlersKey
|
||||
];
|
||||
if (elementListenerSet === undefined) {
|
||||
elementListenerSet = (node: any)[internalEventHandlersKey] = new Set();
|
||||
}
|
||||
@@ -246,6 +306,9 @@ export function getFiberFromScopeInstance(
|
||||
scope: ReactScopeInstance,
|
||||
): null | Fiber {
|
||||
if (enableScopeAPI) {
|
||||
if (enableInternalInstanceMap) {
|
||||
return internalInstanceMap.get(((scope: any): InstanceUnion)) || null;
|
||||
}
|
||||
return (scope: any)[internalInstanceKey] || null;
|
||||
}
|
||||
return null;
|
||||
@@ -318,6 +381,12 @@ export function clearScrollEndTimer(node: EventTarget): void {
|
||||
}
|
||||
|
||||
export function isOwnedInstance(node: Node): boolean {
|
||||
if (enableInternalInstanceMap) {
|
||||
return !!(
|
||||
(node: any)[internalHoistableMarker] ||
|
||||
internalInstanceMap.has((node: any))
|
||||
);
|
||||
}
|
||||
return !!(
|
||||
(node: any)[internalHoistableMarker] || (node: any)[internalInstanceKey]
|
||||
);
|
||||
|
||||
@@ -126,6 +126,7 @@ import {
|
||||
enableHydrationChangeEvent,
|
||||
enableFragmentRefsScrollIntoView,
|
||||
enableProfilerTimer,
|
||||
enableFragmentRefsInstanceHandles,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
import {
|
||||
HostComponent,
|
||||
@@ -214,6 +215,10 @@ export type Container =
|
||||
export type Instance = Element;
|
||||
export type TextInstance = Text;
|
||||
|
||||
type InstanceWithFragmentHandles = Instance & {
|
||||
unstable_reactFragments?: Set<FragmentInstanceType>,
|
||||
};
|
||||
|
||||
declare class ActivityInterface extends Comment {}
|
||||
declare class SuspenseInterface extends Comment {
|
||||
_reactRetry: void | (() => void);
|
||||
@@ -1430,8 +1435,13 @@ export function applyViewTransitionName(
|
||||
className: ?string,
|
||||
): void {
|
||||
instance = ((instance: any): HTMLElement);
|
||||
// If the name isn't valid CSS identifier, base64 encode the name instead.
|
||||
// This doesn't let you select it in custom CSS selectors but it does work in current
|
||||
// browsers.
|
||||
const escapedName =
|
||||
CSS.escape(name) !== name ? 'r-' + btoa(name).replace(/=/g, '') : name;
|
||||
// $FlowFixMe[prop-missing]
|
||||
instance.style.viewTransitionName = name;
|
||||
instance.style.viewTransitionName = escapedName;
|
||||
if (className != null) {
|
||||
// $FlowFixMe[prop-missing]
|
||||
instance.style.viewTransitionClass = className;
|
||||
@@ -3390,10 +3400,44 @@ if (enableFragmentRefsScrollIntoView) {
|
||||
};
|
||||
}
|
||||
|
||||
function addFragmentHandleToFiber(
|
||||
child: Fiber,
|
||||
fragmentInstance: FragmentInstanceType,
|
||||
): boolean {
|
||||
if (enableFragmentRefsInstanceHandles) {
|
||||
const instance =
|
||||
getInstanceFromHostFiber<InstanceWithFragmentHandles>(child);
|
||||
if (instance != null) {
|
||||
addFragmentHandleToInstance(instance, fragmentInstance);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function addFragmentHandleToInstance(
|
||||
instance: InstanceWithFragmentHandles,
|
||||
fragmentInstance: FragmentInstanceType,
|
||||
): void {
|
||||
if (enableFragmentRefsInstanceHandles) {
|
||||
if (instance.unstable_reactFragments == null) {
|
||||
instance.unstable_reactFragments = new Set();
|
||||
}
|
||||
instance.unstable_reactFragments.add(fragmentInstance);
|
||||
}
|
||||
}
|
||||
|
||||
export function createFragmentInstance(
|
||||
fragmentFiber: Fiber,
|
||||
): FragmentInstanceType {
|
||||
return new (FragmentInstance: any)(fragmentFiber);
|
||||
const fragmentInstance = new (FragmentInstance: any)(fragmentFiber);
|
||||
if (enableFragmentRefsInstanceHandles) {
|
||||
traverseFragmentInstance(
|
||||
fragmentFiber,
|
||||
addFragmentHandleToFiber,
|
||||
fragmentInstance,
|
||||
);
|
||||
}
|
||||
return fragmentInstance;
|
||||
}
|
||||
|
||||
export function updateFragmentInstanceFiber(
|
||||
@@ -3404,7 +3448,7 @@ export function updateFragmentInstanceFiber(
|
||||
}
|
||||
|
||||
export function commitNewChildToFragmentInstance(
|
||||
childInstance: Instance,
|
||||
childInstance: InstanceWithFragmentHandles,
|
||||
fragmentInstance: FragmentInstanceType,
|
||||
): void {
|
||||
const eventListeners = fragmentInstance._eventListeners;
|
||||
@@ -3419,17 +3463,25 @@ export function commitNewChildToFragmentInstance(
|
||||
observer.observe(childInstance);
|
||||
});
|
||||
}
|
||||
if (enableFragmentRefsInstanceHandles) {
|
||||
addFragmentHandleToInstance(childInstance, fragmentInstance);
|
||||
}
|
||||
}
|
||||
|
||||
export function deleteChildFromFragmentInstance(
|
||||
childElement: Instance,
|
||||
childInstance: InstanceWithFragmentHandles,
|
||||
fragmentInstance: FragmentInstanceType,
|
||||
): void {
|
||||
const eventListeners = fragmentInstance._eventListeners;
|
||||
if (eventListeners !== null) {
|
||||
for (let i = 0; i < eventListeners.length; i++) {
|
||||
const {type, listener, optionsOrUseCapture} = eventListeners[i];
|
||||
childElement.removeEventListener(type, listener, optionsOrUseCapture);
|
||||
childInstance.removeEventListener(type, listener, optionsOrUseCapture);
|
||||
}
|
||||
}
|
||||
if (enableFragmentRefsInstanceHandles) {
|
||||
if (childInstance.unstable_reactFragments != null) {
|
||||
childInstance.unstable_reactFragments.delete(fragmentInstance);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -341,7 +341,6 @@ export function getEventPriority(domEventName: DOMEventName): EventPriority {
|
||||
case 'pointerup':
|
||||
case 'ratechange':
|
||||
case 'reset':
|
||||
case 'resize':
|
||||
case 'seeked':
|
||||
case 'submit':
|
||||
case 'toggle':
|
||||
@@ -380,6 +379,7 @@ export function getEventPriority(domEventName: DOMEventName): EventPriority {
|
||||
case 'pointermove':
|
||||
case 'pointerout':
|
||||
case 'pointerover':
|
||||
case 'resize':
|
||||
case 'scroll':
|
||||
case 'touchmove':
|
||||
case 'wheel':
|
||||
|
||||
@@ -8,7 +8,7 @@ export const clientRenderBoundary =
|
||||
export const completeBoundary =
|
||||
'$RB=[];$RV=function(a){$RT=performance.now();for(var b=0;b<a.length;b+=2){var c=a[b],e=a[b+1];null!==e.parentNode&&e.parentNode.removeChild(e);var f=c.parentNode;if(f){var g=c.previousSibling,h=0;do{if(c&&8===c.nodeType){var d=c.data;if("/$"===d||"/&"===d)if(0===h)break;else h--;else"$"!==d&&"$?"!==d&&"$~"!==d&&"$!"!==d&&"&"!==d||h++}d=c.nextSibling;f.removeChild(c);c=d}while(c);for(;e.firstChild;)f.insertBefore(e.firstChild,c);g.data="$";g._reactRetry&&requestAnimationFrame(g._reactRetry)}}a.length=0};\n$RC=function(a,b){if(b=document.getElementById(b))(a=document.getElementById(a))?(a.previousSibling.data="$~",$RB.push(a,b),2===$RB.length&&("number"!==typeof $RT?requestAnimationFrame($RV.bind(null,$RB)):(a=performance.now(),setTimeout($RV.bind(null,$RB),2300>a&&2E3<a?2300-a:$RT+300-a)))):b.parentNode.removeChild(b)};';
|
||||
export const completeBoundaryUpgradeToViewTransitions =
|
||||
'$RV=function(A,g){function k(a,b){var e=a.getAttribute(b);e&&(b=a.style,l.push(a,b.viewTransitionName,b.viewTransitionClass),"auto"!==e&&(b.viewTransitionClass=e),(a=a.getAttribute("vt-name"))||(a="_T_"+K++ +"_"),b.viewTransitionName=a,B=!0)}var B=!1,K=0,l=[];try{var f=document.__reactViewTransition;if(f){f.finished.finally($RV.bind(null,g));return}var m=new Map;for(f=1;f<g.length;f+=2)for(var h=g[f].querySelectorAll("[vt-share]"),d=0;d<h.length;d++){var c=h[d];m.set(c.getAttribute("vt-name"),c)}var u=[];for(h=0;h<g.length;h+=2){var C=g[h],x=C.parentNode;if(x){var v=x.getBoundingClientRect();if(v.left||v.top||v.width||v.height){c=C;for(f=0;c;){if(8===c.nodeType){var r=c.data;if("/$"===r)if(0===f)break;else f--;else"$"!==r&&"$?"!==r&&"$~"!==r&&"$!"!==r||f++}else if(1===c.nodeType){d=c;var D=d.getAttribute("vt-name"),y=m.get(D);k(d,y?"vt-share":"vt-exit");y&&(k(y,"vt-share"),m.set(D,null));var E=d.querySelectorAll("[vt-share]");for(d=0;d<E.length;d++){var F=E[d],G=F.getAttribute("vt-name"),\nH=m.get(G);H&&(k(F,"vt-share"),k(H,"vt-share"),m.set(G,null))}}c=c.nextSibling}for(var I=g[h+1],t=I.firstElementChild;t;)null!==m.get(t.getAttribute("vt-name"))&&k(t,"vt-enter"),t=t.nextElementSibling;c=x;do for(var n=c.firstElementChild;n;){var J=n.getAttribute("vt-update");J&&"none"!==J&&!l.includes(n)&&k(n,"vt-update");n=n.nextElementSibling}while((c=c.parentNode)&&1===c.nodeType&&"none"!==c.getAttribute("vt-update"));u.push.apply(u,I.querySelectorAll(\'img[src]:not([loading="lazy"])\'))}}}if(B){var z=\ndocument.__reactViewTransition=document.startViewTransition({update:function(){A(g);for(var a=[document.documentElement.clientHeight,document.fonts.ready],b={},e=0;e<u.length;b={g:b.g},e++)if(b.g=u[e],!b.g.complete){var p=b.g.getBoundingClientRect();0<p.bottom&&0<p.right&&p.top<window.innerHeight&&p.left<window.innerWidth&&(p=new Promise(function(w){return function(q){w.g.addEventListener("load",q);w.g.addEventListener("error",q)}}(b)),a.push(p))}return Promise.race([Promise.all(a),new Promise(function(w){var q=\nperformance.now();setTimeout(w,2300>q&&2E3<q?2300-q:500)})])},types:[]});z.ready.finally(function(){for(var a=l.length-3;0<=a;a-=3){var b=l[a],e=b.style;e.viewTransitionName=l[a+1];e.viewTransitionClass=l[a+1];""===b.getAttribute("style")&&b.removeAttribute("style")}});z.finished.finally(function(){document.__reactViewTransition===z&&(document.__reactViewTransition=null)});$RB=[];return}}catch(a){}A(g)}.bind(null,$RV);';
|
||||
'$RV=function(A,g){function k(a,b){var e=a.getAttribute(b);e&&(b=a.style,l.push(a,b.viewTransitionName,b.viewTransitionClass),"auto"!==e&&(b.viewTransitionClass=e),(a=a.getAttribute("vt-name"))||(a="_T_"+K++ +"_"),a=CSS.escape(a)!==a?"r-"+btoa(a).replace(/=/g,""):a,b.viewTransitionName=a,B=!0)}var B=!1,K=0,l=[];try{var f=document.__reactViewTransition;if(f){f.finished.finally($RV.bind(null,g));return}var m=new Map;for(f=1;f<g.length;f+=2)for(var h=g[f].querySelectorAll("[vt-share]"),d=0;d<h.length;d++){var c=h[d];m.set(c.getAttribute("vt-name"),c)}var u=[];for(h=0;h<g.length;h+=2){var C=g[h],x=C.parentNode;if(x){var v=x.getBoundingClientRect();if(v.left||v.top||v.width||v.height){c=C;for(f=0;c;){if(8===c.nodeType){var r=c.data;if("/$"===r)if(0===f)break;else f--;else"$"!==r&&"$?"!==r&&"$~"!==r&&"$!"!==r||f++}else if(1===c.nodeType){d=c;var D=d.getAttribute("vt-name"),y=m.get(D);k(d,y?"vt-share":"vt-exit");y&&(k(y,"vt-share"),m.set(D,null));var E=d.querySelectorAll("[vt-share]");\nfor(d=0;d<E.length;d++){var F=E[d],G=F.getAttribute("vt-name"),H=m.get(G);H&&(k(F,"vt-share"),k(H,"vt-share"),m.set(G,null))}}c=c.nextSibling}for(var I=g[h+1],t=I.firstElementChild;t;)null!==m.get(t.getAttribute("vt-name"))&&k(t,"vt-enter"),t=t.nextElementSibling;c=x;do for(var n=c.firstElementChild;n;){var J=n.getAttribute("vt-update");J&&"none"!==J&&!l.includes(n)&&k(n,"vt-update");n=n.nextElementSibling}while((c=c.parentNode)&&1===c.nodeType&&"none"!==c.getAttribute("vt-update"));u.push.apply(u,\nI.querySelectorAll(\'img[src]:not([loading="lazy"])\'))}}}if(B){var z=document.__reactViewTransition=document.startViewTransition({update:function(){A(g);for(var a=[document.documentElement.clientHeight,document.fonts.ready],b={},e=0;e<u.length;b={g:b.g},e++)if(b.g=u[e],!b.g.complete){var p=b.g.getBoundingClientRect();0<p.bottom&&0<p.right&&p.top<window.innerHeight&&p.left<window.innerWidth&&(p=new Promise(function(w){return function(q){w.g.addEventListener("load",q);w.g.addEventListener("error",q)}}(b)),\na.push(p))}return Promise.race([Promise.all(a),new Promise(function(w){var q=performance.now();setTimeout(w,2300>q&&2E3<q?2300-q:500)})])},types:[]});z.ready.finally(function(){for(var a=l.length-3;0<=a;a-=3){var b=l[a],e=b.style;e.viewTransitionName=l[a+1];e.viewTransitionClass=l[a+1];""===b.getAttribute("style")&&b.removeAttribute("style")}});z.finished.finally(function(){document.__reactViewTransition===z&&(document.__reactViewTransition=null)});$RB=[];return}}catch(a){}A(g)}.bind(null,\n$RV);';
|
||||
export const completeBoundaryWithStyles =
|
||||
'$RM=new Map;$RR=function(n,w,p){function u(q){this._p=null;q()}for(var r=new Map,t=document,h,b,e=t.querySelectorAll("link[data-precedence],style[data-precedence]"),v=[],k=0;b=e[k++];)"not all"===b.getAttribute("media")?v.push(b):("LINK"===b.tagName&&$RM.set(b.getAttribute("href"),b),r.set(b.dataset.precedence,h=b));e=0;b=[];var l,a;for(k=!0;;){if(k){var f=p[e++];if(!f){k=!1;e=0;continue}var c=!1,m=0;var d=f[m++];if(a=$RM.get(d)){var g=a._p;c=!0}else{a=t.createElement("link");a.href=d;a.rel=\n"stylesheet";for(a.dataset.precedence=l=f[m++];g=f[m++];)a.setAttribute(g,f[m++]);g=a._p=new Promise(function(q,x){a.onload=u.bind(a,q);a.onerror=u.bind(a,x)});$RM.set(d,a)}d=a.getAttribute("media");!g||d&&!matchMedia(d).matches||b.push(g);if(c)continue}else{a=v[e++];if(!a)break;l=a.getAttribute("data-precedence");a.removeAttribute("media")}c=r.get(l)||h;c===h&&(h=a);r.set(l,a);c?c.parentNode.insertBefore(a,c.nextSibling):(c=t.head,c.insertBefore(a,c.firstChild))}if(p=document.getElementById(n))p.previousSibling.data=\n"$~";Promise.all(b).then($RC.bind(null,n,w),$RX.bind(null,n,"CSS failed to load"))};';
|
||||
export const completeSegment =
|
||||
|
||||
@@ -130,7 +130,12 @@ export function revealCompletedBoundariesWithViewTransitions(
|
||||
const idPrefix = '';
|
||||
name = '_' + idPrefix + 'T_' + autoNameIdx++ + '_';
|
||||
}
|
||||
elementStyle['viewTransitionName'] = name;
|
||||
// If the name isn't valid CSS identifier, base64 encode the name instead.
|
||||
// This doesn't let you select it in custom CSS selectors but it does work in current
|
||||
// browsers.
|
||||
const escapedName =
|
||||
CSS.escape(name) !== name ? 'r-' + btoa(name).replace(/=/g, '') : name;
|
||||
elementStyle['viewTransitionName'] = escapedName;
|
||||
shouldStartViewTransition = true;
|
||||
}
|
||||
try {
|
||||
|
||||
@@ -12,6 +12,8 @@ if (process.env.NODE_ENV === 'production') {
|
||||
|
||||
exports.version = b.version;
|
||||
exports.renderToReadableStream = b.renderToReadableStream;
|
||||
exports.renderToPipeableStream = b.renderToPipeableStream;
|
||||
exports.resumeToPipeableStream = b.resumeToPipeableStream;
|
||||
exports.resume = b.resume;
|
||||
exports.renderToString = l.renderToString;
|
||||
exports.renderToStaticMarkup = l.renderToStaticMarkup;
|
||||
|
||||
@@ -38,3 +38,17 @@ export function resume() {
|
||||
arguments,
|
||||
);
|
||||
}
|
||||
|
||||
export function renderToPipeableStream() {
|
||||
return require('./src/server/react-dom-server.bun').renderToPipeableStream.apply(
|
||||
this,
|
||||
arguments,
|
||||
);
|
||||
}
|
||||
|
||||
export function resumeToPipeableStream() {
|
||||
return require('./src/server/react-dom-server.bun').resumeToPipeableStream.apply(
|
||||
this,
|
||||
arguments,
|
||||
);
|
||||
}
|
||||
|
||||
565
packages/react-dom/src/__tests__/ReactDOMActivity-test.js
vendored
Normal file
565
packages/react-dom/src/__tests__/ReactDOMActivity-test.js
vendored
Normal file
@@ -0,0 +1,565 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @emails react-core
|
||||
*/
|
||||
|
||||
'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;
|
||||
|
||||
describe('ReactDOMActivity', () => {
|
||||
let container;
|
||||
|
||||
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;
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
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 ' +
|
||||
'portals it contains, regardless of how deeply nested they are',
|
||||
async () => {
|
||||
const portalContainer = document.createElement('div');
|
||||
|
||||
let setShow;
|
||||
function Accordion({children}) {
|
||||
const [shouldShow, _setShow] = useState(true);
|
||||
setShow = _setShow;
|
||||
return (
|
||||
<Activity mode={shouldShow ? 'visible' : 'hidden'}>
|
||||
{children}
|
||||
</Activity>
|
||||
);
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Accordion>
|
||||
<div>
|
||||
{ReactDOM.createPortal(
|
||||
<div>Portal contents</div>,
|
||||
portalContainer,
|
||||
)}
|
||||
</div>
|
||||
</Accordion>
|
||||
);
|
||||
}
|
||||
|
||||
const root = ReactDOMClient.createRoot(container);
|
||||
await act(() => root.render(<App />));
|
||||
expect(container.innerHTML).toBe('<div></div>');
|
||||
expect(portalContainer.innerHTML).toBe('<div>Portal contents</div>');
|
||||
|
||||
// Hide the Activity boundary. Not only are the nearest DOM elements hidden,
|
||||
// but also the children of the nested portal contained within it.
|
||||
await act(() => setShow(false));
|
||||
expect(container.innerHTML).toBe('<div style="display: none;"></div>');
|
||||
expect(portalContainer.innerHTML).toBe(
|
||||
'<div style="display: none;">Portal contents</div>',
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
// @gate enableActivity
|
||||
it(
|
||||
'revealing an Activity boundary inside a portal does not reveal the ' +
|
||||
'portal contents if has a hidden Activity parent',
|
||||
async () => {
|
||||
const portalContainer = document.createElement('div');
|
||||
|
||||
let setShow;
|
||||
function Accordion({children}) {
|
||||
const [shouldShow, _setShow] = useState(false);
|
||||
setShow = _setShow;
|
||||
return (
|
||||
<Activity mode={shouldShow ? 'visible' : 'hidden'}>
|
||||
{children}
|
||||
</Activity>
|
||||
);
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Activity mode="hidden">
|
||||
<div>
|
||||
{ReactDOM.createPortal(
|
||||
<Accordion>
|
||||
<div>Portal contents</div>
|
||||
</Accordion>,
|
||||
portalContainer,
|
||||
)}
|
||||
</div>
|
||||
</Activity>
|
||||
);
|
||||
}
|
||||
|
||||
// Start with both boundaries hidden.
|
||||
const root = ReactDOMClient.createRoot(container);
|
||||
await act(() => root.render(<App />));
|
||||
expect(container.innerHTML).toBe('<div style="display: none;"></div>');
|
||||
expect(portalContainer.innerHTML).toBe(
|
||||
'<div style="display: none;">Portal contents</div>',
|
||||
);
|
||||
|
||||
// Reveal the inner Activity boundary. It should not reveal its children,
|
||||
// because there's a parent Activity boundary that is still hidden.
|
||||
await act(() => setShow(true));
|
||||
expect(container.innerHTML).toBe('<div style="display: none;"></div>');
|
||||
expect(portalContainer.innerHTML).toBe(
|
||||
'<div style="display: none;">Portal contents</div>',
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
// @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>');
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user