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 ' +