mirror of
https://github.com/facebook/react.git
synced 2026-02-22 03:42:05 +00:00
[compiler] Track locations for dependencies (#35794)
Tracks locations for reactive scope dependencies, both on the deps and portions of the path. The immediate need for this is a non-public experiment where we're exploring type-directed compilation, and sometimes look up the types of expressions by location. We need to preserve locations accurately for that to work, including the locations of the deps. ## Test Plan Locations for dependencies are not easy to test: i manually spot-checked the new fixture to ensure that the deps look right. This is fine as best-effort since it doesn't impact any of our core compilation logic, i may fix forward if there are issues and will think about how to test.
This commit is contained in:
@@ -31,6 +31,7 @@ import {
|
||||
PropertyLiteral,
|
||||
ReactiveScopeDependency,
|
||||
ScopeId,
|
||||
SourceLocation,
|
||||
TInstruction,
|
||||
} from './HIR';
|
||||
|
||||
@@ -244,6 +245,7 @@ class PropertyPathRegistry {
|
||||
getOrCreateIdentifier(
|
||||
identifier: Identifier,
|
||||
reactive: boolean,
|
||||
loc: SourceLocation,
|
||||
): PropertyPathNode {
|
||||
/**
|
||||
* Reads from a statically scoped variable are always safe in JS,
|
||||
@@ -260,6 +262,7 @@ class PropertyPathRegistry {
|
||||
identifier,
|
||||
reactive,
|
||||
path: [],
|
||||
loc,
|
||||
},
|
||||
hasOptional: false,
|
||||
parent: null,
|
||||
@@ -290,6 +293,7 @@ class PropertyPathRegistry {
|
||||
identifier: parent.fullPath.identifier,
|
||||
reactive: parent.fullPath.reactive,
|
||||
path: parent.fullPath.path.concat(entry),
|
||||
loc: entry.loc,
|
||||
},
|
||||
hasOptional: parent.hasOptional || entry.optional,
|
||||
};
|
||||
@@ -304,7 +308,7 @@ class PropertyPathRegistry {
|
||||
* so all subpaths of a PropertyLoad should already exist
|
||||
* (e.g. a.b is added before a.b.c),
|
||||
*/
|
||||
let currNode = this.getOrCreateIdentifier(n.identifier, n.reactive);
|
||||
let currNode = this.getOrCreateIdentifier(n.identifier, n.reactive, n.loc);
|
||||
if (n.path.length === 0) {
|
||||
return currNode;
|
||||
}
|
||||
@@ -323,20 +327,21 @@ class PropertyPathRegistry {
|
||||
}
|
||||
|
||||
function getMaybeNonNullInInstruction(
|
||||
instr: InstructionValue,
|
||||
value: InstructionValue,
|
||||
context: CollectHoistablePropertyLoadsContext,
|
||||
): PropertyPathNode | null {
|
||||
let path: ReactiveScopeDependency | null = null;
|
||||
if (instr.kind === 'PropertyLoad') {
|
||||
path = context.temporaries.get(instr.object.identifier.id) ?? {
|
||||
identifier: instr.object.identifier,
|
||||
reactive: instr.object.reactive,
|
||||
if (value.kind === 'PropertyLoad') {
|
||||
path = context.temporaries.get(value.object.identifier.id) ?? {
|
||||
identifier: value.object.identifier,
|
||||
reactive: value.object.reactive,
|
||||
path: [],
|
||||
loc: value.loc,
|
||||
};
|
||||
} else if (instr.kind === 'Destructure') {
|
||||
path = context.temporaries.get(instr.value.identifier.id) ?? null;
|
||||
} else if (instr.kind === 'ComputedLoad') {
|
||||
path = context.temporaries.get(instr.object.identifier.id) ?? null;
|
||||
} else if (value.kind === 'Destructure') {
|
||||
path = context.temporaries.get(value.value.identifier.id) ?? null;
|
||||
} else if (value.kind === 'ComputedLoad') {
|
||||
path = context.temporaries.get(value.object.identifier.id) ?? null;
|
||||
}
|
||||
return path != null ? context.registry.getOrCreateProperty(path) : null;
|
||||
}
|
||||
@@ -393,7 +398,11 @@ function collectNonNullsInBlocks(
|
||||
) {
|
||||
const identifier = fn.params[0].identifier;
|
||||
knownNonNullIdentifiers.add(
|
||||
context.registry.getOrCreateIdentifier(identifier, true),
|
||||
context.registry.getOrCreateIdentifier(
|
||||
identifier,
|
||||
true,
|
||||
fn.params[0].loc,
|
||||
),
|
||||
);
|
||||
}
|
||||
const nodes = new Map<
|
||||
@@ -468,6 +477,7 @@ function collectNonNullsInBlocks(
|
||||
identifier: dep.root.value.identifier,
|
||||
path: dep.path.slice(0, i),
|
||||
reactive: dep.root.value.reactive,
|
||||
loc: dep.loc,
|
||||
});
|
||||
assumedNonNullObjects.add(depNode);
|
||||
}
|
||||
@@ -654,17 +664,23 @@ function reduceMaybeOptionalChains(
|
||||
changed = false;
|
||||
|
||||
for (const original of optionalChainNodes) {
|
||||
let {identifier, path: origPath, reactive} = original.fullPath;
|
||||
let {
|
||||
identifier,
|
||||
path: origPath,
|
||||
reactive,
|
||||
loc: origLoc,
|
||||
} = original.fullPath;
|
||||
let currNode: PropertyPathNode = registry.getOrCreateIdentifier(
|
||||
identifier,
|
||||
reactive,
|
||||
origLoc,
|
||||
);
|
||||
for (let i = 0; i < origPath.length; i++) {
|
||||
const entry = origPath[i];
|
||||
// If the base is known to be non-null, replace with a non-optional load
|
||||
const nextEntry: DependencyPathEntry =
|
||||
entry.optional && nodes.has(currNode)
|
||||
? {property: entry.property, optional: false}
|
||||
? {property: entry.property, optional: false, loc: entry.loc}
|
||||
: entry;
|
||||
currNode = PropertyPathRegistry.getOrCreatePropertyEntry(
|
||||
currNode,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {CompilerError} from '..';
|
||||
import {CompilerError, SourceLocation} from '..';
|
||||
import {assertNonNull} from './CollectHoistablePropertyLoads';
|
||||
import {
|
||||
BlockId,
|
||||
@@ -169,6 +169,7 @@ function matchOptionalTestBlock(
|
||||
propertyId: IdentifierId;
|
||||
storeLocalInstr: Instruction;
|
||||
consequentGoto: BlockId;
|
||||
propertyLoadLoc: SourceLocation;
|
||||
} | null {
|
||||
const consequentBlock = assertNonNull(blocks.get(terminal.consequent));
|
||||
if (
|
||||
@@ -221,6 +222,7 @@ function matchOptionalTestBlock(
|
||||
propertyId: propertyLoad.lvalue.identifier.id,
|
||||
storeLocalInstr,
|
||||
consequentGoto: consequentBlock.terminal.block,
|
||||
propertyLoadLoc: propertyLoad.loc,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
@@ -275,7 +277,11 @@ function traverseOptionalBlock(
|
||||
instrVal.kind === 'PropertyLoad' &&
|
||||
instrVal.object.identifier.id === prevInstr.lvalue.identifier.id
|
||||
) {
|
||||
path.push({property: instrVal.property, optional: false});
|
||||
path.push({
|
||||
property: instrVal.property,
|
||||
optional: false,
|
||||
loc: instrVal.loc,
|
||||
});
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
@@ -292,6 +298,7 @@ function traverseOptionalBlock(
|
||||
identifier: maybeTest.instructions[0].value.place.identifier,
|
||||
reactive: maybeTest.instructions[0].value.place.reactive,
|
||||
path,
|
||||
loc: maybeTest.instructions[0].value.place.loc,
|
||||
};
|
||||
test = maybeTest.terminal;
|
||||
} else if (maybeTest.terminal.kind === 'optional') {
|
||||
@@ -390,7 +397,7 @@ function traverseOptionalBlock(
|
||||
loc: optional.terminal.loc,
|
||||
},
|
||||
);
|
||||
const load = {
|
||||
const load: ReactiveScopeDependency = {
|
||||
identifier: baseObject.identifier,
|
||||
reactive: baseObject.reactive,
|
||||
path: [
|
||||
@@ -398,8 +405,10 @@ function traverseOptionalBlock(
|
||||
{
|
||||
property: matchConsequentResult.property,
|
||||
optional: optional.terminal.optional,
|
||||
loc: matchConsequentResult.propertyLoadLoc,
|
||||
},
|
||||
],
|
||||
loc: matchConsequentResult.propertyLoadLoc,
|
||||
};
|
||||
context.processedInstrsInOptional.add(matchConsequentResult.storeLocalInstr);
|
||||
context.processedInstrsInOptional.add(test);
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
Identifier,
|
||||
PropertyLiteral,
|
||||
ReactiveScopeDependency,
|
||||
SourceLocation,
|
||||
} from '../HIR';
|
||||
import {printIdentifier} from '../HIR/PrintHIR';
|
||||
|
||||
@@ -36,12 +37,13 @@ export class ReactiveScopeDependencyTreeHIR {
|
||||
* duplicates when traversing the CFG.
|
||||
*/
|
||||
constructor(hoistableObjects: Iterable<ReactiveScopeDependency>) {
|
||||
for (const {path, identifier, reactive} of hoistableObjects) {
|
||||
for (const {path, identifier, reactive, loc} of hoistableObjects) {
|
||||
let currNode = ReactiveScopeDependencyTreeHIR.#getOrCreateRoot(
|
||||
identifier,
|
||||
reactive,
|
||||
this.#hoistableObjects,
|
||||
path.length > 0 && path[0].optional ? 'Optional' : 'NonNull',
|
||||
loc,
|
||||
);
|
||||
|
||||
for (let i = 0; i < path.length; i++) {
|
||||
@@ -62,6 +64,7 @@ export class ReactiveScopeDependencyTreeHIR {
|
||||
nextNode = {
|
||||
properties: new Map(),
|
||||
accessType,
|
||||
loc: path[i].loc,
|
||||
};
|
||||
currNode.properties.set(path[i].property, nextNode);
|
||||
}
|
||||
@@ -75,6 +78,7 @@ export class ReactiveScopeDependencyTreeHIR {
|
||||
reactive: boolean,
|
||||
roots: Map<Identifier, TreeNode<T> & {reactive: boolean}>,
|
||||
defaultAccessType: T,
|
||||
loc: SourceLocation,
|
||||
): TreeNode<T> {
|
||||
// roots can always be accessed unconditionally in JS
|
||||
let rootNode = roots.get(identifier);
|
||||
@@ -84,6 +88,7 @@ export class ReactiveScopeDependencyTreeHIR {
|
||||
properties: new Map(),
|
||||
reactive,
|
||||
accessType: defaultAccessType,
|
||||
loc,
|
||||
};
|
||||
roots.set(identifier, rootNode);
|
||||
} else {
|
||||
@@ -102,12 +107,13 @@ export class ReactiveScopeDependencyTreeHIR {
|
||||
* safe-to-evaluate subpath
|
||||
*/
|
||||
addDependency(dep: ReactiveScopeDependency): void {
|
||||
const {identifier, reactive, path} = dep;
|
||||
const {identifier, reactive, path, loc} = dep;
|
||||
let depCursor = ReactiveScopeDependencyTreeHIR.#getOrCreateRoot(
|
||||
identifier,
|
||||
reactive,
|
||||
this.#deps,
|
||||
PropertyAccessType.UnconditionalAccess,
|
||||
loc,
|
||||
);
|
||||
/**
|
||||
* hoistableCursor is null if depCursor is not an object we can hoist
|
||||
@@ -153,6 +159,7 @@ export class ReactiveScopeDependencyTreeHIR {
|
||||
depCursor,
|
||||
entry.property,
|
||||
accessType,
|
||||
entry.loc,
|
||||
);
|
||||
} else if (
|
||||
hoistableCursor != null &&
|
||||
@@ -163,6 +170,7 @@ export class ReactiveScopeDependencyTreeHIR {
|
||||
depCursor,
|
||||
entry.property,
|
||||
PropertyAccessType.UnconditionalAccess,
|
||||
entry.loc,
|
||||
);
|
||||
} else {
|
||||
/**
|
||||
@@ -306,6 +314,7 @@ function merge(
|
||||
type TreeNode<T extends string> = {
|
||||
properties: Map<PropertyLiteral, TreeNode<T>>;
|
||||
accessType: T;
|
||||
loc: SourceLocation;
|
||||
};
|
||||
type HoistableNode = TreeNode<'Optional' | 'NonNull'>;
|
||||
type DependencyNode = TreeNode<PropertyAccessType>;
|
||||
@@ -323,7 +332,7 @@ function collectMinimalDependenciesInSubtree(
|
||||
results: Set<ReactiveScopeDependency>,
|
||||
): void {
|
||||
if (isDependency(node.accessType)) {
|
||||
results.add({identifier: rootIdentifier, reactive, path});
|
||||
results.add({identifier: rootIdentifier, reactive, path, loc: node.loc});
|
||||
} else {
|
||||
for (const [childName, childNode] of node.properties) {
|
||||
collectMinimalDependenciesInSubtree(
|
||||
@@ -335,6 +344,7 @@ function collectMinimalDependenciesInSubtree(
|
||||
{
|
||||
property: childName,
|
||||
optional: isOptional(childNode.accessType),
|
||||
loc: childNode.loc,
|
||||
},
|
||||
],
|
||||
results,
|
||||
@@ -362,12 +372,14 @@ function makeOrMergeProperty(
|
||||
node: DependencyNode,
|
||||
property: PropertyLiteral,
|
||||
accessType: PropertyAccessType,
|
||||
loc: SourceLocation,
|
||||
): DependencyNode {
|
||||
let child = node.properties.get(property);
|
||||
if (child == null) {
|
||||
child = {
|
||||
properties: new Map(),
|
||||
accessType,
|
||||
loc,
|
||||
};
|
||||
node.properties.set(property, child);
|
||||
} else {
|
||||
|
||||
@@ -1639,6 +1639,7 @@ export function makePropertyLiteral(value: string | number): PropertyLiteral {
|
||||
export type DependencyPathEntry = {
|
||||
property: PropertyLiteral;
|
||||
optional: boolean;
|
||||
loc: SourceLocation;
|
||||
};
|
||||
export type DependencyPath = Array<DependencyPathEntry>;
|
||||
export type ReactiveScopeDependency = {
|
||||
@@ -1656,6 +1657,7 @@ export type ReactiveScopeDependency = {
|
||||
*/
|
||||
reactive: boolean;
|
||||
path: DependencyPath;
|
||||
loc: SourceLocation;
|
||||
};
|
||||
|
||||
export function areEqualPaths(a: DependencyPath, b: DependencyPath): boolean {
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
ObjectMethod,
|
||||
PropertyLiteral,
|
||||
convertHoistedLValueKind,
|
||||
SourceLocation,
|
||||
} from './HIR';
|
||||
import {
|
||||
collectHoistablePropertyLoads,
|
||||
@@ -298,6 +299,7 @@ function collectTemporariesSidemapImpl(
|
||||
value.object,
|
||||
value.property,
|
||||
false,
|
||||
value.loc,
|
||||
temporaries,
|
||||
);
|
||||
temporaries.set(lvalue.identifier.id, property);
|
||||
@@ -318,6 +320,7 @@ function collectTemporariesSidemapImpl(
|
||||
identifier: value.place.identifier,
|
||||
reactive: value.place.reactive,
|
||||
path: [],
|
||||
loc: value.loc,
|
||||
});
|
||||
}
|
||||
} else if (
|
||||
@@ -339,6 +342,7 @@ function getProperty(
|
||||
object: Place,
|
||||
propertyName: PropertyLiteral,
|
||||
optional: boolean,
|
||||
loc: SourceLocation,
|
||||
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
|
||||
): ReactiveScopeDependency {
|
||||
/*
|
||||
@@ -371,13 +375,18 @@ function getProperty(
|
||||
property = {
|
||||
identifier: object.identifier,
|
||||
reactive: object.reactive,
|
||||
path: [{property: propertyName, optional}],
|
||||
path: [{property: propertyName, optional, loc}],
|
||||
loc,
|
||||
};
|
||||
} else {
|
||||
property = {
|
||||
identifier: resolvedDependency.identifier,
|
||||
reactive: resolvedDependency.reactive,
|
||||
path: [...resolvedDependency.path, {property: propertyName, optional}],
|
||||
path: [
|
||||
...resolvedDependency.path,
|
||||
{property: propertyName, optional, loc},
|
||||
],
|
||||
loc,
|
||||
};
|
||||
}
|
||||
return property;
|
||||
@@ -537,6 +546,7 @@ export class DependencyCollectionContext {
|
||||
identifier: place.identifier,
|
||||
reactive: place.reactive,
|
||||
path: [],
|
||||
loc: place.loc,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -545,11 +555,13 @@ export class DependencyCollectionContext {
|
||||
object: Place,
|
||||
property: PropertyLiteral,
|
||||
optional: boolean,
|
||||
loc: SourceLocation,
|
||||
): void {
|
||||
const nextDependency = getProperty(
|
||||
object,
|
||||
property,
|
||||
optional,
|
||||
loc,
|
||||
this.#temporaries,
|
||||
);
|
||||
this.visitDependency(nextDependency);
|
||||
@@ -602,6 +614,7 @@ export class DependencyCollectionContext {
|
||||
identifier: maybeDependency.identifier,
|
||||
reactive: maybeDependency.reactive,
|
||||
path: [],
|
||||
loc: maybeDependency.loc,
|
||||
};
|
||||
}
|
||||
if (this.#checkValidDependency(maybeDependency)) {
|
||||
@@ -626,6 +639,7 @@ export class DependencyCollectionContext {
|
||||
identifier: place.identifier,
|
||||
reactive: place.reactive,
|
||||
path: [],
|
||||
loc: place.loc,
|
||||
})
|
||||
) {
|
||||
currentScope.reassignments.add(place.identifier);
|
||||
@@ -679,7 +693,7 @@ export function handleInstruction(
|
||||
return;
|
||||
}
|
||||
if (value.kind === 'PropertyLoad') {
|
||||
context.visitProperty(value.object, value.property, false);
|
||||
context.visitProperty(value.object, value.property, false, value.loc);
|
||||
} else if (value.kind === 'StoreLocal') {
|
||||
context.visitOperand(value.value);
|
||||
if (value.lvalue.kind === InstructionKind.Reassign) {
|
||||
|
||||
@@ -74,7 +74,10 @@ export function collectMaybeMemoDependencies(
|
||||
return {
|
||||
root: object.root,
|
||||
// TODO: determine if the access is optional
|
||||
path: [...object.path, {property: value.property, optional}],
|
||||
path: [
|
||||
...object.path,
|
||||
{property: value.property, optional, loc: value.loc},
|
||||
],
|
||||
loc: value.loc,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -470,6 +470,7 @@ function canMergeScopes(
|
||||
identifier: declaration.identifier,
|
||||
reactive: true,
|
||||
path: [],
|
||||
loc: GeneratedSource,
|
||||
})),
|
||||
),
|
||||
next.scope.dependencies,
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
printIdentifier,
|
||||
printInstructionValue,
|
||||
printPlace,
|
||||
printSourceLocation,
|
||||
printType,
|
||||
} from '../HIR/PrintHIR';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
@@ -114,7 +115,7 @@ export function printDependency(dependency: ReactiveScopeDependency): string {
|
||||
const identifier =
|
||||
printIdentifier(dependency.identifier) +
|
||||
printType(dependency.identifier.type);
|
||||
return `${identifier}${dependency.path.map(token => `${token.optional ? '?.' : '.'}${token.property}`).join('')}`;
|
||||
return `${identifier}${dependency.path.map(token => `${token.optional ? '?.' : '.'}${token.property}`).join('')}_${printSourceLocation(dependency.loc)}`;
|
||||
}
|
||||
|
||||
export function printReactiveInstructions(
|
||||
|
||||
@@ -756,6 +756,7 @@ function collectDependencies(
|
||||
{
|
||||
optional,
|
||||
property: value.property,
|
||||
loc: value.loc,
|
||||
},
|
||||
],
|
||||
loc: value.loc,
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @flow
|
||||
import {Stringify} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* Example fixture demonstrating a case where we could hoist dependencies
|
||||
* and reuse them across scopes. Here we extract a temporary for `item.value`
|
||||
* and reference it both in the scope for `a`. Then the scope for `c` could
|
||||
* use `<item-value-temp>.inner` as its dependency, avoiding reloading
|
||||
* `item.value`.
|
||||
*/
|
||||
function Test({item, index}: {item: {value: {inner: any}}, index: number}) {
|
||||
// These scopes have the same dependency, `item.value`, and could
|
||||
// share a hoisted expression to evaluate it
|
||||
const a = [];
|
||||
if (index) {
|
||||
a.push({value: item.value, index});
|
||||
}
|
||||
const b = [item.value];
|
||||
|
||||
// This dependency is more precise (nested property), the outer
|
||||
// `item.value` portion could use a hoisted dep for `item.value
|
||||
const c = [item.value.inner];
|
||||
return <Stringify value={[a, b, c]} />;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { Stringify } from "shared-runtime";
|
||||
|
||||
function Test(t0) {
|
||||
const $ = _c(11);
|
||||
const { item, index } = t0;
|
||||
let a;
|
||||
if ($[0] !== index || $[1] !== item.value) {
|
||||
a = [];
|
||||
if (index) {
|
||||
a.push({ value: item.value, index });
|
||||
}
|
||||
$[0] = index;
|
||||
$[1] = item.value;
|
||||
$[2] = a;
|
||||
} else {
|
||||
a = $[2];
|
||||
}
|
||||
let t1;
|
||||
if ($[3] !== item.value) {
|
||||
t1 = [item.value];
|
||||
$[3] = item.value;
|
||||
$[4] = t1;
|
||||
} else {
|
||||
t1 = $[4];
|
||||
}
|
||||
const b = t1;
|
||||
let t2;
|
||||
if ($[5] !== item.value.inner) {
|
||||
t2 = [item.value.inner];
|
||||
$[5] = item.value.inner;
|
||||
$[6] = t2;
|
||||
} else {
|
||||
t2 = $[6];
|
||||
}
|
||||
const c = t2;
|
||||
let t3;
|
||||
if ($[7] !== a || $[8] !== b || $[9] !== c) {
|
||||
t3 = <Stringify value={[a, b, c]} />;
|
||||
$[7] = a;
|
||||
$[8] = b;
|
||||
$[9] = c;
|
||||
$[10] = t3;
|
||||
} else {
|
||||
t3 = $[10];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -0,0 +1,24 @@
|
||||
// @flow
|
||||
import {Stringify} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* Example fixture demonstrating a case where we could hoist dependencies
|
||||
* and reuse them across scopes. Here we extract a temporary for `item.value`
|
||||
* and reference it both in the scope for `a`. Then the scope for `c` could
|
||||
* use `<item-value-temp>.inner` as its dependency, avoiding reloading
|
||||
* `item.value`.
|
||||
*/
|
||||
function Test({item, index}: {item: {value: {inner: any}}, index: number}) {
|
||||
// These scopes have the same dependency, `item.value`, and could
|
||||
// share a hoisted expression to evaluate it
|
||||
const a = [];
|
||||
if (index) {
|
||||
a.push({value: item.value, index});
|
||||
}
|
||||
const b = [item.value];
|
||||
|
||||
// This dependency is more precise (nested property), the outer
|
||||
// `item.value` portion could use a hoisted dep for `item.value
|
||||
const c = [item.value.inner];
|
||||
return <Stringify value={[a, b, c]} />;
|
||||
}
|
||||
Reference in New Issue
Block a user