diff --git a/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js b/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js
index ef8e3c7ee6781..839c9fbb34302 100644
--- a/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js
+++ b/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js
@@ -257,7 +257,6 @@ const tests = {
code: normalizeIndent`
// Valid because they're not matching use[A-Z].
fooState();
- use();
_use();
_useState();
use_hook();
@@ -496,8 +495,6 @@ const tests = {
},
{
code: normalizeIndent`
- Hook.use();
- Hook._use();
Hook.useState();
Hook._useState();
Hook.use42();
@@ -1146,6 +1143,45 @@ if (__EXPERIMENTAL__) {
}
`,
},
+ {
+ code: normalizeIndent`
+ function App() {
+ const text = use(Promise.resolve('A'));
+ return
+ }
+ `,
+ },
+ {
+ code: normalizeIndent`
+ function App() {
+ if (shouldShowText) {
+ const text = use(query);
+ return
+ }
+ return
+ }
+ `,
+ },
+ {
+ code: normalizeIndent`
+ function App() {
+ let data = [];
+ for (const query of queries) {
+ const text = use(item);
+ data.push(text);
+ }
+ return
+ }
+ `,
+ },
+ {
+ code: normalizeIndent`
+ function App() {
+ const data = someCallback((x) => use(x));
+ return
+ }
+ `,
+ },
];
tests.invalid = [
...tests.invalid,
@@ -1220,6 +1256,50 @@ if (__EXPERIMENTAL__) {
`,
errors: [useEventError('onClick')],
},
+ {
+ code: normalizeIndent`
+ Hook.use();
+ Hook._use();
+ Hook.useState();
+ Hook._useState();
+ Hook.use42();
+ Hook.useHook();
+ Hook.use_hook();
+ `,
+ errors: [
+ topLevelError('Hook.use'),
+ topLevelError('Hook.useState'),
+ topLevelError('Hook.use42'),
+ topLevelError('Hook.useHook'),
+ ],
+ },
+ {
+ code: normalizeIndent`
+ function notAComponent() {
+ use(promise);
+ }
+ `,
+ errors: [functionError('use', 'notAComponent')],
+ },
+ {
+ code: normalizeIndent`
+ const text = use(promise);
+ function App() {
+ return
+ }
+ `,
+ errors: [topLevelError('use')],
+ },
+ {
+ code: normalizeIndent`
+ class C {
+ m() {
+ use(promise);
+ }
+ }
+ `,
+ errors: [classError('use')],
+ },
];
}
diff --git a/packages/eslint-plugin-react-hooks/src/RulesOfHooks.js b/packages/eslint-plugin-react-hooks/src/RulesOfHooks.js
index 2164d63aac47b..ca3a6167b36c4 100644
--- a/packages/eslint-plugin-react-hooks/src/RulesOfHooks.js
+++ b/packages/eslint-plugin-react-hooks/src/RulesOfHooks.js
@@ -16,6 +16,9 @@
*/
function isHookName(s) {
+ if (__EXPERIMENTAL__) {
+ return s === 'use' || /^use[A-Z0-9]/.test(s);
+ }
return /^use[A-Z0-9]/.test(s);
}
@@ -107,6 +110,13 @@ function isUseEventIdentifier(node) {
return false;
}
+function isUseIdentifier(node) {
+ if (__EXPERIMENTAL__) {
+ return node.type === 'Identifier' && node.name === 'use';
+ }
+ return false;
+}
+
export default {
meta: {
type: 'problem',
@@ -458,7 +468,8 @@ export default {
for (const hook of reactHooks) {
// Report an error if a hook may be called more then once.
- if (cycled) {
+ // `use(...)` can be called in loops.
+ if (cycled && !isUseIdentifier(hook)) {
context.report({
node: hook,
message:
@@ -479,7 +490,11 @@ export default {
// path segments.
//
// Special case when we think there might be an early return.
- if (!cycled && pathsFromStartToEnd !== allPathsFromStartToEnd) {
+ if (
+ !cycled &&
+ pathsFromStartToEnd !== allPathsFromStartToEnd &&
+ !isUseIdentifier(hook) // `use(...)` can be called conditionally.
+ ) {
const message =
`React Hook "${context.getSource(hook)}" is called ` +
'conditionally. React Hooks must be called in the exact ' +
@@ -525,7 +540,8 @@ export default {
// anonymous function expressions. Hopefully this is clarifying
// enough in the common case that the incorrect message in
// uncommon cases doesn't matter.
- if (isSomewhereInsideComponentOrHook) {
+ // `use(...)` can be called in callbacks.
+ if (isSomewhereInsideComponentOrHook && !isUseIdentifier(hook)) {
const message =
`React Hook "${context.getSource(hook)}" cannot be called ` +
'inside a callback. React Hooks must be called in a ' +