main
ts 190 lines 7.24 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8 import {
9 CompilerDiagnostic,
10 CompilerError,
11 ErrorCategory,
12 } from '../CompilerError';
13 import {HIRFunction, IdentifierId, isSetStateType} from '../HIR';
14 import {computeUnconditionalBlocks} from '../HIR/ComputeUnconditionalBlocks';
15 import {eachInstructionValueOperand} from '../HIR/visitors';
16
17 /**
18 * Validates that the given function does not have an infinite update loop
19 * caused by unconditionally calling setState during render. This validation
20 * is conservative and cannot catch all cases of unconditional setState in
21 * render, but avoids false positives. Examples of cases that are caught:
22 *
23 * ```javascript
24 * // Direct call of setState:
25 * const [state, setState] = useState(false);
26 * setState(true);
27 *
28 * // Indirect via a function:
29 * const [state, setState] = useState(false);
30 * const setTrue = () => setState(true);
31 * setTrue();
32 * ```
33 *
34 * However, storing setState inside another value and accessing it is not yet
35 * validated:
36 *
37 * ```
38 * // false negative, not detected but will cause an infinite render loop
39 * const [state, setState] = useState(false);
40 * const x = [setState];
41 * const y = x.pop();
42 * y();
43 * ```
44 */
45 export function validateNoSetStateInRender(fn: HIRFunction): void {
46 const unconditionalSetStateFunctions: Set<IdentifierId> = new Set();
47 const errors = validateNoSetStateInRenderImpl(
48 fn,
49 unconditionalSetStateFunctions,
50 );
51 for (const detail of errors.details) {
52 fn.env.recordError(detail);
53 }
54 }
55
56 function validateNoSetStateInRenderImpl(
57 fn: HIRFunction,
58 unconditionalSetStateFunctions: Set<IdentifierId>,
59 ): CompilerError {
60 const unconditionalBlocks = computeUnconditionalBlocks(fn);
61 let activeManualMemoId: number | null = null;
62 const errors = new CompilerError();
63 for (const [, block] of fn.body.blocks) {
64 for (const instr of block.instructions) {
65 switch (instr.value.kind) {
66 case 'LoadLocal': {
67 if (
68 unconditionalSetStateFunctions.has(instr.value.place.identifier.id)
69 ) {
70 unconditionalSetStateFunctions.add(instr.lvalue.identifier.id);
71 }
72 break;
73 }
74 case 'StoreLocal': {
75 if (
76 unconditionalSetStateFunctions.has(instr.value.value.identifier.id)
77 ) {
78 unconditionalSetStateFunctions.add(
79 instr.value.lvalue.place.identifier.id,
80 );
81 unconditionalSetStateFunctions.add(instr.lvalue.identifier.id);
82 }
83 break;
84 }
85 case 'ObjectMethod':
86 case 'FunctionExpression': {
87 if (
88 // faster-path to check if the function expression references a setState
89 [...eachInstructionValueOperand(instr.value)].some(
90 operand =>
91 isSetStateType(operand.identifier) ||
92 unconditionalSetStateFunctions.has(operand.identifier.id),
93 ) &&
94 // if yes, does it unconditonally call it?
95 validateNoSetStateInRenderImpl(
96 instr.value.loweredFunc.func,
97 unconditionalSetStateFunctions,
98 ).hasAnyErrors()
99 ) {
100 // This function expression unconditionally calls a setState
101 unconditionalSetStateFunctions.add(instr.lvalue.identifier.id);
102 }
103 break;
104 }
105 case 'StartMemoize': {
106 CompilerError.invariant(activeManualMemoId === null, {
107 reason: 'Unexpected nested StartMemoize instructions',
108 loc: instr.value.loc,
109 });
110 activeManualMemoId = instr.value.manualMemoId;
111 break;
112 }
113 case 'FinishMemoize': {
114 CompilerError.invariant(
115 activeManualMemoId === instr.value.manualMemoId,
116 {
117 reason:
118 'Expected FinishMemoize to align with previous StartMemoize instruction',
119 loc: instr.value.loc,
120 },
121 );
122 activeManualMemoId = null;
123 break;
124 }
125 case 'CallExpression': {
126 const callee = instr.value.callee;
127 if (
128 isSetStateType(callee.identifier) ||
129 unconditionalSetStateFunctions.has(callee.identifier.id)
130 ) {
131 if (activeManualMemoId !== null) {
132 errors.pushDiagnostic(
133 CompilerDiagnostic.create({
134 category: ErrorCategory.RenderSetState,
135 reason:
136 'Calling setState from useMemo may trigger an infinite loop',
137 description:
138 'Each time the memo callback is evaluated it will change state. This can cause a memoization dependency to change, running the memo function again and causing an infinite loop. Instead of setting state in useMemo(), prefer deriving the value during render. (https://react.dev/reference/react/useState)',
139 suggestions: null,
140 }).withDetails({
141 kind: 'error',
142 loc: callee.loc,
143 message: 'Found setState() within useMemo()',
144 }),
145 );
146 } else if (unconditionalBlocks.has(block.id)) {
147 const enableUseKeyedState = fn.env.config.enableUseKeyedState;
148 if (enableUseKeyedState) {
149 errors.pushDiagnostic(
150 CompilerDiagnostic.create({
151 category: ErrorCategory.RenderSetState,
152 reason: 'Cannot call setState during render',
153 description:
154 'Calling setState during render may trigger an infinite loop.\n' +
155 '* To reset state when other state/props change, use `const [state, setState] = useKeyedState(initialState, key)` to reset `state` when `key` changes.\n' +
156 '* To derive data from other state/props, compute the derived data during render without using state',
157 suggestions: null,
158 }).withDetails({
159 kind: 'error',
160 loc: callee.loc,
161 message: 'Found setState() in render',
162 }),
163 );
164 } else {
165 errors.pushDiagnostic(
166 CompilerDiagnostic.create({
167 category: ErrorCategory.RenderSetState,
168 reason: 'Cannot call setState during render',
169 description:
170 'Calling setState during render may trigger an infinite loop.\n' +
171 '* To reset state when other state/props change, store the previous value in state and update conditionally: https://react.dev/reference/react/useState#storing-information-from-previous-renders\n' +
172 '* To derive data from other state/props, compute the derived data during render without using state',
173 suggestions: null,
174 }).withDetails({
175 kind: 'error',
176 loc: callee.loc,
177 message: 'Found setState() in render',
178 }),
179 );
180 }
181 }
182 }
183 break;
184 }
185 }
186 }
187 }
188
189 return errors;
190 }