Compare commits

...

12 Commits

Author SHA1 Message Date
Joe Savona
e85dc80ac2 Update base for Update on "[compiler] Type provider infra for tests"
Adds snap/sprout support for type providers, with declarations describing some new functions in shared-runtime. This allows us to verify that the type provider infrastructure is working. I had to make a few fixes to workaround limitations in zod, but i think the types here are mostly stable enough that we won't have too much work keeping things in sync.

[ghstack-poisoned]
2024-08-21 15:40:25 -07:00
Joe Savona
7ce5834967 Update base for Update on "[compiler] Type provider infra for tests"
Adds snap/sprout support for type providers, with declarations describing some new functions in shared-runtime. This allows us to verify that the type provider infrastructure is working. I had to make a few fixes to workaround limitations in zod, but i think the types here are mostly stable enough that we won't have too much work keeping things in sync.

[ghstack-poisoned]
2024-08-21 15:32:28 -07:00
Joe Savona
0d0ca80a7c Update base for Update on "[compiler] Type provider infra for tests"
Adds snap/sprout support for type providers, with declarations describing some new functions in shared-runtime. This allows us to verify that the type provider infrastructure is working. I had to make a few fixes to workaround limitations in zod, but i think the types here are mostly stable enough that we won't have too much work keeping things in sync.

[ghstack-poisoned]
2024-08-21 15:16:54 -07:00
Joe Savona
8350d693ab Update base for Update on "[compiler] Type provider infra for tests"
Adds snap/sprout support for type providers, with declarations describing some new functions in shared-runtime. This allows us to verify that the type provider infrastructure is working. I had to make a few fixes to workaround limitations in zod, but i think the types here are mostly stable enough that we won't have too much work keeping things in sync.

[ghstack-poisoned]
2024-08-21 14:31:42 -07:00
Joe Savona
8a83d67955 Update base for Update on "[compiler] Type provider infra for tests"
Adds snap/sprout support for type providers, with declarations describing some new functions in shared-runtime. This allows us to verify that the type provider infrastructure is working. I had to make a few fixes to workaround limitations in zod, but i think the types here are mostly stable enough that we won't have too much work keeping things in sync.

[ghstack-poisoned]
2024-08-21 14:20:00 -07:00
Joe Savona
977ea578e5 Update base for Update on "[compiler] Type provider infra for tests"
Adds snap/sprout support for type providers, with declarations describing some new functions in shared-runtime. This allows us to verify that the type provider infrastructure is working. I had to make a few fixes to workaround limitations in zod, but i think the types here are mostly stable enough that we won't have too much work keeping things in sync.

[ghstack-poisoned]
2024-08-21 14:16:52 -07:00
Joe Savona
3968efee62 Update base for Update on "[compiler] Type provider infra for tests"
[ghstack-poisoned]
2024-08-21 14:07:13 -07:00
Joe Savona
86da0d6e46 Update on "[compiler][wip] Environment option for resolving imported module types"
Adds a new Environment config option which allows specifying a function that is called to resolve types of imported modules. The function is passed the name of the imported module (the RHS of the import stmt) and can return a TypeConfig, which is a recursive type of the following form:

* Object of valid identifier keys (or "*" for wildcard) and values that are TypeConfigs
* Function with various properties, whose return type is a TypeConfig
* or a reference to a builtin type using one of a small list (currently Ref, Array, MixedReadonly, Primitive)

Rather than have to eagerly supply all known types (most of which may not be used) when creating the config, this function can do so lazily. During InferTypes we call `getGlobalDeclaration()` to resolve global types. Originally this was just for known react modules, but if the new config option is passed we also call it to see if it can resolve a type. For `import {name} from 'module'` syntax, we first resolve the module type and then call `getPropertyType(moduleType, 'name')` to attempt to retrieve the property of the module (the module would obviously have to be typed as an object type for this to have a chance of yielding a result). If the module type is returned as null, or the property doesn't exist, we fall through to the original checking of whether the name was hook-like.

TODO:
* testing
* cache the results of modules so we don't have to re-parse/install their types on each LoadGlobal of the same module
* decide what to do if the module types are invalid. probably better to fatal rather than bail out, since this would indicate an invalid configuration.

[ghstack-poisoned]
2024-08-20 23:49:52 -07:00
Joe Savona
d3085044bd Update on "[compiler][wip] Environment option for resolving imported module types"
Adds a new Environment config option which allows specifying a function that is called to resolve types of imported modules. The function is passed the name of the imported module (the RHS of the import stmt) and can return a TypeConfig, which is a recursive type of the following form:

* Object of valid identifier keys (or "*" for wildcard) and values that are TypeConfigs
* Function with various properties, whose return type is a TypeConfig
* or a reference to a builtin type using one of a small list (currently Ref, Array, MixedReadonly, Primitive)

Rather than have to eagerly supply all known types (most of which may not be used) when creating the config, this function can do so lazily. During InferTypes we call `getGlobalDeclaration()` to resolve global types. Originally this was just for known react modules, but if the new config option is passed we also call it to see if it can resolve a type. For `import {name} from 'module'` syntax, we first resolve the module type and then call `getPropertyType(moduleType, 'name')` to attempt to retrieve the property of the module (the module would obviously have to be typed as an object type for this to have a chance of yielding a result). If the module type is returned as null, or the property doesn't exist, we fall through to the original checking of whether the name was hook-like.

TODO:
* testing
* cache the results of modules so we don't have to re-parse/install their types on each LoadGlobal of the same module
* decide what to do if the module types are invalid. probably better to fatal rather than bail out, since this would indicate an invalid configuration.

[ghstack-poisoned]
2024-08-20 23:40:28 -07:00
Joe Savona
f1987494be [compiler][wip] Environment option for resolving imported module types
Adds a new Environment config option which allows specifying a function that is called to resolve types of imported modules. The function is passed the name of the imported module (the RHS of the import stmt) and can return a TypeConfig, which is a recursive type of the following form:

* Object of valid identifier keys (or "*" for wildcard) and values that are TypeConfigs
* Function with various properties, whose return type is a TypeConfig
* or a reference to a builtin type using one of a small list (currently Ref, Array, MixedReadonly, Primitive)

Rather than have to eagerly supply all known types (most of which may not be used) when creating the config, this function can do so lazily. During InferTypes we call `getGlobalDeclaration()` to resolve global types. Originally this was just for known react modules, but if the new config option is passed we also call it to see if it can resolve a type. For `import {name} from 'module'` syntax, we first resolve the module type and then call `getPropertyType(moduleType, 'name')` to attempt to retrieve the property of the module (the module would obviously have to be typed as an object type for this to have a chance of yielding a result). If the module type is returned as null, or the property doesn't exist, we fall through to the original checking of whether the name was hook-like.

TODO:
* testing
* cache the results of modules so we don't have to re-parse/install their types on each LoadGlobal of the same module
* decide what to do if the module types are invalid. probably better to fatal rather than bail out, since this would indicate an invalid configuration.

[ghstack-poisoned]
2024-08-20 23:34:18 -07:00
Joe Savona
28ee171614 [compiler] Transitively freezing functions marks values as frozen, not effects
The fixture from the previous PR was getting inconsistent behavior because of the following:
1. Create an object in a useMemo
2. Create a callback in a useCallback, where the callback captures the object from (1) into a local object, then passes that local object into a logging method. We have to assume the logging method could modify the local object, and transitively, the object from (1).
3. Call the callback during render.
4. Pass the callback to JSX.

We correctly infer that the object from (1) is captured and modified in (2). However, in (4) we transitively freeze the callback. When transitively freezing functions we were previously doing two things: updating our internal abstract model of the program values to reflect the values as being frozen *and* also updating function operands to change their effects to freeze.

As the case above demonstrates, that can clobber over information about real potential mutability. The potential fix here is to only walk our abstract value model to mark values as frozen, but _not_ override operand effects. Conceptually, this is a forward data flow propagation — but walking backward to update effects is pushing information backwards in the algorithm. An alternative would be to mark that data was propagated backwards, and trigger another loop over the CFG to propagate information forward again given the updated effects. But the fix in this PR is more correct.

[ghstack-poisoned]
2024-08-20 17:26:57 -07:00
Joe Savona
c9bb95eefc [compiler] Repro for missing memoization due to inferred mutation
This fixture bails out on ValidatePreserveExistingMemo but would ideally memoize since the original memoization is safe. It's trivial to make it pass by commenting out the commented line (`LogEvent.log(() => object)`). I would expect the compiler to infer this as possible mutation of `logData`, since `object` captures a reference to `logData`. But somehow `logData` is getting memoized successfully, but we still infer the callback, `setCurrentIndex`, as having a mutable range that extends to the `setCurrentIndex()` call after the useCallback.

[ghstack-poisoned]
2024-08-20 15:36:22 -07:00
9 changed files with 384 additions and 54 deletions

View File

@@ -17,6 +17,7 @@ import {
Global,
GlobalRegistry,
installReAnimatedTypes,
installTypeConfig,
} from './Globals';
import {
BlockId,
@@ -45,6 +46,7 @@ import {
addHook,
} from './ObjectShape';
import {Scope as BabelScope} from '@babel/traverse';
import {TypeSchema} from './TypeSchema';
export const ExternalFunctionSchema = z.object({
// Source for the imported module that exports the `importSpecifierName` functions
@@ -124,6 +126,11 @@ const HookSchema = z.object({
export type Hook = z.infer<typeof HookSchema>;
export const ModuleTypeResolver = z
.function()
.args(z.string())
.returns(z.nullable(TypeSchema));
/*
* TODO(mofeiZ): User defined global types (with corresponding shapes).
* User defined global types should have inline ObjectShapes instead of directly
@@ -137,6 +144,12 @@ export type Hook = z.infer<typeof HookSchema>;
const EnvironmentConfigSchema = z.object({
customHooks: z.map(z.string(), HookSchema).optional().default(new Map()),
/**
* A function that, given the name of a module, can optionally return a description
* of that module's type signature.
*/
resolveModuleTypeSchema: z.nullable(ModuleTypeResolver).default(null),
/**
* A list of functions which the application compiles as macros, where
* the compiler must ensure they are not compiled to rename the macro or separate the
@@ -577,6 +590,7 @@ export function printFunctionType(type: ReactFunctionType): string {
export class Environment {
#globals: GlobalRegistry;
#shapes: ShapeRegistry;
#moduleTypes: Map<string, Global | null> = new Map();
#nextIdentifer: number = 0;
#nextBlock: number = 0;
#nextScope: number = 0;
@@ -698,6 +712,28 @@ export class Environment {
return this.#outlinedFunctions;
}
#resolveModuleType(moduleName: string): Global | null {
if (this.config.resolveModuleTypeSchema == null) {
return null;
}
let moduleType = this.#moduleTypes.get(moduleName);
if (moduleType === undefined) {
const moduleConfig = this.config.resolveModuleTypeSchema(moduleName);
if (moduleConfig != null) {
const moduleTypes = TypeSchema.parse(moduleConfig);
moduleType = installTypeConfig(
this.#globals,
this.#shapes,
moduleTypes,
);
} else {
moduleType = null;
}
this.#moduleTypes.set(moduleName, moduleType);
}
return moduleType;
}
getGlobalDeclaration(binding: NonLocalBinding): Global | null {
if (this.config.hookPattern != null) {
const match = new RegExp(this.config.hookPattern).exec(binding.name);
@@ -736,6 +772,17 @@ export class Environment {
(isHookName(binding.imported) ? this.#getCustomHookType() : null)
);
} else {
const moduleType = this.#resolveModuleType(binding.module);
if (moduleType !== null) {
const importedType = this.getPropertyType(
moduleType,
binding.imported,
);
if (importedType != null) {
return importedType;
}
}
/**
* For modules we don't own, we look at whether the original name or import alias
* are hook-like. Both of the following are likely hooks so we would return a hook
@@ -758,6 +805,11 @@ export class Environment {
(isHookName(binding.name) ? this.#getCustomHookType() : null)
);
} else {
const moduleType = this.#resolveModuleType(binding.module);
if (moduleType !== null) {
// TODO: distinguish default/namespace cases
return moduleType;
}
return isHookName(binding.name) ? this.#getCustomHookType() : null;
}
}

View File

@@ -9,6 +9,7 @@ import {Effect, ValueKind, ValueReason} from './HIR';
import {
BUILTIN_SHAPES,
BuiltInArrayId,
BuiltInMixedReadonlyId,
BuiltInUseActionStateId,
BuiltInUseContextHookId,
BuiltInUseEffectHookId,
@@ -25,6 +26,8 @@ import {
addObject,
} from './ObjectShape';
import {BuiltInType, PolyType} from './Types';
import {TypeConfig} from './TypeSchema';
import {assertExhaustive} from '../Utils/utils';
/*
* This file exports types and defaults for JavaScript global objects.
@@ -528,6 +531,56 @@ DEFAULT_GLOBALS.set(
addObject(DEFAULT_SHAPES, 'global', TYPED_GLOBALS),
);
export function installTypeConfig(
globals: GlobalRegistry,
shapes: ShapeRegistry,
typeConfig: TypeConfig,
): Global {
switch (typeConfig.kind) {
case 'type': {
switch (typeConfig.name) {
case 'Array': {
return {kind: 'Object', shapeId: BuiltInArrayId};
}
case 'MixedReadonly': {
return {kind: 'Object', shapeId: BuiltInMixedReadonlyId};
}
case 'Primitive': {
return {kind: 'Primitive'};
}
case 'Ref': {
return {kind: 'Object', shapeId: BuiltInUseRefId};
}
default: {
assertExhaustive(
typeConfig.name,
`Unexpected type '${(typeConfig as any).name}'`,
);
}
}
}
case 'function': {
return addFunction(shapes, [], {
positionalParams: typeConfig.positionalParams,
restParam: typeConfig.restParam,
calleeEffect: typeConfig.calleeEffect,
returnType: installTypeConfig(globals, shapes, typeConfig.returnType),
returnValueKind: typeConfig.returnValueKind,
});
}
case 'object': {
return addObject(
shapes,
null,
Object.entries(typeConfig.properties ?? {}).map(([key, value]) => [
key,
installTypeConfig(globals, shapes, value),
]),
);
}
}
}
export function installReAnimatedTypes(
globals: GlobalRegistry,
registry: ShapeRegistry,

View File

@@ -12,6 +12,7 @@ import {assertExhaustive} from '../Utils/utils';
import {Environment, ReactFunctionType} from './Environment';
import {HookKind} from './ObjectShape';
import {Type, makeType} from './Types';
import {z} from 'zod';
/*
* *******************************************************************************************
@@ -1389,6 +1390,15 @@ export enum Effect {
Store = 'store',
}
export const EffectSchema = z.enum([
Effect.Read,
Effect.Mutate,
Effect.ConditionallyMutate,
Effect.Capture,
Effect.Store,
Effect.Freeze,
]);
export function isMutableEffect(
effect: Effect,
location: SourceLocation,

View File

@@ -0,0 +1,76 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {isValidIdentifier} from '@babel/types';
import {z} from 'zod';
import {Effect, ValueKind} from '..';
import {EffectSchema} from './HIR';
export type ObjectPropertiesConfig = {[key: string]: TypeConfig};
export const ObjectPropertiesSchema: z.ZodType<ObjectPropertiesConfig> = z
.record(
z.string(),
z.lazy(() => TypeSchema),
)
.refine(record => {
return Object.keys(record).every(
key => key === '*' || isValidIdentifier(key),
);
}, 'Expected all "object" property names to be valid identifiers or `*` to match any property');
export type ObjectTypeConfig = {
kind: 'object';
properties: ObjectPropertiesConfig | null;
};
export const ObjectTypeSchema: z.ZodType<ObjectTypeConfig> = z.object({
kind: z.literal('object'),
properties: ObjectPropertiesSchema.nullable(),
});
export type FunctionTypeConfig = {
kind: 'function';
positionalParams: Array<Effect>;
restParam: Effect | null;
calleeEffect: Effect;
returnType: TypeConfig;
returnValueKind: ValueKind;
};
export const FunctionTypeSchema: z.ZodType<FunctionTypeConfig> = z.object({
kind: z.literal('function'),
positionalParams: z.array(EffectSchema),
restParam: EffectSchema.nullable(),
calleeEffect: EffectSchema,
returnType: z.lazy(() => TypeSchema),
returnValueKind: z.nativeEnum(ValueKind),
});
export type BuiltInTypeConfig = 'Ref' | 'Array' | 'Primitive' | 'MixedReadonly';
export const BuiltInTypeSchema: z.ZodType<BuiltInTypeConfig> = z.union([
z.literal('Ref'),
z.literal('Array'),
z.literal('Primitive'),
z.literal('MixedReadonly'),
]);
export type TypeReferenceConfig = {
kind: 'type';
name: BuiltInTypeConfig;
};
export const TypeReferenceSchema: z.ZodType<TypeReferenceConfig> = z.object({
kind: z.literal('type'),
name: BuiltInTypeSchema,
});
export type TypeConfig =
| ObjectTypeConfig
| FunctionTypeConfig
| TypeReferenceConfig;
export const TypeSchema: z.ZodType<TypeConfig> = z.union([
ObjectTypeSchema,
FunctionTypeSchema,
TypeReferenceSchema,
]);

View File

@@ -453,6 +453,37 @@ class InferenceState {
}
}
freezeValues(values: Set<InstructionValue>, reason: Set<ValueReason>): void {
for (const value of values) {
this.#values.set(value, {
kind: ValueKind.Frozen,
reason,
context: new Set(),
});
if (value.kind === 'FunctionExpression') {
if (
this.#env.config.enablePreserveExistingMemoizationGuarantees ||
this.#env.config.enableTransitivelyFreezeFunctionExpressions
) {
if (value.kind === 'FunctionExpression') {
/*
* We want to freeze the captured values, not mark the operands
* themselves as frozen. There could be mutations that occur
* before the freeze we are processing, and it would be invalid
* to overwrite those mutations as a freeze.
*/
for (const operand of eachInstructionValueOperand(value)) {
const operandValues = this.#variables.get(operand.identifier.id);
if (operandValues !== undefined) {
this.freezeValues(operandValues, reason);
}
}
}
}
}
}
}
reference(
place: Place,
effectKind: Effect,
@@ -482,29 +513,7 @@ class InferenceState {
reason: reasonSet,
context: new Set(),
};
values.forEach(value => {
this.#values.set(value, {
kind: ValueKind.Frozen,
reason: reasonSet,
context: new Set(),
});
if (
this.#env.config.enablePreserveExistingMemoizationGuarantees ||
this.#env.config.enableTransitivelyFreezeFunctionExpressions
) {
if (value.kind === 'FunctionExpression') {
for (const operand of eachInstructionValueOperand(value)) {
this.referenceAndRecordEffects(
operand,
Effect.Freeze,
ValueReason.Other,
[],
);
}
}
}
});
this.freezeValues(values, reasonSet);
} else {
effect = Effect.Read;
}
@@ -1241,6 +1250,7 @@ function inferBlock(
case 'ObjectMethod':
case 'FunctionExpression': {
let hasMutableOperand = false;
const mutableOperands: Array<Place> = [];
for (const operand of eachInstructionOperand(instr)) {
state.referenceAndRecordEffects(
operand,
@@ -1248,6 +1258,9 @@ function inferBlock(
ValueReason.Other,
[],
);
if (isMutableEffect(operand.effect, operand.loc)) {
mutableOperands.push(operand);
}
hasMutableOperand ||= isMutableEffect(operand.effect, operand.loc);
/**

View File

@@ -23,7 +23,7 @@ import {
ScopeId,
SourceLocation,
} from '../HIR';
import {printManualMemoDependency} from '../HIR/PrintHIR';
import {printIdentifier, printManualMemoDependency} from '../HIR/PrintHIR';
import {eachInstructionValueOperand} from '../HIR/visitors';
import {collectMaybeMemoDependencies} from '../Inference/DropManualMemoization';
import {
@@ -537,7 +537,9 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
state.errors.push({
reason:
'React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This value was memoized in source but not in compilation output.',
description: null,
description: DEBUG
? `${printIdentifier(identifier)} was not memoized`
: null,
severity: ErrorSeverity.CannotPreserveMemoization,
loc,
suggestions: null,

View File

@@ -46,54 +46,57 @@ import { useEffect, useState } from "react";
let someGlobal = { value: null };
function Component() {
const $ = _c(6);
const $ = _c(7);
const [state, setState] = useState(someGlobal);
let x = someGlobal;
while (x == null) {
x = someGlobal;
}
const y = x;
let t0;
let t1;
let t2;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = () => {
let x = someGlobal;
while (x == null) {
x = someGlobal;
}
const y = x;
t0 = useEffect;
t1 = () => {
y.value = "hello";
};
t1 = [];
t2 = [];
$[0] = t0;
$[1] = t1;
$[2] = t2;
} else {
t0 = $[0];
t1 = $[1];
t2 = $[2];
}
useEffect(t0, t1);
let t2;
t0(t1, t2);
let t3;
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
t2 = () => {
let t4;
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
t3 = () => {
setState(someGlobal.value);
};
t3 = [someGlobal];
$[2] = t2;
t4 = [someGlobal];
$[3] = t3;
} else {
t2 = $[2];
t3 = $[3];
}
useEffect(t2, t3);
const t4 = String(state);
let t5;
if ($[4] !== t4) {
t5 = <div>{t4}</div>;
$[4] = t4;
$[5] = t5;
} else {
t5 = $[5];
t3 = $[3];
t4 = $[4];
}
return t5;
useEffect(t3, t4);
const t5 = String(state);
let t6;
if ($[5] !== t5) {
t6 = <div>{t5}</div>;
$[5] = t5;
$[6] = t6;
} else {
t6 = $[6];
}
return t6;
}
export const FIXTURE_ENTRYPOINT = {

View File

@@ -0,0 +1,78 @@
## Input
```javascript
// @flow @validatePreserveExistingMemoizationGuarantees
import {useFragment} from 'react-relay';
import LogEvent from 'LogEvent';
import {useCallback, useMemo} from 'react';
component Component(id) {
const items = useFragment();
const [index, setIndex] = useState(0);
const logData = useMemo(() => {
const item = items[index];
return {
key: item.key,
};
}, [index, items]);
const setCurrentIndex = useCallback(
(index: number) => {
const object = {
tracking: logData.key,
};
// We infer that this may mutate `object`, which in turn aliases
// data from `logData`, such that `logData` may be mutated.
LogEvent.log(() => object);
setIndex(index);
},
[index, logData, items]
);
if (prevId !== id) {
setCurrentIndex(0);
}
return (
<Foo
index={index}
items={items}
current={mediaList[index]}
setCurrentIndex={setCurrentIndex}
/>
);
}
```
## Error
```
9 | const [index, setIndex] = useState(0);
10 |
> 11 | const logData = useMemo(() => {
| ^^^^^^^^^^^^^^^
> 12 | const item = items[index];
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 13 | return {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 14 | key: item.key,
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 15 | };
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 16 | }, [index, items]);
| ^^^^^^^^^^^^^^^^^^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This value was memoized in source but not in compilation output. (11:16)
CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This dependency may be mutated later, which could cause the value to change unexpectedly (28:28)
CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This value was memoized in source but not in compilation output. (19:27)
17 |
18 | const setCurrentIndex = useCallback(
19 | (index: number) => {
```

View File

@@ -0,0 +1,43 @@
// @flow @validatePreserveExistingMemoizationGuarantees
import {useFragment} from 'react-relay';
import LogEvent from 'LogEvent';
import {useCallback, useMemo} from 'react';
component Component(id) {
const items = useFragment();
const [index, setIndex] = useState(0);
const logData = useMemo(() => {
const item = items[index];
return {
key: item.key,
};
}, [index, items]);
const setCurrentIndex = useCallback(
(index: number) => {
const object = {
tracking: logData.key,
};
// We infer that this may mutate `object`, which in turn aliases
// data from `logData`, such that `logData` may be mutated.
LogEvent.log(() => object);
setIndex(index);
},
[index, logData, items]
);
if (prevId !== id) {
setCurrentIndex(0);
}
return (
<Foo
index={index}
items={items}
current={mediaList[index]}
setCurrentIndex={setCurrentIndex}
/>
);
}