Add RulesOfHooks support for `use`
Usage of the new `use` hook needs to conform to the rules of hooks, with the one exception that it can be called conditionally. ghstack-source-id: 7ea5beceaf2c080f2c48821b5117bdd0c1194836 Pull Request resolved: https://github.com/facebook/react/pull/25370
Lauren Tan committed
Oct 4, 2022 at 11:52 UTC
3fd9bd8e748231f2efb3624ee29f55633c0f3e05
2 files changed
+102
-6
packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js
+83
-3
@@ -257,7 +257,6 @@ const tests = {
257
code: normalizeIndent`
258
// Valid because they're not matching use[A-Z].
259
fooState();
260
- use();
260
_use();
261
_useState();
262
use_hook();
@@ -496,8 +495,6 @@ const tests = {
495
},
496
{
497
code: normalizeIndent`
499
- Hook.use();
500
- Hook._use();
498
Hook.useState();
499
Hook._useState();
500
Hook.use42();
@@ -1146,6 +1143,45 @@ if (__EXPERIMENTAL__) {
1143
}
1144
`,
1145
},
1146
+ {
1147
+ code: normalizeIndent`
1148
+ function App() {
1149
+ const text = use(Promise.resolve('A'));
1150
+ return <Text text={text} />
1151
+ }
1152
+ `,
1153
+ },
1154
+ {
1155
+ code: normalizeIndent`
1156
+ function App() {
1157
+ if (shouldShowText) {
1158
+ const text = use(query);
1159
+ return <Text text={text} />
1160
+ }
1161
+ return <Text text={shouldFetchBackupText ? use(backupQuery) : "Nothing to see here"} />
1162
+ }
1163
+ `,
1164
+ },
1165
+ {
1166
+ code: normalizeIndent`
1167
+ function App() {
1168
+ let data = [];
1169
+ for (const query of queries) {
1170
+ const text = use(item);
1171
+ data.push(text);
1172
+ }
1173
+ return <Child data={data} />
1174
+ }
1175
+ `,
1176
+ },
1177
+ {
1178
+ code: normalizeIndent`
1179
+ function App() {
1180
+ const data = someCallback((x) => use(x));
1181
+ return <Child data={data} />
1182
+ }
1183
+ `,
1184
+ },
1185
];
1186
tests.invalid = [
1187
...tests.invalid,
@@ -1220,6 +1256,50 @@ if (__EXPERIMENTAL__) {
1256
`,
1257
errors: [useEventError('onClick')],
1258
},
1259
+ {
1260
+ code: normalizeIndent`
1261
+ Hook.use();
1262
+ Hook._use();
1263
+ Hook.useState();
1264
+ Hook._useState();
1265
+ Hook.use42();
1266
+ Hook.useHook();
1267
+ Hook.use_hook();
1268
+ `,
1269
+ errors: [
1270
+ topLevelError('Hook.use'),
1271
+ topLevelError('Hook.useState'),
1272
+ topLevelError('Hook.use42'),
1273
+ topLevelError('Hook.useHook'),
1274
+ ],
1275
+ },
1276
+ {
1277
+ code: normalizeIndent`
1278
+ function notAComponent() {
1279
+ use(promise);
1280
+ }
1281
+ `,
1282
+ errors: [functionError('use', 'notAComponent')],
1283
+ },
1284
+ {
1285
+ code: normalizeIndent`
1286
+ const text = use(promise);
1287
+ function App() {
1288
+ return <Text text={text} />
1289
+ }
1290
+ `,
1291
+ errors: [topLevelError('use')],
1292
+ },
1293
+ {
1294
+ code: normalizeIndent`
1295
+ class C {
1296
+ m() {
1297
+ use(promise);
1298
+ }
1299
+ }
1300
+ `,
1301
+ errors: [classError('use')],
1302
+ },
1303
];
1304
}
1305
packages/eslint-plugin-react-hooks/src/RulesOfHooks.js
+19
-3
@@ -16,6 +16,9 @@
16
*/
17
18
function isHookName(s) {
19
+ if (__EXPERIMENTAL__) {
20
+ return s === 'use' || /^use[A-Z0-9]/.test(s);
21
+ }
22
return /^use[A-Z0-9]/.test(s);
23
}
24
@@ -107,6 +110,13 @@ function isUseEventIdentifier(node) {
110
return false;
111
}
112
113
+function isUseIdentifier(node) {
114
+ if (__EXPERIMENTAL__) {
115
+ return node.type === 'Identifier' && node.name === 'use';
116
+ }
117
+ return false;
118
+}
119
+
120
export default {
121
meta: {
122
type: 'problem',
@@ -458,7 +468,8 @@ export default {
468
469
for (const hook of reactHooks) {
470
// Report an error if a hook may be called more then once.
461
- if (cycled) {
471
+ // `use(...)` can be called in loops.
472
+ if (cycled && !isUseIdentifier(hook)) {
473
context.report({
474
node: hook,
475
message:
@@ -479,7 +490,11 @@ export default {
490
// path segments.
491
//
492
// Special case when we think there might be an early return.
482
- if (!cycled && pathsFromStartToEnd !== allPathsFromStartToEnd) {
493
+ if (
494
+ !cycled &&
495
+ pathsFromStartToEnd !== allPathsFromStartToEnd &&
496
+ !isUseIdentifier(hook) // `use(...)` can be called conditionally.
497
+ ) {
498
const message =
499
`React Hook "${context.getSource(hook)}" is called ` +
500
'conditionally. React Hooks must be called in the exact ' +
@@ -525,7 +540,8 @@ export default {
540
// anonymous function expressions. Hopefully this is clarifying
541
// enough in the common case that the incorrect message in
542
// uncommon cases doesn't matter.
528
- if (isSomewhereInsideComponentOrHook) {
543
+ // `use(...)` can be called in callbacks.
544
+ if (isSomewhereInsideComponentOrHook && !isUseIdentifier(hook)) {
545
const message =
546
`React Hook "${context.getSource(hook)}" cannot be called ` +
547
'inside a callback. React Hooks must be called in a ' +