Skip to content

Commit 3fd9bd8

Browse files
committed
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: #25370
1 parent 338e6a9 commit 3fd9bd8

File tree

2 files changed

+102
-6
lines changed

2 files changed

+102
-6
lines changed

packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js

+83-3
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,6 @@ const tests = {
257257
code: normalizeIndent`
258258
// Valid because they're not matching use[A-Z].
259259
fooState();
260-
use();
261260
_use();
262261
_useState();
263262
use_hook();
@@ -496,8 +495,6 @@ const tests = {
496495
},
497496
{
498497
code: normalizeIndent`
499-
Hook.use();
500-
Hook._use();
501498
Hook.useState();
502499
Hook._useState();
503500
Hook.use42();
@@ -1146,6 +1143,45 @@ if (__EXPERIMENTAL__) {
11461143
}
11471144
`,
11481145
},
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+
},
11491185
];
11501186
tests.invalid = [
11511187
...tests.invalid,
@@ -1220,6 +1256,50 @@ if (__EXPERIMENTAL__) {
12201256
`,
12211257
errors: [useEventError('onClick')],
12221258
},
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+
},
12231303
];
12241304
}
12251305

packages/eslint-plugin-react-hooks/src/RulesOfHooks.js

+19-3
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
*/
1717

1818
function isHookName(s) {
19+
if (__EXPERIMENTAL__) {
20+
return s === 'use' || /^use[A-Z0-9]/.test(s);
21+
}
1922
return /^use[A-Z0-9]/.test(s);
2023
}
2124

@@ -107,6 +110,13 @@ function isUseEventIdentifier(node) {
107110
return false;
108111
}
109112

113+
function isUseIdentifier(node) {
114+
if (__EXPERIMENTAL__) {
115+
return node.type === 'Identifier' && node.name === 'use';
116+
}
117+
return false;
118+
}
119+
110120
export default {
111121
meta: {
112122
type: 'problem',
@@ -458,7 +468,8 @@ export default {
458468

459469
for (const hook of reactHooks) {
460470
// 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)) {
462473
context.report({
463474
node: hook,
464475
message:
@@ -479,7 +490,11 @@ export default {
479490
// path segments.
480491
//
481492
// 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+
) {
483498
const message =
484499
`React Hook "${context.getSource(hook)}" is called ` +
485500
'conditionally. React Hooks must be called in the exact ' +
@@ -525,7 +540,8 @@ export default {
525540
// anonymous function expressions. Hopefully this is clarifying
526541
// enough in the common case that the incorrect message in
527542
// uncommon cases doesn't matter.
528-
if (isSomewhereInsideComponentOrHook) {
543+
// `use(...)` can be called in callbacks.
544+
if (isSomewhereInsideComponentOrHook && !isUseIdentifier(hook)) {
529545
const message =
530546
`React Hook "${context.getSource(hook)}" cannot be called ` +
531547
'inside a callback. React Hooks must be called in a ' +

0 commit comments

Comments
 (0)