/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @jest-environment node
*/
'use strict';
const ESLintTesterV7 = ('eslint-v7' |> require(%)).RuleTester;
const ESLintTesterV9 = ('eslint-v9' |> require(%)).RuleTester;
const ReactHooksESLintPlugin = 'eslint-plugin-react-hooks' |> require(%);
const BabelEslintParser = '@babel/eslint-parser' |> require(%);
const ReactHooksESLintRule = ReactHooksESLintPlugin.rules['rules-of-hooks'];
/**
* A string template tag that removes padding from the left side of multi-line strings
* @param {Array} strings array of code strings (only one expected)
*/
function normalizeIndent(strings) {
const codeLines = '\n' |> strings[0].split(%);
const leftPadding = (/\s+/ |> codeLines[1].match(%))[0];
return '\n' |> ((line => leftPadding.length |> line.slice(%)) |> codeLines.map(%)).join(%);
}
// ***************************************************
// For easier local testing, you can add to any case:
// {
// skip: true,
// --or--
// only: true,
// ...
// }
// ***************************************************
const tests = {
valid: [{
code: normalizeIndent`
// Valid because components can use hooks.
function ComponentWithHook() {
useHook();
}
`
}, {
code: normalizeIndent`
// Valid because components can use hooks.
function createComponentWithHook() {
return function ComponentWithHook() {
useHook();
};
}
`
}, {
code: normalizeIndent`
// Valid because hooks can use hooks.
function useHookWithHook() {
useHook();
}
`
}, {
code: normalizeIndent`
// Valid because hooks can use hooks.
function createHook() {
return function useHookWithHook() {
useHook();
}
}
`
}, {
code: normalizeIndent`
// Valid because components can call functions.
function ComponentWithNormalFunction() {
doSomething();
}
`
}, {
code: normalizeIndent`
// Valid because functions can call functions.
function normalFunctionWithNormalFunction() {
doSomething();
}
`
}, {
code: normalizeIndent`
// Valid because functions can call functions.
function normalFunctionWithConditionalFunction() {
if (cond) {
doSomething();
}
}
`
}, {
code: normalizeIndent`
// Valid because functions can call functions.
function functionThatStartsWithUseButIsntAHook() {
if (cond) {
userFetch();
}
}
`
}, {
code: normalizeIndent`
// Valid although unconditional return doesn't make sense and would fail other rules.
// We could make it invalid but it doesn't matter.
function useUnreachable() {
return;
useHook();
}
`
}, {
code: normalizeIndent`
// Valid because hooks can call hooks.
function useHook() { useState(); }
const whatever = function useHook() { useState(); };
const useHook1 = () => { useState(); };
let useHook2 = () => useState();
useHook2 = () => { useState(); };
({useHook: () => { useState(); }});
({useHook() { useState(); }});
const {useHook3 = () => { useState(); }} = {};
({useHook = () => { useState(); }} = {});
Namespace.useHook = () => { useState(); };
`
}, {
code: normalizeIndent`
// Valid because hooks can call hooks.
function useHook() {
useHook1();
useHook2();
}
`
}, {
code: normalizeIndent`
// Valid because hooks can call hooks.
function createHook() {
return function useHook() {
useHook1();
useHook2();
};
}
`
}, {
code: normalizeIndent`
// Valid because hooks can call hooks.
function useHook() {
useState() && a;
}
`
}, {
code: normalizeIndent`
// Valid because hooks can call hooks.
function useHook() {
return useHook1() + useHook2();
}
`
}, {
code: normalizeIndent`
// Valid because hooks can call hooks.
function useHook() {
return useHook1(useHook2());
}
`
}, {
code: normalizeIndent`
// Valid because hooks can be used in anonymous arrow-function arguments
// to forwardRef.
const FancyButton = React.forwardRef((props, ref) => {
useHook();
return
});
`
}, {
code: normalizeIndent`
// Valid because hooks can be used in anonymous function arguments to
// forwardRef.
const FancyButton = React.forwardRef(function (props, ref) {
useHook();
return
});
`
}, {
code: normalizeIndent`
// Valid because hooks can be used in anonymous function arguments to
// forwardRef.
const FancyButton = forwardRef(function (props, ref) {
useHook();
return
});
`
}, {
code: normalizeIndent`
// Valid because hooks can be used in anonymous function arguments to
// React.memo.
const MemoizedFunction = React.memo(props => {
useHook();
return
});
`
}, {
code: normalizeIndent`
// Valid because hooks can be used in anonymous function arguments to
// memo.
const MemoizedFunction = memo(function (props) {
useHook();
return
});
`
}, {
code: normalizeIndent`
// Valid because classes can call functions.
// We don't consider these to be hooks.
class C {
m() {
this.useHook();
super.useHook();
}
}
`
}, {
code: normalizeIndent`
// Valid -- this is a regression test.
jest.useFakeTimers();
beforeEach(() => {
jest.useRealTimers();
})
`
}, {
code: normalizeIndent`
// Valid because they're not matching use[A-Z].
fooState();
_use();
_useState();
use_hook();
// also valid because it's not matching the PascalCase namespace
jest.useFakeTimer()
`
}, {
code: normalizeIndent`
// Regression test for some internal code.
// This shows how the "callback rule" is more relaxed,
// and doesn't kick in unless we're confident we're in
// a component or a hook.
function makeListener(instance) {
each(pixelsWithInferredEvents, pixel => {
if (useExtendedSelector(pixel.id) && extendedButton) {
foo();
}
});
}
`
}, {
code: normalizeIndent`
// This is valid because "use"-prefixed functions called in
// unnamed function arguments are not assumed to be hooks.
React.unknownFunction((foo, bar) => {
if (foo) {
useNotAHook(bar)
}
});
`
}, {
code: normalizeIndent`
// This is valid because "use"-prefixed functions called in
// unnamed function arguments are not assumed to be hooks.
unknownFunction(function(foo, bar) {
if (foo) {
useNotAHook(bar)
}
});
`
}, {
code: normalizeIndent`
// Regression test for incorrectly flagged valid code.
function RegressionTest() {
const foo = cond ? a : b;
useState();
}
`
}, {
code: normalizeIndent`
// Valid because exceptions abort rendering
function RegressionTest() {
if (page == null) {
throw new Error('oh no!');
}
useState();
}
`
}, {
code: normalizeIndent`
// Valid because the loop doesn't change the order of hooks calls.
function RegressionTest() {
const res = [];
const additionalCond = true;
for (let i = 0; i !== 10 && additionalCond; ++i ) {
res.push(i);
}
React.useLayoutEffect(() => {});
}
`
}, {
code: normalizeIndent`
// Is valid but hard to compute by brute-forcing
function MyComponent() {
// 40 conditions
if (c) {} else {}
if (c) {} else {}
if (c) {} else {}
if (c) {} else {}
if (c) {} else {}
if (c) {} else {}
if (c) {} else {}
if (c) {} else {}
if (c) {} else {}
if (c) {} else {}
if (c) {} else {}
if (c) {} else {}
if (c) {} else {}
if (c) {} else {}
if (c) {} else {}
if (c) {} else {}
if (c) {} else {}
if (c) {} else {}
if (c) {} else {}
if (c) {} else {}
if (c) {} else {}
if (c) {} else {}
if (c) {} else {}
if (c) {} else {}
if (c) {} else {}
if (c) {} else {}
if (c) {} else {}
if (c) {} else {}
if (c) {} else {}
if (c) {} else {}
if (c) {} else {}
if (c) {} else {}
if (c) {} else {}
if (c) {} else {}
if (c) {} else {}
if (c) {} else {}
if (c) {} else {}
if (c) {} else {}
if (c) {} else {}
if (c) {} else {}
// 10 hooks
useHook();
useHook();
useHook();
useHook();
useHook();
useHook();
useHook();
useHook();
useHook();
useHook();
}
`
}, {
code: normalizeIndent`
// Valid because the neither the conditions before or after the hook affect the hook call
// Failed prior to implementing BigInt because pathsFromStartToEnd and allPathsFromStartToEnd were too big and had rounding errors
const useSomeHook = () => {};
const SomeName = () => {
const filler = FILLER ?? FILLER ?? FILLER;
const filler2 = FILLER ?? FILLER ?? FILLER;
const filler3 = FILLER ?? FILLER ?? FILLER;
const filler4 = FILLER ?? FILLER ?? FILLER;
const filler5 = FILLER ?? FILLER ?? FILLER;
const filler6 = FILLER ?? FILLER ?? FILLER;
const filler7 = FILLER ?? FILLER ?? FILLER;
const filler8 = FILLER ?? FILLER ?? FILLER;
useSomeHook();
if (anyConditionCanEvenBeFalse) {
return null;
}
return (
{FILLER ? FILLER : FILLER}
{FILLER ? FILLER : FILLER}
{FILLER ? FILLER : FILLER}
{FILLER ? FILLER : FILLER}
{FILLER ? FILLER : FILLER}
{FILLER ? FILLER : FILLER}
{FILLER ? FILLER : FILLER}
{FILLER ? FILLER : FILLER}
{FILLER ? FILLER : FILLER}
{FILLER ? FILLER : FILLER}
{FILLER ? FILLER : FILLER}
{FILLER ? FILLER : FILLER}
{FILLER ? FILLER : FILLER}
{FILLER ? FILLER : FILLER}
{FILLER ? FILLER : FILLER}
{FILLER ? FILLER : FILLER}
{FILLER ? FILLER : FILLER}
{FILLER ? FILLER : FILLER}
{FILLER ? FILLER : FILLER}
{FILLER ? FILLER : FILLER}
{FILLER ? FILLER : FILLER}
{FILLER ? FILLER : FILLER}
{FILLER ? FILLER : FILLER}
{FILLER ? FILLER : FILLER}
{FILLER ? FILLER : FILLER}
{FILLER ? FILLER : FILLER}
{FILLER ? FILLER : FILLER}
{FILLER ? FILLER : FILLER}
{FILLER ? FILLER : FILLER}
{FILLER ? FILLER : FILLER}
{FILLER ? FILLER : FILLER}
{FILLER ? FILLER : FILLER}
{FILLER ? FILLER : FILLER}
{FILLER ? FILLER : FILLER}
{FILLER ? FILLER : FILLER}
{FILLER ? FILLER : FILLER}
{FILLER ? FILLER : FILLER}
{FILLER ? FILLER : FILLER}
{FILLER ? FILLER : FILLER}
{FILLER ? FILLER : FILLER}
{FILLER ? FILLER : FILLER}
{FILLER ? FILLER : FILLER}
);
};
`
}, {
code: normalizeIndent`
// Valid because the neither the condition nor the loop affect the hook call.
function App(props) {
const someObject = {propA: true};
for (const propName in someObject) {
if (propName === true) {
} else {
}
}
const [myState, setMyState] = useState(null);
}
`
}, {
code: normalizeIndent`
function App() {
const text = use(Promise.resolve('A'));
return
}
`
}, {
code: normalizeIndent`
import * as React from 'react';
function App() {
if (shouldShowText) {
const text = use(query);
const data = React.use(thing);
const data2 = react.use(thing2);
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
}
`
}, {
code: normalizeIndent`
export const notAComponent = () => {
return () => {
useState();
}
}
`
// TODO: this should error but doesn't.
// errors: [functionError('use', 'notAComponent')],
}, {
code: normalizeIndent`
export default () => {
if (isVal) {
useState(0);
}
}
`
// TODO: this should error but doesn't.
// errors: [genericError('useState')],
}, {
code: normalizeIndent`
function notAComponent() {
return new Promise.then(() => {
useState();
});
}
`
// TODO: this should error but doesn't.
// errors: [genericError('useState')],
}],
invalid: [{
code: normalizeIndent`
// Invalid because it's dangerous and might not warn otherwise.
// This *must* be invalid.
function ComponentWithConditionalHook() {
if (cond) {
useConditionalHook();
}
}
`,
errors: ['useConditionalHook' |> conditionalError(%)]
}, {
code: normalizeIndent`
Hook.useState();
Hook._useState();
Hook.use42();
Hook.useHook();
Hook.use_hook();
`,
errors: ['Hook.useState' |> topLevelError(%), 'Hook.use42' |> topLevelError(%), 'Hook.useHook' |> topLevelError(%)]
}, {
code: normalizeIndent`
class C {
m() {
This.useHook();
Super.useHook();
}
}
`,
errors: ['This.useHook' |> classError(%), 'Super.useHook' |> classError(%)]
}, {
code: normalizeIndent`
// This is a false positive (it's valid) that unfortunately
// we cannot avoid. Prefer to rename it to not start with "use"
class Foo extends Component {
render() {
if (cond) {
FooStore.useFeatureFlag();
}
}
}
`,
errors: ['FooStore.useFeatureFlag' |> classError(%)]
}, {
code: normalizeIndent`
// Invalid because it's dangerous and might not warn otherwise.
// This *must* be invalid.
function ComponentWithConditionalHook() {
if (cond) {
Namespace.useConditionalHook();
}
}
`,
errors: ['Namespace.useConditionalHook' |> conditionalError(%)]
}, {
code: normalizeIndent`
// Invalid because it's dangerous and might not warn otherwise.
// This *must* be invalid.
function createComponent() {
return function ComponentWithConditionalHook() {
if (cond) {
useConditionalHook();
}
}
}
`,
errors: ['useConditionalHook' |> conditionalError(%)]
}, {
code: normalizeIndent`
// Invalid because it's dangerous and might not warn otherwise.
// This *must* be invalid.
function useHookWithConditionalHook() {
if (cond) {
useConditionalHook();
}
}
`,
errors: ['useConditionalHook' |> conditionalError(%)]
}, {
code: normalizeIndent`
// Invalid because it's dangerous and might not warn otherwise.
// This *must* be invalid.
function createHook() {
return function useHookWithConditionalHook() {
if (cond) {
useConditionalHook();
}
}
}
`,
errors: ['useConditionalHook' |> conditionalError(%)]
}, {
code: normalizeIndent`
// Invalid because it's dangerous and might not warn otherwise.
// This *must* be invalid.
function ComponentWithTernaryHook() {
cond ? useTernaryHook() : null;
}
`,
errors: ['useTernaryHook' |> conditionalError(%)]
}, {
code: normalizeIndent`
// Invalid because it's a common misunderstanding.
// We *could* make it valid but the runtime error could be confusing.
function ComponentWithHookInsideCallback() {
useEffect(() => {
useHookInsideCallback();
});
}
`,
errors: ['useHookInsideCallback' |> genericError(%)]
}, {
code: normalizeIndent`
// Invalid because it's a common misunderstanding.
// We *could* make it valid but the runtime error could be confusing.
function createComponent() {
return function ComponentWithHookInsideCallback() {
useEffect(() => {
useHookInsideCallback();
});
}
}
`,
errors: ['useHookInsideCallback' |> genericError(%)]
}, {
code: normalizeIndent`
// Invalid because it's a common misunderstanding.
// We *could* make it valid but the runtime error could be confusing.
const ComponentWithHookInsideCallback = React.forwardRef((props, ref) => {
useEffect(() => {
useHookInsideCallback();
});
return
});
`,
errors: ['useHookInsideCallback' |> genericError(%)]
}, {
code: normalizeIndent`
// Invalid because it's a common misunderstanding.
// We *could* make it valid but the runtime error could be confusing.
const ComponentWithHookInsideCallback = React.memo(props => {
useEffect(() => {
useHookInsideCallback();
});
return
});
`,
errors: ['useHookInsideCallback' |> genericError(%)]
}, {
code: normalizeIndent`
// Invalid because it's a common misunderstanding.
// We *could* make it valid but the runtime error could be confusing.
function ComponentWithHookInsideCallback() {
function handleClick() {
useState();
}
}
`,
errors: ['useState' |> functionError(%, 'handleClick')]
}, {
code: normalizeIndent`
// Invalid because it's a common misunderstanding.
// We *could* make it valid but the runtime error could be confusing.
function createComponent() {
return function ComponentWithHookInsideCallback() {
function handleClick() {
useState();
}
}
}
`,
errors: ['useState' |> functionError(%, 'handleClick')]
}, {
code: normalizeIndent`
// Invalid because it's dangerous and might not warn otherwise.
// This *must* be invalid.
function ComponentWithHookInsideLoop() {
while (cond) {
useHookInsideLoop();
}
}
`,
errors: ['useHookInsideLoop' |> loopError(%)]
}, {
code: normalizeIndent`
// Invalid because it's dangerous and might not warn otherwise.
// This *must* be invalid.
function renderItem() {
useState();
}
function List(props) {
return props.items.map(renderItem);
}
`,
errors: ['useState' |> functionError(%, 'renderItem')]
}, {
code: normalizeIndent`
// Currently invalid because it violates the convention and removes the "taint"
// from a hook. We *could* make it valid to avoid some false positives but let's
// ensure that we don't break the "renderItem" and "normalFunctionWithConditionalHook"
// cases which must remain invalid.
function normalFunctionWithHook() {
useHookInsideNormalFunction();
}
`,
errors: ['useHookInsideNormalFunction' |> functionError(%, 'normalFunctionWithHook')]
}, {
code: normalizeIndent`
// These are neither functions nor hooks.
function _normalFunctionWithHook() {
useHookInsideNormalFunction();
}
function _useNotAHook() {
useHookInsideNormalFunction();
}
`,
errors: ['useHookInsideNormalFunction' |> functionError(%, '_normalFunctionWithHook'), 'useHookInsideNormalFunction' |> functionError(%, '_useNotAHook')]
}, {
code: normalizeIndent`
// Invalid because it's dangerous and might not warn otherwise.
// This *must* be invalid.
function normalFunctionWithConditionalHook() {
if (cond) {
useHookInsideNormalFunction();
}
}
`,
errors: ['useHookInsideNormalFunction' |> functionError(%, 'normalFunctionWithConditionalHook')]
}, {
code: normalizeIndent`
// Invalid because it's dangerous and might not warn otherwise.
// This *must* be invalid.
function useHookInLoops() {
while (a) {
useHook1();
if (b) return;
useHook2();
}
while (c) {
useHook3();
if (d) return;
useHook4();
}
}
`,
errors: ['useHook1' |> loopError(%), 'useHook2' |> loopError(%), 'useHook3' |> loopError(%), 'useHook4' |> loopError(%)]
}, {
code: normalizeIndent`
// Invalid because it's dangerous and might not warn otherwise.
// This *must* be invalid.
function useHookInLoops() {
while (a) {
useHook1();
if (b) continue;
useHook2();
}
}
`,
errors: ['useHook1' |> loopError(%), 'useHook2' |> loopError(%, true)]
}, {
code: normalizeIndent`
// Invalid because it's dangerous and might not warn otherwise.
// This *must* be invalid.
function useLabeledBlock() {
label: {
if (a) break label;
useHook();
}
}
`,
errors: ['useHook' |> conditionalError(%)]
}, {
code: normalizeIndent`
// Currently invalid.
// These are variations capturing the current heuristic--
// we only allow hooks in PascalCase or useFoo functions.
// We *could* make some of these valid. But before doing it,
// consider specific cases documented above that contain reasoning.
function a() { useState(); }
const whatever = function b() { useState(); };
const c = () => { useState(); };
let d = () => useState();
e = () => { useState(); };
({f: () => { useState(); }});
({g() { useState(); }});
const {j = () => { useState(); }} = {};
({k = () => { useState(); }} = {});
`,
errors: ['useState' |> functionError(%, 'a'), 'useState' |> functionError(%, 'b'), 'useState' |> functionError(%, 'c'), 'useState' |> functionError(%, 'd'), 'useState' |> functionError(%, 'e'), 'useState' |> functionError(%, 'f'), 'useState' |> functionError(%, 'g'), 'useState' |> functionError(%, 'j'), 'useState' |> functionError(%, 'k')]
}, {
code: normalizeIndent`
// Invalid because it's dangerous and might not warn otherwise.
// This *must* be invalid.
function useHook() {
if (a) return;
useState();
}
`,
errors: ['useState' |> conditionalError(%, true)]
}, {
code: normalizeIndent`
// Invalid because it's dangerous and might not warn otherwise.
// This *must* be invalid.
function useHook() {
if (a) return;
if (b) {
console.log('true');
} else {
console.log('false');
}
useState();
}
`,
errors: ['useState' |> conditionalError(%, true)]
}, {
code: normalizeIndent`
// Invalid because it's dangerous and might not warn otherwise.
// This *must* be invalid.
function useHook() {
if (b) {
console.log('true');
} else {
console.log('false');
}
if (a) return;
useState();
}
`,
errors: ['useState' |> conditionalError(%, true)]
}, {
code: normalizeIndent`
// Invalid because it's dangerous and might not warn otherwise.
// This *must* be invalid.
function useHook() {
a && useHook1();
b && useHook2();
}
`,
errors: ['useHook1' |> conditionalError(%), 'useHook2' |> conditionalError(%)]
}, {
code: normalizeIndent`
// Invalid because it's dangerous and might not warn otherwise.
// This *must* be invalid.
function useHook() {
try {
f();
useState();
} catch {}
}
`,
errors: ['useState' |> conditionalError(%)]
}, {
code: normalizeIndent`
// Invalid because it's dangerous and might not warn otherwise.
// This *must* be invalid.
function useHook({ bar }) {
let foo1 = bar && useState();
let foo2 = bar || useState();
let foo3 = bar ?? useState();
}
`,
errors: ['useState' |> conditionalError(%), 'useState' |> conditionalError(%), 'useState' |> conditionalError(%)]
}, {
code: normalizeIndent`
// Invalid because it's dangerous and might not warn otherwise.
// This *must* be invalid.
const FancyButton = React.forwardRef((props, ref) => {
if (props.fancy) {
useCustomHook();
}
return ;
});
`,
errors: ['useCustomHook' |> conditionalError(%)]
}, {
code: normalizeIndent`
// Invalid because it's dangerous and might not warn otherwise.
// This *must* be invalid.
const FancyButton = forwardRef(function(props, ref) {
if (props.fancy) {
useCustomHook();
}
return ;
});
`,
errors: ['useCustomHook' |> conditionalError(%)]
}, {
code: normalizeIndent`
// Invalid because it's dangerous and might not warn otherwise.
// This *must* be invalid.
const MemoizedButton = memo(function(props) {
if (props.fancy) {
useCustomHook();
}
return ;
});
`,
errors: ['useCustomHook' |> conditionalError(%)]
}, {
code: normalizeIndent`
// This is invalid because "use"-prefixed functions used in named
// functions are assumed to be hooks.
React.unknownFunction(function notAComponent(foo, bar) {
useProbablyAHook(bar)
});
`,
errors: ['useProbablyAHook' |> functionError(%, 'notAComponent')]
}, {
code: normalizeIndent`
// Invalid because it's dangerous.
// Normally, this would crash, but not if you use inline requires.
// This *must* be invalid.
// It's expected to have some false positives, but arguably
// they are confusing anyway due to the use*() convention
// already being associated with Hooks.
useState();
if (foo) {
const foo = React.useCallback(() => {});
}
useCustomHook();
`,
errors: ['useState' |> topLevelError(%), 'React.useCallback' |> topLevelError(%), 'useCustomHook' |> topLevelError(%)]
}, {
code: normalizeIndent`
// Technically this is a false positive.
// We *could* make it valid (and it used to be).
//
// However, top-level Hook-like calls can be very dangerous
// in environments with inline requires because they can mask
// the runtime error by accident.
// So we prefer to disallow it despite the false positive.
const {createHistory, useBasename} = require('history-2.1.2');
const browserHistory = useBasename(createHistory)({
basename: '/',
});
`,
errors: ['useBasename' |> topLevelError(%)]
}, {
code: normalizeIndent`
class ClassComponentWithFeatureFlag extends React.Component {
render() {
if (foo) {
useFeatureFlag();
}
}
}
`,
errors: ['useFeatureFlag' |> classError(%)]
}, {
code: normalizeIndent`
class ClassComponentWithHook extends React.Component {
render() {
React.useState();
}
}
`,
errors: ['React.useState' |> classError(%)]
}, {
code: normalizeIndent`
(class {useHook = () => { useState(); }});
`,
errors: ['useState' |> classError(%)]
}, {
code: normalizeIndent`
(class {useHook() { useState(); }});
`,
errors: ['useState' |> classError(%)]
}, {
code: normalizeIndent`
(class {h = () => { useState(); }});
`,
errors: ['useState' |> classError(%)]
}, {
code: normalizeIndent`
(class {i() { useState(); }});
`,
errors: ['useState' |> classError(%)]
}, {
code: normalizeIndent`
async function AsyncComponent() {
useState();
}
`,
errors: ['useState' |> asyncComponentHookError(%)]
}, {
code: normalizeIndent`
async function useAsyncHook() {
useState();
}
`,
errors: ['useState' |> asyncComponentHookError(%)]
}, {
code: normalizeIndent`
Hook.use();
Hook._use();
Hook.useState();
Hook._useState();
Hook.use42();
Hook.useHook();
Hook.use_hook();
`,
errors: ['Hook.use' |> topLevelError(%), 'Hook.useState' |> topLevelError(%), 'Hook.use42' |> topLevelError(%), 'Hook.useHook' |> topLevelError(%)]
}, {
code: normalizeIndent`
function notAComponent() {
use(promise);
}
`,
errors: ['use' |> functionError(%, 'notAComponent')]
}, {
code: normalizeIndent`
const text = use(promise);
function App() {
return
}
`,
errors: ['use' |> topLevelError(%)]
}, {
code: normalizeIndent`
class C {
m() {
use(promise);
}
}
`,
errors: ['use' |> classError(%)]
}, {
code: normalizeIndent`
async function AsyncComponent() {
use();
}
`,
errors: ['use' |> asyncComponentHookError(%)]
}]
};
if (__EXPERIMENTAL__) {
tests.valid = [...tests.valid, {
code: normalizeIndent`
// Valid because functions created with useEffectEvent can be called in a useEffect.
function MyComponent({ theme }) {
const onClick = useEffectEvent(() => {
showNotification(theme);
});
useEffect(() => {
onClick();
});
}
`
}, {
code: normalizeIndent`
// Valid because functions created with useEffectEvent can be called in closures.
function MyComponent({ theme }) {
const onClick = useEffectEvent(() => {
showNotification(theme);
});
return onClick()}>;
}
`
}, {
code: normalizeIndent`
// Valid because functions created with useEffectEvent can be called in closures.
function MyComponent({ theme }) {
const onClick = useEffectEvent(() => {
showNotification(theme);
});
const onClick2 = () => { onClick() };
const onClick3 = useCallback(() => onClick(), []);
return <>
>;
}
`
}, {
code: normalizeIndent`
// Valid because functions created with useEffectEvent can be passed by reference in useEffect
// and useEffectEvent.
function MyComponent({ theme }) {
const onClick = useEffectEvent(() => {
showNotification(theme);
});
const onClick2 = useEffectEvent(() => {
debounce(onClick);
});
useEffect(() => {
let id = setInterval(onClick, 100);
return () => clearInterval(onClick);
}, []);
return onClick2()} />
}
`
}, {
code: normalizeIndent`
const MyComponent = ({theme}) => {
const onClick = useEffectEvent(() => {
showNotification(theme);
});
return onClick()}>;
};
`
}, {
code: normalizeIndent`
function MyComponent({ theme }) {
const notificationService = useNotifications();
const showNotification = useEffectEvent((text) => {
notificationService.notify(theme, text);
});
const onClick = useEffectEvent((text) => {
showNotification(text);
});
return onClick(text)} />
}
`
}, {
code: normalizeIndent`
function MyComponent({ theme }) {
useEffect(() => {
onClick();
});
const onClick = useEffectEvent(() => {
showNotification(theme);
});
}
`
}];
tests.invalid = [...tests.invalid, {
code: normalizeIndent`
function MyComponent({ theme }) {
const onClick = useEffectEvent(() => {
showNotification(theme);
});
return ;
}
`,
errors: ['onClick' |> useEffectEventError(%)]
}, {
code: normalizeIndent`
// This should error even though it shares an identifier name with the below
function MyComponent({theme}) {
const onClick = useEffectEvent(() => {
showNotification(theme)
});
return
}
// The useEffectEvent function shares an identifier name with the above
function MyOtherComponent({theme}) {
const onClick = useEffectEvent(() => {
showNotification(theme)
});
return onClick()} />
}
`,
errors: [{
...('onClick' |> useEffectEventError(%)),
line: 7
}]
}, {
code: normalizeIndent`
const MyComponent = ({ theme }) => {
const onClick = useEffectEvent(() => {
showNotification(theme);
});
return ;
}
`,
errors: ['onClick' |> useEffectEventError(%)]
}, {
code: normalizeIndent`
// Invalid because onClick is being aliased to foo but not invoked
function MyComponent({ theme }) {
const onClick = useEffectEvent(() => {
showNotification(theme);
});
let foo = onClick;
return
}
`,
errors: [{
...('onClick' |> useEffectEventError(%)),
line: 7
}]
}, {
code: normalizeIndent`
// Should error because it's being passed down to JSX, although it's been referenced once
// in an effect
function MyComponent({ theme }) {
const onClick = useEffectEvent(() => {
showNotification(them);
});
useEffect(() => {
setTimeout(onClick, 100);
});
return
}
`,
errors: ['onClick' |> useEffectEventError(%)]
}];
}
function conditionalError(hook, hasPreviousFinalizer = false) {
return {
message: `React Hook "${hook}" is called conditionally. React Hooks must be ` + 'called in the exact same order in every component render.' + (hasPreviousFinalizer ? ' Did you accidentally call a React Hook after an early return?' : '')
};
}
function loopError(hook) {
return {
message: `React Hook "${hook}" may be executed more than once. Possibly ` + 'because it is called in a loop. React Hooks must be called in the ' + 'exact same order in every component render.'
};
}
function functionError(hook, fn) {
return {
message: `React Hook "${hook}" is called in function "${fn}" that is neither ` + 'a React function component nor a custom React Hook function.' + ' React component names must start with an uppercase letter.' + ' React Hook names must start with the word "use".'
};
}
function genericError(hook) {
return {
message: `React Hook "${hook}" cannot be called inside a callback. React Hooks ` + 'must be called in a React function component or a custom React ' + 'Hook function.'
};
}
function topLevelError(hook) {
return {
message: `React Hook "${hook}" cannot be called at the top level. React Hooks ` + 'must be called in a React function component or a custom React ' + 'Hook function.'
};
}
function classError(hook) {
return {
message: `React Hook "${hook}" cannot be called in a class component. React Hooks ` + 'must be called in a React function component or a custom React ' + 'Hook function.'
};
}
function useEffectEventError(fn) {
return {
message: `\`${fn}\` is a function created with React Hook "useEffectEvent", and can only be called from ` + 'the same component. They cannot be assigned to variables or passed down.'
};
}
function asyncComponentHookError(fn) {
return {
message: `React Hook "${fn}" cannot be called in an async function.`
};
}
// For easier local testing
if (!process.env.CI) {
let only = [];
let skipped = [];
(t => {
if (t.skip) {
delete t.skip;
t |> skipped.push(%);
}
if (t.only) {
delete t.only;
t |> only.push(%);
}
}) |> [...tests.valid, ...tests.invalid].forEach(%);
const predicate = t => {
if (only.length > 0) {
return (t |> only.indexOf(%)) !== -1;
}
if (skipped.length > 0) {
return (t |> skipped.indexOf(%)) === -1;
}
return true;
};
tests.valid = predicate |> tests.valid.filter(%);
tests.invalid = predicate |> tests.invalid.filter(%);
}
'rules-of-hooks/rules-of-hooks' |> describe(%, () => {
new ESLintTesterV7({
parser: 'babel-eslint' |> require.resolve(%),
parserOptions: {
ecmaVersion: 6,
sourceType: 'module'
}
}).run('eslint: v7', ReactHooksESLintRule, tests);
new ESLintTesterV9({
languageOptions: {
parser: BabelEslintParser,
ecmaVersion: 6,
sourceType: 'module'
}
}).run('eslint: v9', ReactHooksESLintRule, tests);
});