main
ts 229 lines 7.02 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 {CompilerError, SourceLocation} from '..';
9 import {CompilerErrorDetail, ErrorCategory} from '../CompilerError';
10 import {
11 ArrayExpression,
12 BlockId,
13 FunctionExpression,
14 HIRFunction,
15 IdentifierId,
16 isSetStateType,
17 isUseEffectHookType,
18 } from '../HIR';
19 import {
20 eachInstructionValueOperand,
21 eachTerminalOperand,
22 } from '../HIR/visitors';
23 import {Environment} from '../HIR/Environment';
24
25 /**
26 * Validates that useEffect is not used for derived computations which could/should
27 * be performed in render.
28 *
29 * See https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state
30 *
31 * Example:
32 *
33 * ```
34 * // 🔴 Avoid: redundant state and unnecessary Effect
35 * const [fullName, setFullName] = useState('');
36 * useEffect(() => {
37 * setFullName(firstName + ' ' + lastName);
38 * }, [firstName, lastName]);
39 * ```
40 *
41 * Instead use:
42 *
43 * ```
44 * // ✅ Good: calculated during rendering
45 * const fullName = firstName + ' ' + lastName;
46 * ```
47 */
48 export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
49 const candidateDependencies: Map<IdentifierId, ArrayExpression> = new Map();
50 const functions: Map<IdentifierId, FunctionExpression> = new Map();
51 const locals: Map<IdentifierId, IdentifierId> = new Map();
52
53 for (const block of fn.body.blocks.values()) {
54 for (const instr of block.instructions) {
55 const {lvalue, value} = instr;
56 if (value.kind === 'LoadLocal') {
57 locals.set(lvalue.identifier.id, value.place.identifier.id);
58 } else if (value.kind === 'ArrayExpression') {
59 candidateDependencies.set(lvalue.identifier.id, value);
60 } else if (value.kind === 'FunctionExpression') {
61 functions.set(lvalue.identifier.id, value);
62 } else if (
63 value.kind === 'CallExpression' ||
64 value.kind === 'MethodCall'
65 ) {
66 const callee =
67 value.kind === 'CallExpression' ? value.callee : value.property;
68 if (
69 isUseEffectHookType(callee.identifier) &&
70 value.args.length === 2 &&
71 value.args[0].kind === 'Identifier' &&
72 value.args[1].kind === 'Identifier'
73 ) {
74 const effectFunction = functions.get(value.args[0].identifier.id);
75 const deps = candidateDependencies.get(value.args[1].identifier.id);
76 if (
77 effectFunction != null &&
78 deps != null &&
79 deps.elements.length !== 0 &&
80 deps.elements.every(element => element.kind === 'Identifier')
81 ) {
82 const dependencies: Array<IdentifierId> = deps.elements.map(dep => {
83 CompilerError.invariant(dep.kind === 'Identifier', {
84 reason: `Dependency is checked as a place above`,
85 loc: value.loc,
86 });
87 return locals.get(dep.identifier.id) ?? dep.identifier.id;
88 });
89 validateEffect(
90 effectFunction.loweredFunc.func,
91 dependencies,
92 fn.env,
93 );
94 }
95 }
96 }
97 }
98 }
99 }
100
101 function validateEffect(
102 effectFunction: HIRFunction,
103 effectDeps: Array<IdentifierId>,
104 env: Environment,
105 ): void {
106 for (const operand of effectFunction.context) {
107 if (isSetStateType(operand.identifier)) {
108 continue;
109 } else if (effectDeps.find(dep => dep === operand.identifier.id) != null) {
110 continue;
111 } else {
112 // Captured something other than the effect dep or setState
113 return;
114 }
115 }
116 for (const dep of effectDeps) {
117 if (
118 effectFunction.context.find(operand => operand.identifier.id === dep) ==
119 null
120 ) {
121 // effect dep wasn't actually used in the function
122 return;
123 }
124 }
125
126 const seenBlocks: Set<BlockId> = new Set();
127 const values: Map<IdentifierId, Array<IdentifierId>> = new Map();
128 for (const dep of effectDeps) {
129 values.set(dep, [dep]);
130 }
131
132 const setStateLocations: Array<SourceLocation> = [];
133 for (const block of effectFunction.body.blocks.values()) {
134 for (const pred of block.preds) {
135 if (!seenBlocks.has(pred)) {
136 // skip if block has a back edge
137 return;
138 }
139 }
140 for (const phi of block.phis) {
141 const aggregateDeps: Set<IdentifierId> = new Set();
142 for (const operand of phi.operands.values()) {
143 const deps = values.get(operand.identifier.id);
144 if (deps != null) {
145 for (const dep of deps) {
146 aggregateDeps.add(dep);
147 }
148 }
149 }
150 if (aggregateDeps.size !== 0) {
151 values.set(phi.place.identifier.id, Array.from(aggregateDeps));
152 }
153 }
154 for (const instr of block.instructions) {
155 switch (instr.value.kind) {
156 case 'Primitive':
157 case 'JSXText':
158 case 'LoadGlobal': {
159 break;
160 }
161 case 'LoadLocal': {
162 const deps = values.get(instr.value.place.identifier.id);
163 if (deps != null) {
164 values.set(instr.lvalue.identifier.id, deps);
165 }
166 break;
167 }
168 case 'ComputedLoad':
169 case 'PropertyLoad':
170 case 'BinaryExpression':
171 case 'TemplateLiteral':
172 case 'CallExpression':
173 case 'MethodCall': {
174 const aggregateDeps: Set<IdentifierId> = new Set();
175 for (const operand of eachInstructionValueOperand(instr.value)) {
176 const deps = values.get(operand.identifier.id);
177 if (deps != null) {
178 for (const dep of deps) {
179 aggregateDeps.add(dep);
180 }
181 }
182 }
183 if (aggregateDeps.size !== 0) {
184 values.set(instr.lvalue.identifier.id, Array.from(aggregateDeps));
185 }
186
187 if (
188 instr.value.kind === 'CallExpression' &&
189 isSetStateType(instr.value.callee.identifier) &&
190 instr.value.args.length === 1 &&
191 instr.value.args[0].kind === 'Identifier'
192 ) {
193 const deps = values.get(instr.value.args[0].identifier.id);
194 if (deps != null && new Set(deps).size === effectDeps.length) {
195 setStateLocations.push(instr.value.callee.loc);
196 } else {
197 // doesn't depend on any deps
198 return;
199 }
200 }
201 break;
202 }
203 default: {
204 return;
205 }
206 }
207 }
208 for (const operand of eachTerminalOperand(block.terminal)) {
209 if (values.has(operand.identifier.id)) {
210 //
211 return;
212 }
213 }
214 seenBlocks.add(block.id);
215 }
216
217 for (const loc of setStateLocations) {
218 env.recordError(
219 new CompilerErrorDetail({
220 category: ErrorCategory.EffectDerivationsOfState,
221 reason:
222 'Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)',
223 description: null,
224 loc,
225 suggestions: null,
226 }),
227 );
228 }
229 }