Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ import {validateNoSetStateInPassiveEffects} from '../Validation/ValidateNoSetSta
import {validateNoJSXInTryStatement} from '../Validation/ValidateNoJSXInTryStatement';
import {propagateScopeDependenciesHIR} from '../HIR/PropagateScopeDependenciesHIR';
import {outlineJSX} from '../Optimization/OutlineJsx';
import {validateUseState} from '../Validation/ValidateUseState';

export type CompilerPipelineValue =
| {kind: 'ast'; name: string; value: CodegenFunction}
Expand Down Expand Up @@ -251,6 +252,10 @@ function* runWithEnvironment(
validateNoJSXInTryStatement(hir);
}

if (env.config.validateUseState) {
validateUseState(hir);
}

inferReactivePlaces(hir);
yield log({kind: 'hir', name: 'InferReactivePlaces', value: hir});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,15 @@ const EnvironmentConfigSchema = z.object({
validateNoCapitalizedCalls: z.nullable(z.array(z.string())).default(null),
validateBlocklistedImports: z.nullable(z.array(z.string())).default(null),

/**
* Validates that useState() calls use both the state and setter. If either are unused, the program
* may fail to update properly. An unused state value is generally used to force an update, but
* with the compiler applied a force update should not have any impact. An unused setter will cause
* the state value to never update, which implies turning a reactive state input into a non-reactive
* output, which will trigger stale UIs.
*/
validateUseState: z.boolean().default(false),

/*
* When enabled, the compiler assumes that hooks follow the Rules of React:
* - Hooks may memoize computation based on any of their parameters, thus
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/**
* 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 {CompilerError, ErrorSeverity} from '..';
import {HIRFunction, isUseStateType} from '../HIR';

/**
* Validates that `useState()` is not used for common anti-patterns:
*
* - Discarding the setter (`const [value] = useState(...)`) is a way
* to force a reactive value not to update, which is not recommended.
* - Discarding the value (`const [,setter] = useState(...)`) is a way
* to force-update the component, which is generally only required if
* there is some shared mutable value that isn't properly subscribed.
*
* Note: this pass relies on DCE having run first to prune unused patterns
* from destructuring of useState results.
*/
export function validateUseState(fn: HIRFunction): void {
const error = new CompilerError();
for (const [, block] of fn.body.blocks) {
for (const instr of block.instructions) {
const value = instr.value;
if (
value.kind !== 'Destructure' ||
!isUseStateType(value.value.identifier) ||
value.lvalue.pattern.kind !== 'ArrayPattern' ||
value.lvalue.pattern.items.some(item => item.kind === 'Spread')
) {
continue;
}
const items = value.lvalue.pattern.items;
if (items.length === 0 || items[0].kind === 'Hole') {
// unused state value
error.push({
reason:
'Using only a state setter, but not its value, will cause a component to re-render without updating',
description: null,
severity: ErrorSeverity.InvalidReact,
loc: value.value.loc,
suggestions: null,
});
}
if (items.length < 2 || items[1].kind === 'Hole') {
// unused setter
error.push({
reason:
'Using only a state value, but not its setter, will cause the component not to update when the state input changes',
description: null,
severity: ErrorSeverity.InvalidReact,
loc: value.value.loc,
suggestions: null,
});
}
}
}
if (error.hasErrors()) {
throw error;
}
}
Loading