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` code: normalizeIndent`
// Valid because they're not matching use[A-Z]. // Valid because they're not matching use[A-Z].
fooState(); fooState();
use();
_use(); _use();
_useState(); _useState();
use_hook(); use_hook();
@ -496,8 +495,6 @@ const tests = {
}, },
{ {
code: normalizeIndent` code: normalizeIndent`
Hook.use();
Hook._use();
Hook.useState(); Hook.useState();
Hook._useState(); Hook._useState();
Hook.use42(); 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 = [
...tests.invalid, ...tests.invalid,
@ -1220,6 +1256,50 @@ if (__EXPERIMENTAL__) {
`, `,
errors: [useEventError('onClick')], 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) { function isHookName(s) {
if (__EXPERIMENTAL__) {
return s === 'use' || /^use[A-Z0-9]/.test(s);
}
return /^use[A-Z0-9]/.test(s); return /^use[A-Z0-9]/.test(s);
} }
@ -107,6 +110,13 @@ function isUseEventIdentifier(node) {
return false; return false;
} }
function isUseIdentifier(node) {
if (__EXPERIMENTAL__) {
return node.type === 'Identifier' && node.name === 'use';
}
return false;
}
export default { export default {
meta: { meta: {
type: 'problem', type: 'problem',
@ -458,7 +468,8 @@ export default {
for (const hook of reactHooks) { for (const hook of reactHooks) {
// Report an error if a hook may be called more then once. // 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({ context.report({
node: hook, node: hook,
message: message:
@ -479,7 +490,11 @@ export default {
// path segments. // path segments.
// //
// Special case when we think there might be an early return. // 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 = const message =
`React Hook "${context.getSource(hook)}" is called ` + `React Hook "${context.getSource(hook)}" is called ` +
'conditionally. React Hooks must be called in the exact ' + 'conditionally. React Hooks must be called in the exact ' +
@ -525,7 +540,8 @@ export default {
// anonymous function expressions. Hopefully this is clarifying // anonymous function expressions. Hopefully this is clarifying
// enough in the common case that the incorrect message in // enough in the common case that the incorrect message in
// uncommon cases doesn't matter. // uncommon cases doesn't matter.
if (isSomewhereInsideComponentOrHook) { // `use(...)` can be called in callbacks.
if (isSomewhereInsideComponentOrHook && !isUseIdentifier(hook)) {
const message = const message =
`React Hook "${context.getSource(hook)}" cannot be called ` + `React Hook "${context.getSource(hook)}" cannot be called ` +
'inside a callback. React Hooks must be called in a ' + 'inside a callback. React Hooks must be called in a ' +