mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
refactor to use scope dependency
Summary: Test Plan: Reviewers: Subscribers: Tasks: Tags:
This commit is contained in:
@@ -21,6 +21,7 @@ import {
|
||||
lower,
|
||||
mergeConsecutiveBlocks,
|
||||
mergeOverlappingReactiveScopesHIR,
|
||||
printHIR,
|
||||
pruneUnusedLabelsHIR,
|
||||
} from '../HIR';
|
||||
import {
|
||||
|
||||
+176
-13
@@ -1,3 +1,4 @@
|
||||
import {CompilerError, SourceLocation} from '..';
|
||||
import {
|
||||
ArrayExpression,
|
||||
Effect,
|
||||
@@ -9,8 +10,19 @@ import {
|
||||
Instruction,
|
||||
isUseEffectHookType,
|
||||
makeInstructionId,
|
||||
TInstruction,
|
||||
InstructionId,
|
||||
ScopeId,
|
||||
ReactiveScopeDependency,
|
||||
Place,
|
||||
ReactiveScopeDependencies,
|
||||
} from '../HIR';
|
||||
import {createTemporaryPlace} from '../HIR/HIRBuilder';
|
||||
import {
|
||||
createTemporaryPlace,
|
||||
fixScopeAndIdentifierRanges,
|
||||
markInstructionIds,
|
||||
} from '../HIR/HIRBuilder';
|
||||
import {eachInstructionOperand, eachTerminalOperand} from '../HIR/visitors';
|
||||
|
||||
/**
|
||||
* Infers reactive dependencies captured by useEffect lambdas and adds them as
|
||||
@@ -20,14 +32,48 @@ export function inferEffectDependencies(
|
||||
env: Environment,
|
||||
fn: HIRFunction,
|
||||
): void {
|
||||
const fnExpressions = new Map<IdentifierId, FunctionExpression>();
|
||||
let hasRewrite = false;
|
||||
const fnExpressions = new Map<
|
||||
IdentifierId,
|
||||
TInstruction<FunctionExpression>
|
||||
>();
|
||||
const scopeInfos = new Map<
|
||||
ScopeId,
|
||||
{pruned: boolean; deps: ReactiveScopeDependencies; hasSingleInstr: boolean}
|
||||
>();
|
||||
|
||||
/**
|
||||
* When inserting LoadLocals, we need to retain the reactivity of the base
|
||||
* identifier, as later passes e.g. PruneNonReactiveDeps take the reactivity of
|
||||
* a base identifier as the "maximal" reactivity of all its references.
|
||||
* Concretely,
|
||||
* reactive(Identifier i) = Union_{reference of i}(reactive(reference))
|
||||
*/
|
||||
const reactiveIds = inferReactiveIdentifiers(fn);
|
||||
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
let rewriteInstrs = new Map();
|
||||
if (
|
||||
block.terminal.kind === 'scope' ||
|
||||
block.terminal.kind === 'pruned-scope'
|
||||
) {
|
||||
const scopeBlock = fn.body.blocks.get(block.terminal.block)!;
|
||||
scopeInfos.set(block.terminal.scope.id, {
|
||||
pruned: block.terminal.kind === 'pruned-scope',
|
||||
deps: block.terminal.scope.dependencies,
|
||||
hasSingleInstr:
|
||||
scopeBlock.instructions.length === 1 &&
|
||||
scopeBlock.terminal.kind === 'goto' &&
|
||||
scopeBlock.terminal.block === block.terminal.fallthrough,
|
||||
});
|
||||
}
|
||||
const rewriteInstrs = new Map<InstructionId, Array<Instruction>>();
|
||||
for (const instr of block.instructions) {
|
||||
const {value, lvalue} = instr;
|
||||
if (value.kind === 'FunctionExpression') {
|
||||
fnExpressions.set(lvalue.identifier.id, value);
|
||||
fnExpressions.set(
|
||||
lvalue.identifier.id,
|
||||
instr as TInstruction<FunctionExpression>,
|
||||
);
|
||||
} else if (
|
||||
/*
|
||||
* This check is not final. Right now we only look for useEffects without a dependency array.
|
||||
@@ -41,34 +87,73 @@ export function inferEffectDependencies(
|
||||
) {
|
||||
const fnExpr = fnExpressions.get(value.args[0].identifier.id);
|
||||
if (fnExpr != null) {
|
||||
const scopeInfo =
|
||||
fnExpr.lvalue.identifier.scope != null
|
||||
? scopeInfos.get(fnExpr.lvalue.identifier.scope.id)
|
||||
: null;
|
||||
CompilerError.invariant(scopeInfo != null, {
|
||||
reason: 'Expected function expression scope to exist',
|
||||
loc: value.loc,
|
||||
});
|
||||
if (scopeInfo.pruned || !scopeInfo.hasSingleInstr) {
|
||||
// TODO: retry pipeline that ensures effect function expressions
|
||||
// are placed into their own scope
|
||||
CompilerError.throwTodo({
|
||||
reason:
|
||||
'[InferEffectDependencies] Expected effect function to have non-pruned scope and its scope to have exactly one instruction',
|
||||
loc: fnExpr.loc,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Step 1: write new instructions to insert a dependency array
|
||||
*/
|
||||
const effectDeps: Array<Place> = [];
|
||||
const newInstructions: Array<Instruction> = [];
|
||||
for (const dep of scopeInfo.deps) {
|
||||
/**
|
||||
* Invalid to prune non-reactive values in this pass, see the
|
||||
* `infer-effect-deps/pruned-nonreactive-obj` fixture for an
|
||||
* explanation
|
||||
*/
|
||||
const {place, instructions} = writeDependencyToInstructions(
|
||||
dep,
|
||||
reactiveIds.has(dep.identifier.id),
|
||||
fn.env,
|
||||
fnExpr.loc,
|
||||
);
|
||||
newInstructions.push(...instructions);
|
||||
effectDeps.push(place);
|
||||
}
|
||||
const deps: ArrayExpression = {
|
||||
kind: 'ArrayExpression',
|
||||
elements: fnExpr.loweredFunc.dependencies.filter(
|
||||
place => place.reactive,
|
||||
),
|
||||
elements: effectDeps,
|
||||
loc: GeneratedSource,
|
||||
};
|
||||
|
||||
const depsPlace = createTemporaryPlace(env, GeneratedSource);
|
||||
depsPlace.effect = Effect.Read;
|
||||
value.args[1] = depsPlace;
|
||||
|
||||
const newInstruction: Instruction = {
|
||||
newInstructions.push({
|
||||
id: makeInstructionId(0),
|
||||
loc: GeneratedSource,
|
||||
lvalue: depsPlace,
|
||||
lvalue: {...depsPlace, effect: Effect.Mutate},
|
||||
value: deps,
|
||||
};
|
||||
rewriteInstrs.set(instr.id, newInstruction);
|
||||
});
|
||||
|
||||
// Step 2: insert the deps array as an argument of the useEffect
|
||||
value.args[1] = {...depsPlace, effect: Effect.Freeze};
|
||||
rewriteInstrs.set(instr.id, newInstructions);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (rewriteInstrs.size > 0) {
|
||||
hasRewrite = true;
|
||||
const newInstrs = [];
|
||||
for (const instr of block.instructions) {
|
||||
const newInstr = rewriteInstrs.get(instr.id);
|
||||
if (newInstr != null) {
|
||||
newInstrs.push(newInstr, instr);
|
||||
newInstrs.push(...newInstr, instr);
|
||||
} else {
|
||||
newInstrs.push(instr);
|
||||
}
|
||||
@@ -76,4 +161,82 @@ export function inferEffectDependencies(
|
||||
block.instructions = newInstrs;
|
||||
}
|
||||
}
|
||||
if (hasRewrite) {
|
||||
// Renumber instructions and fix scope ranges
|
||||
markInstructionIds(fn.body);
|
||||
fixScopeAndIdentifierRanges(fn.body);
|
||||
}
|
||||
}
|
||||
|
||||
function writeDependencyToInstructions(
|
||||
dep: ReactiveScopeDependency,
|
||||
reactive: boolean,
|
||||
env: Environment,
|
||||
loc: SourceLocation,
|
||||
): {place: Place; instructions: Array<Instruction>} {
|
||||
const instructions: Array<Instruction> = [];
|
||||
let currValue = createTemporaryPlace(env, GeneratedSource);
|
||||
currValue.reactive = reactive;
|
||||
instructions.push({
|
||||
id: makeInstructionId(0),
|
||||
loc: GeneratedSource,
|
||||
lvalue: {...currValue, effect: Effect.Mutate},
|
||||
value: {
|
||||
kind: 'LoadLocal',
|
||||
place: {
|
||||
kind: 'Identifier',
|
||||
identifier: dep.identifier,
|
||||
effect: Effect.Capture,
|
||||
reactive,
|
||||
loc: loc,
|
||||
},
|
||||
loc: loc,
|
||||
},
|
||||
});
|
||||
for (const path of dep.path) {
|
||||
if (path.optional) {
|
||||
// TODO: instead of truncating optional paths, reuse
|
||||
// instructions from hoisted dependencies block
|
||||
break;
|
||||
}
|
||||
const nextValue = createTemporaryPlace(env, GeneratedSource);
|
||||
nextValue.reactive = reactive;
|
||||
instructions.push({
|
||||
id: makeInstructionId(0),
|
||||
loc: GeneratedSource,
|
||||
lvalue: {...nextValue, effect: Effect.Mutate},
|
||||
value: {
|
||||
kind: 'PropertyLoad',
|
||||
object: {...currValue, effect: Effect.Capture},
|
||||
property: path.property,
|
||||
loc: loc,
|
||||
},
|
||||
});
|
||||
currValue = nextValue;
|
||||
}
|
||||
currValue.effect = Effect.Freeze;
|
||||
return {place: currValue, instructions};
|
||||
}
|
||||
|
||||
function inferReactiveIdentifiers(fn: HIRFunction): Set<IdentifierId> {
|
||||
const reactiveIds: Set<IdentifierId> = new Set();
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
for (const instr of block.instructions) {
|
||||
// no need to traverse into nested functions as
|
||||
// 1. Their effects are recorded in `LoweredFunction.dependencies`
|
||||
// 2. we don't mark `reactive` in these anyways
|
||||
for (const place of eachInstructionOperand(instr)) {
|
||||
if (place.reactive) {
|
||||
reactiveIds.add(place.identifier.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const place of eachTerminalOperand(block.terminal)) {
|
||||
if (place.reactive) {
|
||||
reactiveIds.add(place.identifier.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
return reactiveIds;
|
||||
}
|
||||
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @inferEffectDependencies
|
||||
import {useEffect} from 'react';
|
||||
import {makeObject_Primitives, print} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* Note that `obj` is currently added to the effect dependency array, even
|
||||
* though it's non-reactive due to memoization.
|
||||
*
|
||||
* This is a TODO in effect dependency inference. Note that we cannot simply
|
||||
* filter out non-reactive effect dependencies, as some non-reactive (by data
|
||||
* flow) values become reactive due to scope pruning. See the
|
||||
* `infer-effect-deps/pruned-nonreactive-obj` fixture for why this matters.
|
||||
*
|
||||
* Realizing that this `useEffect` should have an empty dependency array
|
||||
* requires effect dependency inference to be structured similarly to memo
|
||||
* dependency inference.
|
||||
* Pass 1: add all potential dependencies regardless of dataflow reactivity
|
||||
* Pass 2: (todo) prune non-reactive dependencies
|
||||
*
|
||||
* Note that instruction reordering should significantly reduce scope pruning
|
||||
*/
|
||||
function NonReactiveDepInEffect() {
|
||||
const obj = makeObject_Primitives();
|
||||
useEffect(() => print(obj));
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @inferEffectDependencies
|
||||
import { useEffect } from "react";
|
||||
import { makeObject_Primitives, print } from "shared-runtime";
|
||||
|
||||
/**
|
||||
* Note that `obj` is currently added to the effect dependency array, even
|
||||
* though it's non-reactive due to memoization.
|
||||
*
|
||||
* This is a TODO in effect dependency inference. Note that we cannot simply
|
||||
* filter out non-reactive effect dependencies, as some non-reactive (by data
|
||||
* flow) values become reactive due to scope pruning. See the
|
||||
* `infer-effect-deps/pruned-nonreactive-obj` fixture for why this matters.
|
||||
*
|
||||
* Realizing that this `useEffect` should have an empty dependency array
|
||||
* requires effect dependency inference to be structured similarly to memo
|
||||
* dependency inference.
|
||||
* Pass 1: add all potential dependencies regardless of dataflow reactivity
|
||||
* Pass 2: (todo) prune non-reactive dependencies
|
||||
*
|
||||
* Note that instruction reordering should significantly reduce scope pruning
|
||||
*/
|
||||
function NonReactiveDepInEffect() {
|
||||
const $ = _c(2);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = makeObject_Primitives();
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
const obj = t0;
|
||||
let t1;
|
||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = () => print(obj);
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
useEffect(t1, [obj]);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
// @inferEffectDependencies
|
||||
import {useEffect} from 'react';
|
||||
import {makeObject_Primitives, print} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* Note that `obj` is currently added to the effect dependency array, even
|
||||
* though it's non-reactive due to memoization.
|
||||
*
|
||||
* This is a TODO in effect dependency inference. Note that we cannot simply
|
||||
* filter out non-reactive effect dependencies, as some non-reactive (by data
|
||||
* flow) values become reactive due to scope pruning. See the
|
||||
* `infer-effect-deps/pruned-nonreactive-obj` fixture for why this matters.
|
||||
*
|
||||
* Realizing that this `useEffect` should have an empty dependency array
|
||||
* requires effect dependency inference to be structured similarly to memo
|
||||
* dependency inference.
|
||||
* Pass 1: add all potential dependencies regardless of dataflow reactivity
|
||||
* Pass 2: (todo) prune non-reactive dependencies
|
||||
*
|
||||
* Note that instruction reordering should significantly reduce scope pruning
|
||||
*/
|
||||
function NonReactiveDepInEffect() {
|
||||
const obj = makeObject_Primitives();
|
||||
useEffect(() => print(obj));
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @inferEffectDependencies
|
||||
import {useEffect, useRef} from 'react';
|
||||
import {print} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* Special case of `infer-effect-deps/nonreactive-dep`.
|
||||
*
|
||||
* We know that local `useRef` return values are stable, regardless of
|
||||
* inferred memoization.
|
||||
*/
|
||||
function NonReactiveRefInEffect() {
|
||||
const ref = useRef('initial value');
|
||||
useEffect(() => print(ref.current));
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @inferEffectDependencies
|
||||
import { useEffect, useRef } from "react";
|
||||
import { print } from "shared-runtime";
|
||||
|
||||
/**
|
||||
* Special case of `infer-effect-deps/nonreactive-dep`.
|
||||
*
|
||||
* We know that local `useRef` return values are stable, regardless of
|
||||
* inferred memoization.
|
||||
*/
|
||||
function NonReactiveRefInEffect() {
|
||||
const $ = _c(1);
|
||||
const ref = useRef("initial value");
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = () => print(ref.current);
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
useEffect(t0, [ref]);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
// @inferEffectDependencies
|
||||
import {useEffect, useRef} from 'react';
|
||||
import {print} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* Special case of `infer-effect-deps/nonreactive-dep`.
|
||||
*
|
||||
* We know that local `useRef` return values are stable, regardless of
|
||||
* inferred memoization.
|
||||
*/
|
||||
function NonReactiveRefInEffect() {
|
||||
const ref = useRef('initial value');
|
||||
useEffect(() => print(ref.current));
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @inferEffectDependencies
|
||||
import {useEffect} from 'react';
|
||||
import {print} from 'shared-runtime';
|
||||
/**
|
||||
* This compiled output is technically incorrect but this is currently the same
|
||||
* case as a bailout (an effect that overfires).
|
||||
*
|
||||
* To ensure an empty deps array is passed, we need special case
|
||||
* `InferEffectDependencies` for outlined functions (likely easier) or run it
|
||||
* before OutlineFunctions
|
||||
*/
|
||||
function OutlinedFunctionInEffect() {
|
||||
useEffect(() => print('hello world!'));
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
// @inferEffectDependencies
|
||||
import { useEffect } from "react";
|
||||
import { print } from "shared-runtime";
|
||||
/**
|
||||
* This compiled output is technically incorrect but this is currently the same
|
||||
* case as a bailout (an effect that overfires).
|
||||
*
|
||||
* To ensure an empty deps array is passed, we need special case
|
||||
* `InferEffectDependencies` for outlined functions (likely easier) or run it
|
||||
* before OutlineFunctions
|
||||
*/
|
||||
function OutlinedFunctionInEffect() {
|
||||
useEffect(_temp);
|
||||
}
|
||||
function _temp() {
|
||||
return print("hello world!");
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
// @inferEffectDependencies
|
||||
import {useEffect} from 'react';
|
||||
import {print} from 'shared-runtime';
|
||||
/**
|
||||
* This compiled output is technically incorrect but this is currently the same
|
||||
* case as a bailout (an effect that overfires).
|
||||
*
|
||||
* To ensure an empty deps array is passed, we need special case
|
||||
* `InferEffectDependencies` for outlined functions (likely easier) or run it
|
||||
* before OutlineFunctions
|
||||
*/
|
||||
function OutlinedFunctionInEffect() {
|
||||
useEffect(() => print('hello world!'));
|
||||
}
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @inferEffectDependencies
|
||||
import {useIdentity, mutate, makeObject} from 'shared-runtime';
|
||||
import {useEffect} from 'react';
|
||||
|
||||
/**
|
||||
* When a semantically non-reactive value has a pruned scope (i.e. the object
|
||||
* identity becomes reactive, but the underlying value it represents should be
|
||||
* constant), the compiler can choose to either
|
||||
* - add it as a dependency (and rerun the effect)
|
||||
* - not add it as a dependency
|
||||
*
|
||||
* We keep semantically non-reactive values in both memo block and effect
|
||||
* dependency arrays to avoid versioning invariants e.g. `x !== y.aliasedX`.
|
||||
* ```js
|
||||
* function Component() {
|
||||
* // obj is semantically non-reactive, but its memo scope is pruned due to
|
||||
* // the interleaving hook call
|
||||
* const obj = {};
|
||||
* useHook();
|
||||
* write(obj);
|
||||
*
|
||||
* const ref = useRef();
|
||||
*
|
||||
* // this effect needs to be rerun when obj's referential identity changes,
|
||||
* // because it might alias obj to a useRef / mutable store.
|
||||
* useEffect(() => ref.current = obj, ???);
|
||||
*
|
||||
* // in a custom hook (or child component), the user might expect versioning
|
||||
* // invariants to hold
|
||||
* useHook(ref, obj);
|
||||
* }
|
||||
*
|
||||
* // defined elsewhere
|
||||
* function useHook(someRef, obj) {
|
||||
* useEffect(
|
||||
* () => assert(someRef.current === obj),
|
||||
* [someRef, obj]
|
||||
* );
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
function PrunedNonReactive() {
|
||||
const obj = makeObject();
|
||||
useIdentity(null);
|
||||
mutate(obj);
|
||||
|
||||
useEffect(() => print(obj.value));
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @inferEffectDependencies
|
||||
import { useIdentity, mutate, makeObject } from "shared-runtime";
|
||||
import { useEffect } from "react";
|
||||
|
||||
/**
|
||||
* When a semantically non-reactive value has a pruned scope (i.e. the object
|
||||
* identity becomes reactive, but the underlying value it represents should be
|
||||
* constant), the compiler can choose to either
|
||||
* - add it as a dependency (and rerun the effect)
|
||||
* - not add it as a dependency
|
||||
*
|
||||
* We keep semantically non-reactive values in both memo block and effect
|
||||
* dependency arrays to avoid versioning invariants e.g. `x !== y.aliasedX`.
|
||||
* ```js
|
||||
* function Component() {
|
||||
* // obj is semantically non-reactive, but its memo scope is pruned due to
|
||||
* // the interleaving hook call
|
||||
* const obj = {};
|
||||
* useHook();
|
||||
* write(obj);
|
||||
*
|
||||
* const ref = useRef();
|
||||
*
|
||||
* // this effect needs to be rerun when obj's referential identity changes,
|
||||
* // because it might alias obj to a useRef / mutable store.
|
||||
* useEffect(() => ref.current = obj, ???);
|
||||
*
|
||||
* // in a custom hook (or child component), the user might expect versioning
|
||||
* // invariants to hold
|
||||
* useHook(ref, obj);
|
||||
* }
|
||||
*
|
||||
* // defined elsewhere
|
||||
* function useHook(someRef, obj) {
|
||||
* useEffect(
|
||||
* () => assert(someRef.current === obj),
|
||||
* [someRef, obj]
|
||||
* );
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
function PrunedNonReactive() {
|
||||
const $ = _c(2);
|
||||
const obj = makeObject();
|
||||
useIdentity(null);
|
||||
mutate(obj);
|
||||
let t0;
|
||||
if ($[0] !== obj.value) {
|
||||
t0 = () => print(obj.value);
|
||||
$[0] = obj.value;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
useEffect(t0, [obj.value]);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
// @inferEffectDependencies
|
||||
import {useIdentity, mutate, makeObject} from 'shared-runtime';
|
||||
import {useEffect} from 'react';
|
||||
|
||||
/**
|
||||
* When a semantically non-reactive value has a pruned scope (i.e. the object
|
||||
* identity becomes reactive, but the underlying value it represents should be
|
||||
* constant), the compiler can choose to either
|
||||
* - add it as a dependency (and rerun the effect)
|
||||
* - not add it as a dependency
|
||||
*
|
||||
* We keep semantically non-reactive values in both memo block and effect
|
||||
* dependency arrays to avoid versioning invariants e.g. `x !== y.aliasedX`.
|
||||
* ```js
|
||||
* function Component() {
|
||||
* // obj is semantically non-reactive, but its memo scope is pruned due to
|
||||
* // the interleaving hook call
|
||||
* const obj = {};
|
||||
* useHook();
|
||||
* write(obj);
|
||||
*
|
||||
* const ref = useRef();
|
||||
*
|
||||
* // this effect needs to be rerun when obj's referential identity changes,
|
||||
* // because it might alias obj to a useRef / mutable store.
|
||||
* useEffect(() => ref.current = obj, ???);
|
||||
*
|
||||
* // in a custom hook (or child component), the user might expect versioning
|
||||
* // invariants to hold
|
||||
* useHook(ref, obj);
|
||||
* }
|
||||
*
|
||||
* // defined elsewhere
|
||||
* function useHook(someRef, obj) {
|
||||
* useEffect(
|
||||
* () => assert(someRef.current === obj),
|
||||
* [someRef, obj]
|
||||
* );
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
function PrunedNonReactive() {
|
||||
const obj = makeObject();
|
||||
useIdentity(null);
|
||||
mutate(obj);
|
||||
|
||||
useEffect(() => print(obj.value));
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @inferEffectDependencies
|
||||
import {useEffect} from 'react';
|
||||
import {print} from 'shared-runtime';
|
||||
|
||||
function ReactiveMemberExprMerge({propVal}) {
|
||||
const obj = {a: {b: propVal}};
|
||||
useEffect(() => print(obj.a, obj.a.b));
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @inferEffectDependencies
|
||||
import { useEffect } from "react";
|
||||
import { print } from "shared-runtime";
|
||||
|
||||
function ReactiveMemberExprMerge(t0) {
|
||||
const $ = _c(4);
|
||||
const { propVal } = t0;
|
||||
let t1;
|
||||
if ($[0] !== propVal) {
|
||||
t1 = { a: { b: propVal } };
|
||||
$[0] = propVal;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
const obj = t1;
|
||||
let t2;
|
||||
if ($[2] !== obj.a) {
|
||||
t2 = () => print(obj.a, obj.a.b);
|
||||
$[2] = obj.a;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
useEffect(t2, [obj.a]);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
// @inferEffectDependencies
|
||||
import {useEffect} from 'react';
|
||||
import {print} from 'shared-runtime';
|
||||
|
||||
function ReactiveMemberExprMerge({propVal}) {
|
||||
const obj = {a: {b: propVal}};
|
||||
useEffect(() => print(obj.a, obj.a.b));
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @inferEffectDependencies
|
||||
import {useEffect} from 'react';
|
||||
import {print} from 'shared-runtime';
|
||||
|
||||
function ReactiveMemberExpr({propVal}) {
|
||||
const obj = {a: {b: propVal}};
|
||||
useEffect(() => print(obj.a.b));
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @inferEffectDependencies
|
||||
import { useEffect } from "react";
|
||||
import { print } from "shared-runtime";
|
||||
|
||||
function ReactiveMemberExpr(t0) {
|
||||
const $ = _c(4);
|
||||
const { propVal } = t0;
|
||||
let t1;
|
||||
if ($[0] !== propVal) {
|
||||
t1 = { a: { b: propVal } };
|
||||
$[0] = propVal;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
const obj = t1;
|
||||
let t2;
|
||||
if ($[2] !== obj.a.b) {
|
||||
t2 = () => print(obj.a.b);
|
||||
$[2] = obj.a.b;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
useEffect(t2, [obj.a.b]);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
// @inferEffectDependencies
|
||||
import {useEffect} from 'react';
|
||||
import {print} from 'shared-runtime';
|
||||
|
||||
function ReactiveMemberExpr({propVal}) {
|
||||
const obj = {a: {b: propVal}};
|
||||
useEffect(() => print(obj.a.b));
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @inferEffectDependencies
|
||||
import {useEffect} from 'react';
|
||||
import {print} from 'shared-runtime';
|
||||
|
||||
// TODO: take optional chains as dependencies
|
||||
function ReactiveMemberExpr({cond, propVal}) {
|
||||
const obj = {a: cond ? {b: propVal} : null};
|
||||
useEffect(() => print(obj.a?.b));
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @inferEffectDependencies
|
||||
import { useEffect } from "react";
|
||||
import { print } from "shared-runtime";
|
||||
|
||||
// TODO: take optional chains as dependencies
|
||||
function ReactiveMemberExpr(t0) {
|
||||
const $ = _c(7);
|
||||
const { cond, propVal } = t0;
|
||||
let t1;
|
||||
if ($[0] !== cond || $[1] !== propVal) {
|
||||
t1 = cond ? { b: propVal } : null;
|
||||
$[0] = cond;
|
||||
$[1] = propVal;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
let t2;
|
||||
if ($[3] !== t1) {
|
||||
t2 = { a: t1 };
|
||||
$[3] = t1;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
}
|
||||
const obj = t2;
|
||||
let t3;
|
||||
if ($[5] !== obj.a?.b) {
|
||||
t3 = () => print(obj.a?.b);
|
||||
$[5] = obj.a?.b;
|
||||
$[6] = t3;
|
||||
} else {
|
||||
t3 = $[6];
|
||||
}
|
||||
useEffect(t3, [obj.a]);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
// @inferEffectDependencies
|
||||
import {useEffect} from 'react';
|
||||
import {print} from 'shared-runtime';
|
||||
|
||||
// TODO: take optional chains as dependencies
|
||||
function ReactiveMemberExpr({cond, propVal}) {
|
||||
const obj = {a: cond ? {b: propVal} : null};
|
||||
useEffect(() => print(obj.a?.b));
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @inferEffectDependencies
|
||||
import {useEffect} from 'react';
|
||||
import {print} from 'shared-runtime';
|
||||
|
||||
function ReactiveVariable({propVal}) {
|
||||
const arr = [propVal];
|
||||
useEffect(() => print(arr));
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @inferEffectDependencies
|
||||
import { useEffect } from "react";
|
||||
import { print } from "shared-runtime";
|
||||
|
||||
function ReactiveVariable(t0) {
|
||||
const $ = _c(4);
|
||||
const { propVal } = t0;
|
||||
let t1;
|
||||
if ($[0] !== propVal) {
|
||||
t1 = [propVal];
|
||||
$[0] = propVal;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
const arr = t1;
|
||||
let t2;
|
||||
if ($[2] !== arr) {
|
||||
t2 = () => print(arr);
|
||||
$[2] = arr;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
useEffect(t2, [arr]);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
// @inferEffectDependencies
|
||||
import {useEffect} from 'react';
|
||||
import {print} from 'shared-runtime';
|
||||
|
||||
function ReactiveVariable({propVal}) {
|
||||
const arr = [propVal];
|
||||
useEffect(() => print(arr));
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @inferEffectDependencies
|
||||
import * as React from 'react';
|
||||
|
||||
/**
|
||||
* TODO: recognize import namespace
|
||||
*/
|
||||
function NonReactiveDepInEffect() {
|
||||
const obj = makeObject_Primitives();
|
||||
React.useEffect(() => print(obj));
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @inferEffectDependencies
|
||||
import * as React from "react";
|
||||
|
||||
/**
|
||||
* TODO: recognize import namespace
|
||||
*/
|
||||
function NonReactiveDepInEffect() {
|
||||
const $ = _c(2);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = makeObject_Primitives();
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
const obj = t0;
|
||||
let t1;
|
||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = () => print(obj);
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
React.useEffect(t1);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
// @inferEffectDependencies
|
||||
import * as React from 'react';
|
||||
|
||||
/**
|
||||
* TODO: recognize import namespace
|
||||
*/
|
||||
function NonReactiveDepInEffect() {
|
||||
const obj = makeObject_Primitives();
|
||||
React.useEffect(() => print(obj));
|
||||
}
|
||||
Reference in New Issue
Block a user