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: 7ea5beceaf
Pull Request resolved: https://github.com/facebook/react/pull/25370
This commit is contained in:
Lauren Tan 2022-10-04 11:52:21 -04:00
parent 338e6a967c
commit 3fd9bd8e74
2 changed files with 102 additions and 6 deletions

View File

@ -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 <Text text={text} />
}
`,
},
{
code: normalizeIndent`
function App() {
if (shouldShowText) {
const text = use(query);
return <Text text={text} />
}
return <Text text={shouldFetchBackupText ? use(backupQuery) : "Nothing to see here"} />
}
`,
},
{
code: normalizeIndent`
function App() {
let data = [];
for (const query of queries) {
const text = use(item);
data.push(text);
}
return <Child data={data} />
}
`,
},
{
code: normalizeIndent`
function App() {
const data = someCallback((x) => use(x));
return <Child data={data} />
}
`,
},
];
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 <Text text={text} />
}
`,
errors: [topLevelError('use')],
},
{
code: normalizeIndent`
class C {
m() {
use(promise);
}
}
`,
errors: [classError('use')],
},
];
}

View File

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