Compare commits

..

11 Commits

Author SHA1 Message Date
Jorge Cabiedes Acosta
91e739e950 [compiler] Don't validate when effect cleanup function depends on effect localized setState state derived values
Summary:
If we are using a clean up function in an effect and that clean up function depends on a value that is used to set the state we are validating for we shouldn't throw an error since it is a valid use case for an effect.

Test Plan:
added test
2025-11-10 12:27:30 -08:00
Jorge Cabiedes
f76c3617e0 [compiler] Switch to track setStates by aliasing and id instead of identifier names (#34973)
Summary:
This makes the setState usage logic much more robust. We no longer rely
on identifierName.

Now we track when a setState is loaded into a new promoted identifier
variable and track this in a map `setStateLoaded` map.

For other types of instructions we consider the setState to be being
used. In this case we record its usage into the `setStateUsages` map.



Test Plan:
We expect no changes in behavior for the current tests

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34973).
* #35044
* #35020
* __->__ #34973
* #34972
2025-11-10 12:16:27 -08:00
Jorge Cabiedes
7296120396 [compiler] Update ValidateNoDerivedComputationsInEffects_exp to log the error instead of throwing (#34972)
Summary:
TSIA

Simple change to log errors in Pipeline.ts instead of throwing in the
validation

Test Plan:
updated snap tests

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34972).
* #35044
* #35020
* #34973
* __->__ #34972
2025-11-10 12:16:13 -08:00
Jorge Cabiedes
6347c6d373 [compiler] Fix false negatives and add data flow tree to compiler error for no-deriving-state-in-effects (#34995)
Summary:
Revamped the derivationCache graph.

This fixes a bunch of bugs where sometimes we fail to track from which
props/state we derived values from.

Also, it is more intuitive and allows us to easily implement a Data Flow
Tree.

We can print this tree which gives insight on how the data is derived
and should facilitate error resolution in complicated components

Test Plan:
Added a test case where we were failing to track derivations. Also
updated the test cases with the new error containing the data flow tree

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34995).
* #35044
* #35020
* #34973
* #34972
* __->__ #34995
* #34967
2025-11-10 12:09:13 -08:00
Jorge Cabiedes
01fb328632 [compiler] Prevent overriding a derivationEntry on effect mutation and instead update typeOfValue and fix infinite loops (#34967)
Summary:
With this we are now comparing a snapshot of the derivationCache with
the new changes every time we are done recording the derivations
happening in the HIR.

We have to do this after recording everything since we still do some
mutations on the cache when recording mutations.



Test Plan:
Test the following in playground:
```
// @validateNoDerivedComputationsInEffects_exp

function Component({ value }) {
  const [checked, setChecked] = useState('');

  useEffect(() => {
    setChecked(value === '' ? [] : value.split(','));
  }, [value]);

  return (
    <div>{checked}</div>
  )
}
```

This no longer causes an infinite loop.

Added a test case in the next PR in the stack

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34967).
* #35044
* #35020
* #34973
* #34972
* #34995
* __->__ #34967
2025-11-10 12:08:05 -08:00
Sebastian "Sebbie" Silbermann
ce4054ebdd [DevTools] Measure when reconnecting Suspense (#35098) 2025-11-10 20:55:31 +01:00
Sebastian "Sebbie" Silbermann
21c1d51acb [DevTools] Don't attempt to draw bounding box if inspected element is not a Suspense (#35097) 2025-11-10 20:01:59 +01:00
Facebook Community Bot
be48396dbd Remove Dead Code in WWW JS
Differential Revision: D86593830

Pull Request resolved: https://github.com/facebook/react/pull/35085
2025-11-10 16:34:01 +00:00
Andrew Clark
5268492536 Fix: Activity should hide portal contents (#35091)
This PR updates the behavior of Activity so that when it is hidden, it
hides the contents of any portals contained within it.

Previously we had intentionally chosen not to implement this behavior,
because it was thought that this concern should be left to the userspace
code that manages the portal, e.g. by adding or removing the portal
container from the DOM. Depending on the use case for the portal, this
is often desirable anyway because the portal container itself is not
controlled by React.

However, React does own the _contents_ of the portal, and we can hide
those elements regardless of what the user chooses to do with the
container. This makes the hiding/unhiding behavior of portals with
Activity automatic in the majority of cases, and also benefits from
aligning the DOM mutations with the rest of the React's commit phase
lifecycle.

The reason we have to special case this at all is because usually we
only hide the direct DOM children of the Activity boundary. There's no
reason to go deeper than that, because hiding a parent DOM element
effectively hides everything inside of it. Portals are the exception,
because they don't exist in the normal DOM hierarchy; we can't assume
that just because a portal has a parent in the React tree that it will
also have that parent in the actual DOM.

So, whenever an Activity boundary is hidden, we must search for and hide
_any_ portal that is contained within it, and recursively hide its
direct children, too.

To optimize this search, we use a new subtree flag, PortalStatic, that
is set only on fiber paths that contain a HostPortal. This lets us skip
over any subtree that does not contain a portal.
2025-11-10 10:42:26 -05:00
Sebastian Markbåge
c83be7da9f [Fizz] Simplify createSuspenseBoundary path (#35087)
Small follow up to #35068.

Since this is now a single argument we can simplify the creation
branching a bit and make sure it's const.
2025-11-09 15:19:43 -05:00
Sebastian Markbåge
6362b5c711 [DevTools] Special case the selected root outline (#35071)
When I moved the outline to above all other rects, I thought it was
clever to unify with the root so that the outline was also used for the
root selection. But the root outline is not drawn like the other rects.
It's outside the padding and doesn't have the 1px adjustment which leads
the overlay to be slightly inside the other rect instead of above it.

This goes back to just having the selected root be drawn by the root
element.

Before:

<img width="652" height="253" alt="Screenshot 2025-11-07 at 11 39 28 AM"
src="https://github.com/user-attachments/assets/334237d1-f190-4995-94cc-9690ec0f7ce1"
/>

After:

<img width="674" height="220" alt="Screenshot 2025-11-07 at 11 44 01 AM"
src="https://github.com/user-attachments/assets/afaa86d8-942a-44d8-a1a5-67c7fb642c0d"
/>
2025-11-09 15:03:31 -05:00
55 changed files with 1768 additions and 670 deletions

View File

@@ -277,7 +277,7 @@ function runWithEnvironment(
}
if (env.config.validateNoDerivedComputationsInEffects_exp) {
validateNoDerivedComputationsInEffects_exp(hir);
env.logErrors(validateNoDerivedComputationsInEffects_exp(hir));
}
if (env.config.validateNoSetStateInEffects) {

View File

@@ -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,7 +21,6 @@ import {
isUseStateType,
BasicBlock,
isUseRefType,
GeneratedSource,
SourceLocation,
} from '../HIR';
import {eachInstructionLValue, eachInstructionOperand} from '../HIR/visitors';
@@ -33,6 +33,7 @@ type DerivationMetadata = {
typeOfValue: TypeOfValue;
place: Place;
sourcesIds: Set<IdentifierId>;
isStateSource: boolean;
};
type ValidationContext = {
@@ -40,13 +41,51 @@ type ValidationContext = {
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 setStateLoads: Map<IdentifierId, IdentifierId | null>;
readonly setStateUsages: Map<IdentifierId, Set<SourceLocation>>;
};
class DerivationCache {
hasChanges: boolean = false;
cache: Map<IdentifierId, DerivationMetadata> = new Map();
private previousCache: Map<IdentifierId, DerivationMetadata> | null = null;
takeSnapshot(): void {
this.previousCache = new Map();
for (const [key, value] of this.cache.entries()) {
this.previousCache.set(key, {
place: value.place,
sourcesIds: new Set(value.sourcesIds),
typeOfValue: value.typeOfValue,
isStateSource: value.isStateSource,
});
}
}
checkForChanges(): void {
if (this.previousCache === null) {
this.hasChanges = true;
return;
}
for (const [key, value] of this.cache.entries()) {
const previousValue = this.previousCache.get(key);
if (
previousValue === undefined ||
!this.isDerivationEqual(previousValue, value)
) {
this.hasChanges = true;
return;
}
}
if (this.cache.size !== this.previousCache.size) {
this.hasChanges = true;
return;
}
this.hasChanges = false;
}
snapshot(): boolean {
const hasChanges = this.hasChanges;
@@ -58,48 +97,28 @@ class DerivationCache {
derivedVar: Place,
sourcesIds: Set<IdentifierId>,
typeOfValue: TypeOfValue,
isStateSource: boolean,
): void {
let newValue: DerivationMetadata = {
place: derivedVar,
sourcesIds: new Set(),
typeOfValue: typeOfValue ?? 'ignored',
};
if (sourcesIds !== undefined) {
for (const id of sourcesIds) {
const sourcePlace = this.cache.get(id)?.place;
if (sourcePlace === undefined) {
continue;
}
/*
* If the identifier of the source is a promoted identifier, then
* we should set the target as the source.
*/
let finalIsSource = isStateSource;
if (!finalIsSource) {
for (const sourceId of sourcesIds) {
const sourceMetadata = this.cache.get(sourceId);
if (
sourcePlace.identifier.name === null ||
sourcePlace.identifier.name?.kind === 'promoted'
sourceMetadata?.isStateSource &&
sourceMetadata.place.identifier.name?.kind !== 'named'
) {
newValue.sourcesIds.add(derivedVar.identifier.id);
} else {
newValue.sourcesIds.add(sourcePlace.identifier.id);
finalIsSource = true;
break;
}
}
}
if (newValue.sourcesIds.size === 0) {
newValue.sourcesIds.add(derivedVar.identifier.id);
}
const existingValue = this.cache.get(derivedVar.identifier.id);
if (
existingValue === undefined ||
!this.isDerivationEqual(existingValue, newValue)
) {
this.cache.set(derivedVar.identifier.id, newValue);
this.hasChanges = true;
}
this.cache.set(derivedVar.identifier.id, {
place: derivedVar,
sourcesIds: sourcesIds,
typeOfValue: typeOfValue ?? 'ignored',
isStateSource: finalIsSource,
});
}
private isDerivationEqual(
@@ -121,6 +140,14 @@ class DerivationCache {
}
}
function isNamedIdentifier(place: Place): place is Place & {
identifier: {name: NonNullable<Place['identifier']['name']>};
} {
return (
place.identifier.name !== null && place.identifier.name.kind === 'named'
);
}
/**
* Validates that useEffect is not used for derived computations which could/should
* be performed in render.
@@ -146,25 +173,22 @@ class DerivationCache {
*/
export function validateNoDerivedComputationsInEffects_exp(
fn: HIRFunction,
): void {
): Result<void, CompilerError> {
const functions: Map<IdentifierId, FunctionExpression> = new Map();
const derivationCache = new DerivationCache();
const errors = new CompilerError();
const effects: Set<HIRFunction> = new Set();
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,
errors,
derivationCache,
effects,
setStateCache,
effectSetStateCache,
setStateLoads,
setStateUsages,
};
if (fn.fnType === 'Hook') {
@@ -172,10 +196,10 @@ export function validateNoDerivedComputationsInEffects_exp(
if (param.kind === 'Identifier') {
context.derivationCache.cache.set(param.identifier.id, {
place: param,
sourcesIds: new Set([param.identifier.id]),
sourcesIds: new Set(),
typeOfValue: 'fromProps',
isStateSource: true,
});
context.derivationCache.hasChanges = true;
}
}
} else if (fn.fnType === 'Component') {
@@ -183,15 +207,17 @@ export function validateNoDerivedComputationsInEffects_exp(
if (props != null && props.kind === 'Identifier') {
context.derivationCache.cache.set(props.identifier.id, {
place: props,
sourcesIds: new Set([props.identifier.id]),
sourcesIds: new Set(),
typeOfValue: 'fromProps',
isStateSource: true,
});
context.derivationCache.hasChanges = true;
}
}
let isFirstPass = true;
do {
context.derivationCache.takeSnapshot();
for (const block of fn.body.blocks.values()) {
recordPhiDerivations(block, context);
for (const instr of block.instructions) {
@@ -199,6 +225,7 @@ export function validateNoDerivedComputationsInEffects_exp(
}
}
context.derivationCache.checkForChanges();
isFirstPass = false;
} while (context.derivationCache.snapshot());
@@ -206,9 +233,7 @@ export function validateNoDerivedComputationsInEffects_exp(
validateEffect(effect, context);
}
if (errors.hasAnyErrors()) {
throw errors;
}
return errors.asResult();
}
function recordPhiDerivations(
@@ -236,6 +261,7 @@ function recordPhiDerivations(
phi.place,
sourcesIds,
typeOfValue,
false,
);
}
}
@@ -251,17 +277,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);
}
@@ -280,24 +358,19 @@ function recordInstructionDerivations(
context.effects.add(effectFunction.loweredFunc.func);
}
} else if (isUseStateType(lvalue.identifier) && value.args.length > 0) {
const stateValueSource = value.args[0];
if (stateValueSource.kind === 'Identifier') {
sources.add(stateValueSource.identifier.id);
}
isSource = true;
typeOfValue = joinValue(typeOfValue, 'fromState');
}
}
for (const operand of eachInstructionOperand(instr)) {
if (
isSetStateType(operand.identifier) &&
operand.loc !== GeneratedSource &&
isFirstPass
) {
if (context.setStateCache.has(operand.loc.identifierName)) {
context.setStateCache.get(operand.loc.identifierName)!.push(operand);
} else {
context.setStateCache.set(operand.loc.identifierName, [operand]);
if (context.setStateLoads.has(operand.identifier.id)) {
const rootSetStateId = getRootSetState(
operand.identifier.id,
context.setStateLoads,
);
if (rootSetStateId !== null) {
context.setStateUsages.get(rootSetStateId)?.add(operand.loc);
}
}
@@ -310,9 +383,7 @@ function recordInstructionDerivations(
}
typeOfValue = joinValue(typeOfValue, operandMetadata.typeOfValue);
for (const id of operandMetadata.sourcesIds) {
sources.add(id);
}
sources.add(operand.identifier.id);
}
if (typeOfValue === 'ignored') {
@@ -320,7 +391,12 @@ function recordInstructionDerivations(
}
for (const lvalue of eachInstructionLValue(instr)) {
context.derivationCache.addDerivationEntry(lvalue, sources, typeOfValue);
context.derivationCache.addDerivationEntry(
lvalue,
sources,
typeOfValue,
isSource,
);
}
for (const operand of eachInstructionOperand(instr)) {
@@ -331,11 +407,25 @@ function recordInstructionDerivations(
case Effect.ConditionallyMutateIterator:
case Effect.Mutate: {
if (isMutable(instr, operand)) {
context.derivationCache.addDerivationEntry(
operand,
sources,
typeOfValue,
);
if (context.derivationCache.cache.has(operand.identifier.id)) {
const operandMetadata = context.derivationCache.cache.get(
operand.identifier.id,
);
if (operandMetadata !== undefined) {
operandMetadata.typeOfValue = joinValue(
typeOfValue,
operandMetadata.typeOfValue,
);
}
} else {
context.derivationCache.addDerivationEntry(
operand,
sources,
typeOfValue,
false,
);
}
}
break;
}
@@ -367,6 +457,137 @@ 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,
context: ValidationContext,
@@ -375,13 +596,33 @@ function validateEffect(
const effectDerivedSetStateCalls: Array<{
value: CallExpression;
loc: SourceLocation;
id: IdentifierId;
sourceIds: Set<IdentifierId>;
typeOfValue: TypeOfValue;
}> = [];
const effectSetStateUsages: Map<
IdentifierId,
Set<SourceLocation>
> = new Map();
let cleanUpFunctionDeps: Set<IdentifierId> | undefined;
const globals: Set<IdentifierId> = new Set();
for (const block of effectFunction.body.blocks.values()) {
/*
* if the block is in an effect and is of type return then its an effect's cleanup function
* if the cleanup function depends on a value from which effect-set state is derived then
* we can't validate
*/
if (
block.terminal.kind === 'return' &&
block.terminal.returnVariant === 'Explicit'
) {
cleanUpFunctionDeps = getFnLocalDeps(
context.functions.get(block.terminal.value.identifier.id),
);
}
for (const pred of block.preds) {
if (!seenBlocks.has(pred)) {
// skip if block has a back edge
@@ -395,19 +636,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);
}
}
}
@@ -425,7 +663,7 @@ function validateEffect(
if (argMetadata !== undefined) {
effectDerivedSetStateCalls.push({
value: instr.value,
loc: instr.value.callee.loc,
id: instr.value.callee.identifier.id,
sourceIds: argMetadata.sourcesIds,
typeOfValue: argMetadata.typeOfValue,
});
@@ -459,37 +697,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.',

View File

@@ -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>

View File

@@ -1,4 +1,4 @@
// @validateNoDerivedComputationsInEffects_exp
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import {useEffect, useState} from 'react';
function Component({value, enabled}) {

View File

@@ -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>

View File

@@ -1,4 +1,4 @@
// @validateNoDerivedComputationsInEffects_exp
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import {useEffect, useState} from 'react';
export default function Component({input = 'empty'}) {

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -1,4 +1,4 @@
// @validateNoDerivedComputationsInEffects_exp
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import {useEffect, useState} from 'react';
function Component({initialName}) {

View File

@@ -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

View File

@@ -0,0 +1,11 @@
// @validateNoDerivedComputationsInEffects_exp
function Component({value}) {
const [checked, setChecked] = useState('');
useEffect(() => {
setChecked(value === '' ? [] : value.split(','));
}, [value]);
return <div>{checked}</div>;
}

View File

@@ -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>

View File

@@ -1,4 +1,4 @@
// @validateNoDerivedComputationsInEffects_exp
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import {useEffect, useState} from 'react';
function MockComponent({onSet}) {

View File

@@ -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>

View File

@@ -1,4 +1,4 @@
// @validateNoDerivedComputationsInEffects_exp
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import {useEffect, useState} from 'react';
function Component({value}) {

View File

@@ -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

View File

@@ -1,4 +1,4 @@
// @validateNoDerivedComputationsInEffects_exp
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import {useEffect, useState, useRef} from 'react';
export default function Component({test}) {

View File

@@ -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']

View File

@@ -1,4 +1,4 @@
// @validateNoDerivedComputationsInEffects_exp
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import {useEffect, useState} from 'react';
function Component({propValue}) {

View File

@@ -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>

View File

@@ -1,4 +1,4 @@
// @validateNoDerivedComputationsInEffects_exp
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import {useEffect, useState} from 'react';
function Component({propValue, onChange}) {

View File

@@ -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

View File

@@ -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} />;
}

View File

@@ -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

View File

@@ -1,4 +1,4 @@
// @validateNoDerivedComputationsInEffects_exp
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import {useEffect, useState} from 'react';
function Component({propValue}) {

View File

@@ -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 | }
```

View File

@@ -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>;
```

View File

@@ -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 |
```

View File

@@ -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 (
```

View File

@@ -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 |
```

View File

@@ -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 |
```

View File

@@ -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>;
```

View File

@@ -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>;
```

View File

@@ -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>;
```

View File

@@ -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>

View File

@@ -1,4 +1,4 @@
// @validateNoDerivedComputationsInEffects_exp
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import {useEffect, useState} from 'react';
function Component() {

View File

@@ -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>

View File

@@ -1,4 +1,4 @@
// @validateNoDerivedComputationsInEffects_exp
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import {useEffect, useState} from 'react';
export default function Component(props) {

View File

@@ -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>

View File

@@ -1,4 +1,4 @@
// @validateNoDerivedComputationsInEffects_exp
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import {useEffect, useState} from 'react';
export default function Component({props}) {

View File

@@ -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

View File

@@ -1,4 +1,4 @@
// @validateNoDerivedComputationsInEffects_exp
// @validateNoDerivedComputationsInEffects_exp @loggerTestOnly
import {useEffect, useState, useRef} from 'react';
export default function Component({test}) {

View File

@@ -3298,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}]}>
`);
});
});

View File

@@ -2586,6 +2586,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);

View File

@@ -669,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) {

View File

@@ -12,6 +12,11 @@
background-color: color-mix(in srgb, var(--color-transition) 5%, transparent);
}
.SuspenseRectsRootOutline {
outline-width: 4px;
border-radius: 0.125rem;
}
.SuspenseRectsRoot[data-hovered='true'] {
background-color: color-mix(in srgb, var(--color-transition) 15%, transparent);
}
@@ -100,10 +105,6 @@
pointer-events: none;
}
.SuspenseRectOutlineRoot {
outline-color: var(--color-transition);
}
.SuspenseRectsBoundary[data-selected='true'] > .SuspenseRectsRect {
box-shadow: none;
}

View File

@@ -510,9 +510,12 @@ function SuspenseRectsContainer({
let selectedBoundingBox = null;
let selectedEnvironment = null;
if (isRootSelected) {
selectedBoundingBox = boundingBox;
selectedEnvironment = rootEnvironment;
} else if (inspectedElementID !== null) {
} else if (
inspectedElementID !== null &&
// TODO: Separate inspected element and inspected Suspense and use the inspected Suspense ID here.
store.containsSuspense(inspectedElementID)
) {
const selectedSuspenseNode = store.getSuspenseByID(inspectedElementID);
if (
selectedSuspenseNode !== null &&
@@ -534,6 +537,7 @@ function SuspenseRectsContainer({
className={
styles.SuspenseRectsContainer +
(hasRootSuspenders ? ' ' + styles.SuspenseRectsRoot : '') +
(isRootSelected ? ' ' + styles.SuspenseRectsRootOutline : '') +
' ' +
getClassNameForEnvironment(rootEnvironment)
}
@@ -551,7 +555,6 @@ function SuspenseRectsContainer({
<ScaledRect
className={
styles.SuspenseRectOutline +
(isRootSelected ? ' ' + styles.SuspenseRectOutlineRoot : '') +
' ' +
getClassNameForEnvironment(selectedEnvironment)
}

View File

@@ -0,0 +1,134 @@
/**
* 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 Activity;
let useState;
let ReactDOM;
let ReactDOMClient;
let act;
describe('ReactDOMActivity', () => {
let container;
beforeEach(() => {
jest.resetModules();
React = require('react');
Activity = React.Activity;
useState = React.useState;
ReactDOM = require('react-dom');
ReactDOMClient = require('react-dom/client');
act = require('internal-test-utils').act;
container = document.createElement('div');
document.body.appendChild(container);
});
afterEach(() => {
document.body.removeChild(container);
});
// @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({portalContents}) {
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({portalContents}) {
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>',
);
},
);
});

View File

@@ -117,6 +117,7 @@ import {
DidCapture,
AffectedParentLayout,
ViewTransitionNamedStatic,
PortalStatic,
} from './ReactFiberFlags';
import {
commitStartTime,
@@ -1182,66 +1183,104 @@ function commitTransitionProgress(offscreenFiber: Fiber) {
}
}
function hideOrUnhideAllChildren(finishedWork: Fiber, isHidden: boolean) {
// Only hide or unhide the top-most host nodes.
let hostSubtreeRoot = null;
function hideOrUnhideAllChildren(parentFiber: Fiber, isHidden: boolean) {
if (!supportsMutation) {
return;
}
// Finds the nearest host component children and updates their visibility
// to either hidden or visible.
let child = parentFiber.child;
while (child !== null) {
hideOrUnhideAllChildrenOnFiber(child, isHidden);
child = child.sibling;
}
}
if (supportsMutation) {
// We only have the top Fiber that was inserted but we need to recurse down its
// children to find all the terminal nodes.
let node: Fiber = finishedWork;
while (true) {
if (
node.tag === HostComponent ||
(supportsResources ? node.tag === HostHoistable : false)
) {
if (hostSubtreeRoot === null) {
hostSubtreeRoot = node;
commitShowHideHostInstance(node, isHidden);
}
} else if (node.tag === HostText) {
if (hostSubtreeRoot === null) {
commitShowHideHostTextInstance(node, isHidden);
}
} else if (node.tag === DehydratedFragment) {
if (hostSubtreeRoot === null) {
commitShowHideSuspenseBoundary(node, isHidden);
}
} else if (
(node.tag === OffscreenComponent ||
node.tag === LegacyHiddenComponent) &&
(node.memoizedState: OffscreenState) !== null &&
node !== finishedWork
) {
function hideOrUnhideAllChildrenOnFiber(fiber: Fiber, isHidden: boolean) {
if (!supportsMutation) {
return;
}
switch (fiber.tag) {
case HostComponent:
case HostHoistable: {
// Found the nearest host component. Hide it.
commitShowHideHostInstance(fiber, isHidden);
// Typically, only the nearest host nodes need to be hidden, since that
// has the effect of also hiding everything inside of them.
//
// However, there's a special case for portals, because portals do not
// exist in the regular host tree hierarchy; we can't assume that just
// because a portal's HostComponent parent in the React tree will also be
// a parent in the actual host tree.
//
// So, if any portals exist within the tree, regardless of how deeply
// nested they are, we need to repeat this algorithm for its children.
hideOrUnhideNearestPortals(fiber, isHidden);
return;
}
case HostText: {
commitShowHideHostTextInstance(fiber, isHidden);
return;
}
case DehydratedFragment: {
commitShowHideSuspenseBoundary(fiber, isHidden);
return;
}
case OffscreenComponent:
case LegacyHiddenComponent: {
const offscreenState: OffscreenState | null = fiber.memoizedState;
if (offscreenState !== null) {
// Found a nested Offscreen component that is hidden.
// Don't search any deeper. This tree should remain hidden.
} else if (node.child !== null) {
node.child.return = node;
node = node.child;
continue;
} else {
hideOrUnhideAllChildren(fiber, isHidden);
}
return;
}
default: {
hideOrUnhideAllChildren(fiber, isHidden);
return;
}
}
}
if (node === finishedWork) {
return;
function hideOrUnhideNearestPortals(parentFiber: Fiber, isHidden: boolean) {
if (!supportsMutation) {
return;
}
if (parentFiber.subtreeFlags & PortalStatic) {
let child = parentFiber.child;
while (child !== null) {
hideOrUnhideNearestPortalsOnFiber(child, isHidden);
child = child.sibling;
}
}
}
function hideOrUnhideNearestPortalsOnFiber(fiber: Fiber, isHidden: boolean) {
if (!supportsMutation) {
return;
}
switch (fiber.tag) {
case HostPortal: {
// Found a portal. Switch back to the normal hide/unhide algorithm to
// toggle the visibility of its children.
hideOrUnhideAllChildrenOnFiber(fiber, isHidden);
return;
}
case OffscreenComponent: {
const offscreenState: OffscreenState | null = fiber.memoizedState;
if (offscreenState !== null) {
// Found a nested Offscreen component that is hidden. Don't search any
// deeper. This tree should remain hidden.
} else {
hideOrUnhideNearestPortals(fiber, isHidden);
}
while (node.sibling === null) {
if (node.return === null || node.return === finishedWork) {
return;
}
if (hostSubtreeRoot === node) {
hostSubtreeRoot = null;
}
node = node.return;
}
if (hostSubtreeRoot === node) {
hostSubtreeRoot = null;
}
node.sibling.return = node.return;
node = node.sibling;
return;
}
default: {
hideOrUnhideNearestPortals(fiber, isHidden);
return;
}
}
}
@@ -2305,6 +2344,15 @@ function commitMutationEffectsOnFiber(
break;
}
case HostPortal: {
// For the purposes of visibility toggling, the direct children of a
// portal are considered "children" of the nearest hidden
// OffscreenComponent, regardless of whether there are any host components
// in between them. This is because portals are not part of the regular
// host tree hierarchy; we can't assume that just because a portal's
// HostComponent parent in the React tree will also be a parent in the
// actual host tree. So we must hide all of them.
const prevOffscreenDirectParentIsHidden = offscreenDirectParentIsHidden;
offscreenDirectParentIsHidden = offscreenSubtreeIsHidden;
const prevMutationContext = pushMutationContext();
if (supportsResources) {
const previousHoistableRoot = currentHoistableRoot;
@@ -2326,6 +2374,7 @@ function commitMutationEffectsOnFiber(
rootViewTransitionAffected = true;
}
popMutationContext(prevMutationContext);
offscreenDirectParentIsHidden = prevOffscreenDirectParentIsHidden;
if (flags & Update) {
if (supportsPersistence) {

View File

@@ -99,6 +99,7 @@ import {
Cloned,
ViewTransitionStatic,
Hydrate,
PortalStatic,
} from './ReactFiberFlags';
import {
@@ -1665,6 +1666,7 @@ function completeWork(
if (current === null) {
preparePortalMount(workInProgress.stateNode.containerInfo);
}
workInProgress.flags |= PortalStatic;
bubbleProperties(workInProgress);
return null;
case ContextProvider:

View File

@@ -83,11 +83,13 @@ export const ViewTransitionNamedStatic =
// ViewTransitionStatic tracks whether there are an ViewTransition components from
// the nearest HostComponent down. It resets at every HostComponent level.
export const ViewTransitionStatic = /* */ 0b0000010000000000000000000000000;
// Tracks whether a HostPortal is present in the tree.
export const PortalStatic = /* */ 0b0000100000000000000000000000000;
// Flag used to identify newly inserted fibers. It isn't reset after commit unlike `Placement`.
export const PlacementDEV = /* */ 0b0000100000000000000000000000000;
export const MountLayoutDev = /* */ 0b0001000000000000000000000000000;
export const MountPassiveDev = /* */ 0b0010000000000000000000000000000;
export const PlacementDEV = /* */ 0b0001000000000000000000000000000;
export const MountLayoutDev = /* */ 0b0010000000000000000000000000000;
export const MountPassiveDev = /* */ 0b0100000000000000000000000000000;
// Groups of flags that are used in the commit phase to skip over trees that
// don't contain effects, by checking subtreeFlags.
@@ -139,4 +141,5 @@ export const StaticMask =
RefStatic |
MaySuspendCommit |
ViewTransitionStatic |
ViewTransitionNamedStatic;
ViewTransitionNamedStatic |
PortalStatic;

View File

@@ -1291,24 +1291,13 @@ function renderSuspenseBoundary(
const defer: boolean = enableCPUSuspense && props.defer === true;
const fallbackAbortSet: Set<Task> = new Set();
let newBoundary: SuspenseBoundary;
if (canHavePreamble(task.formatContext)) {
newBoundary = createSuspenseBoundary(
request,
task.row,
fallbackAbortSet,
createPreamble(),
defer,
);
} else {
newBoundary = createSuspenseBoundary(
request,
task.row,
fallbackAbortSet,
null,
defer,
);
}
const newBoundary = createSuspenseBoundary(
request,
task.row,
fallbackAbortSet,
canHavePreamble(task.formatContext) ? createPreamble() : null,
defer,
);
const insertionIndex = parentSegment.chunks.length;
// The children of the boundary segment is actually the fallback.
@@ -1603,24 +1592,13 @@ function replaySuspenseBoundary(
const defer: boolean = enableCPUSuspense && props.defer === true;
const fallbackAbortSet: Set<Task> = new Set();
let resumedBoundary: SuspenseBoundary;
if (canHavePreamble(task.formatContext)) {
resumedBoundary = createSuspenseBoundary(
request,
task.row,
fallbackAbortSet,
createPreamble(),
defer,
);
} else {
resumedBoundary = createSuspenseBoundary(
request,
task.row,
fallbackAbortSet,
null,
defer,
);
}
const resumedBoundary = createSuspenseBoundary(
request,
task.row,
fallbackAbortSet,
canHavePreamble(task.formatContext) ? createPreamble() : null,
defer,
);
resumedBoundary.parentFlushed = true;
// We restore the same id of this boundary as was used during prerender.
resumedBoundary.rootSegmentID = id;