diff --git a/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js b/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js index 3d60a36824d2..ea534b6e244d 100644 --- a/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js +++ b/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js @@ -1971,16 +1971,10 @@ const allTests = { `, // Explicitly test error messages here for various cases errors: [ - `\`onClick\` is a function created with React Hook "useEffectEvent", and can only be called from ` + - 'Effects and Effect Events in the same component.', - `\`onClick\` is a function created with React Hook "useEffectEvent", and can only be called from ` + - 'Effects and Effect Events in the same component.', - `\`onClick\` is a function created with React Hook "useEffectEvent", and can only be called from ` + - `Effects and Effect Events in the same component. ` + - `It cannot be assigned to a variable or passed down.`, - `\`onClick\` is a function created with React Hook "useEffectEvent", and can only be called from ` + - `Effects and Effect Events in the same component. ` + - `It cannot be assigned to a variable or passed down.`, + useEffectEventError('onClick', true), + useEffectEventError('onClick', true), + useEffectEventError('onClick', false), + useEffectEventError('onClick', false), ], }, { @@ -2008,16 +2002,10 @@ const allTests = { `, // Explicitly test error messages here for various cases errors: [ - `\`onClick\` is a function created with React Hook "useEffectEvent", and can only be called from ` + - 'Effects and Effect Events in the same component.', - `\`onClick\` is a function created with React Hook "useEffectEvent", and can only be called from ` + - 'Effects and Effect Events in the same component.', - `\`onClick\` is a function created with React Hook "useEffectEvent", and can only be called from ` + - `Effects and Effect Events in the same component. ` + - `It cannot be assigned to a variable or passed down.`, - `\`onClick\` is a function created with React Hook "useEffectEvent", and can only be called from ` + - `Effects and Effect Events in the same component. ` + - `It cannot be assigned to a variable or passed down.`, + useEffectEventError('onClick', true), + useEffectEventError('onClick', true), + useEffectEventError('onClick', false), + useEffectEventError('onClick', false), ], }, ], @@ -2025,87 +2013,80 @@ const allTests = { 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 + messageId: 'calledConditionally', + data: { + hookName: hook, + earlyReturnHint: 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.', + messageId: 'executedMoreThanOnce', + data: {hookName: hook}, }; } 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".', + messageId: 'calledInInvalidFunction', + data: {hookName: hook, functionName: fn}, }; } 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.', + messageId: 'calledInsideCallback', + data: {hookName: hook}, }; } 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.', + messageId: 'calledAtTopLevel', + data: {hookName: hook}, }; } 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.', + messageId: 'calledInClass', + data: {hookName: hook}, }; } function useEffectEventError(fn, called) { if (fn === null) { return { - message: - `React Hook "useEffectEvent" can only be called at the top level of your component.` + - ` It cannot be passed down.`, + messageId: 'useEffectEventTopLevel', }; } return { - message: - `\`${fn}\` is a function created with React Hook "useEffectEvent", and can only be called from ` + - 'Effects and Effect Events in the same component.' + - (called ? '' : ' It cannot be assigned to a variable or passed down.'), + messageId: 'useEffectEventInvalidUsage', + data: { + functionName: fn, + passDownHint: called + ? '' + : ' It cannot be assigned to a variable or passed down.', + }, }; } function asyncComponentHookError(fn) { return { - message: `React Hook "${fn}" cannot be called in an async function.`, + messageId: 'calledInAsyncFunction', + data: {hookName: fn}, }; } function tryCatchUseError(fn) { return { - message: `React Hook "${fn}" cannot be called in a try/catch block.`, + messageId: 'useInTryCatch', + data: {hookName: fn}, }; } diff --git a/packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts b/packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts index 6b790680608d..ffef7ea918f3 100644 --- a/packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts +++ b/packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts @@ -51,6 +51,83 @@ const rule = { }, fixable: 'code', hasSuggestions: true, + messages: { + asyncEffectCallback: + 'Effect callbacks are synchronous to prevent race conditions. ' + + 'Put the async function inside:\n\n' + + 'useEffect(() => {\n' + + ' async function fetchData() {\n' + + ' // You can await here\n' + + ' const response = await MyAPI.getData(someId);\n' + + ' // ...\n' + + ' }\n' + + ' fetchData();\n' + + '}, [someId]); // Or [] if effect doesn\'t need props or state\n\n' + + 'Learn more about data fetching with Hooks: https://react.dev/link/hooks-data-fetching', + refCurrentInCleanup: + "The ref value '{{dependency}}.current' will likely have " + + 'changed by the time this effect cleanup function runs. If ' + + 'this ref points to a node rendered by React, copy ' + + "'{{dependency}}.current' to a variable inside the effect, and " + + 'use that variable in the cleanup function.', + staleAssignment: + "Assignments to the '{{key}}' variable from inside React Hook " + + '{{reactiveHookName}} will be lost after each ' + + 'render. To preserve the value over time, store it in a useRef ' + + "Hook and keep the mutable value in the '.current' property. " + + 'Otherwise, you can move this variable directly inside ' + + '{{reactiveHookName}}.', + setStateInEffectWithoutDeps: + "React Hook {{reactiveHookName}} contains a call to '{{stateCall}}'. " + + 'Without a list of dependencies, this can lead to an infinite chain of updates. ' + + 'To fix this, pass [{{suggestedDeps}}] as a second argument to the {{reactiveHookName}} Hook.', + depsNotArray: + 'React Hook {{reactiveHookName}} was passed a ' + + 'dependency list that is not an array literal. This means we ' + + "can't statically verify whether you've passed the correct " + + 'dependencies.', + spreadInDeps: + 'React Hook {{reactiveHookName}} has a spread ' + + "element in its dependency array. This means we can't " + + "statically verify whether you've passed the " + + 'correct dependencies.', + useEffectEventInDeps: + 'Functions returned from `useEffectEvent` must not be included in the dependency array. ' + + 'Remove `{{depName}}` from the list.', + literalInDepsWithSuggestion: + 'The {{literalRaw}} literal is not a valid dependency ' + + 'because it never changes. ' + + 'Did you mean to include {{literalValue}} in the array instead?', + literalInDeps: + 'The {{literalRaw}} literal is not a valid dependency ' + + 'because it never changes. You can safely remove it.', + complexExpressionInDeps: + 'React Hook {{reactiveHookName}} has a ' + + 'complex expression in the dependency array. ' + + 'Extract it to a separate variable so it can be statically checked.', + constructionChangesEveryRender: + "The '{{constructionName}}' {{depType}} {{causation}} the dependencies of " + + '{{reactiveHookName}} Hook (at line {{line}}) ' + + 'change on every render. {{advice}}', + depsChanged: + 'React Hook {{reactiveHookName}} has {{problem}}{{extraWarning}}', + noCallbackProvided: + 'React Hook {{reactiveHookName}} requires an effect callback. ' + + 'Did you forget to pass a callback to the hook?', + requiresExplicitDeps: + 'React Hook {{reactiveHookName}} always requires dependencies. ' + + 'Please add a dependency array or an explicit `undefined`', + noDepsForOptimization: + 'React Hook {{reactiveHookName}} does nothing when called with ' + + 'only one argument. Did you forget to pass an array of ' + + 'dependencies?', + unknownDependencies: + 'React Hook {{reactiveHookName}} received a function whose dependencies ' + + 'are unknown. Pass an inline function instead.', + missingDependencyFallback: + "React Hook {{reactiveHookName}} has a missing dependency: '{{callbackName}}'. " + + 'Either include it or remove the dependency array.', + }, schema: [ { type: 'object', @@ -189,18 +266,7 @@ const rule = { if (isEffect && node.async) { reportProblem({ node: node, - message: - `Effect callbacks are synchronous to prevent race conditions. ` + - `Put the async function inside:\n\n` + - 'useEffect(() => {\n' + - ' async function fetchData() {\n' + - ' // You can await here\n' + - ' const response = await MyAPI.getData(someId);\n' + - ' // ...\n' + - ' }\n' + - ' fetchData();\n' + - `}, [someId]); // Or [] if effect doesn't need props or state\n\n` + - 'Learn more about data fetching with Hooks: https://react.dev/link/hooks-data-fetching', + messageId: 'asyncEffectCallback', }); } @@ -627,12 +693,8 @@ const rule = { reportProblem({ // @ts-expect-error We can do better here (dependencyNode.parent has not been type narrowed) node: dependencyNode.parent.property, - message: - `The ref value '${dependency}.current' will likely have ` + - `changed by the time this effect cleanup function runs. If ` + - `this ref points to a node rendered by React, copy ` + - `'${dependency}.current' to a variable inside the effect, and ` + - `use that variable in the cleanup function.`, + messageId: 'refCurrentInCleanup', + data: {dependency}, }); }, ); @@ -647,13 +709,11 @@ const rule = { staleAssignments.add(key); reportProblem({ node: writeExpr, - message: - `Assignments to the '${key}' variable from inside React Hook ` + - `${getSourceCode().getText(reactiveHook)} will be lost after each ` + - `render. To preserve the value over time, store it in a useRef ` + - `Hook and keep the mutable value in the '.current' property. ` + - `Otherwise, you can move this variable directly inside ` + - `${getSourceCode().getText(reactiveHook)}.`, + messageId: 'staleAssignment', + data: { + key, + reactiveHookName: getSourceCode().getText(reactiveHook), + }, }); } @@ -718,12 +778,12 @@ const rule = { }); reportProblem({ node: reactiveHook, - message: - `React Hook ${reactiveHookName} contains a call to '${setStateInsideEffectWithoutDeps}'. ` + - `Without a list of dependencies, this can lead to an infinite chain of updates. ` + - `To fix this, pass [` + - suggestedDependencies.join(', ') + - `] as a second argument to the ${reactiveHookName} Hook.`, + messageId: 'setStateInEffectWithoutDeps', + data: { + reactiveHookName, + stateCall: setStateInsideEffectWithoutDeps, + suggestedDeps: suggestedDependencies.join(', '), + }, suggest: [ { desc: `Add dependencies array: [${suggestedDependencies.join( @@ -763,11 +823,10 @@ const rule = { // the user this in an error. reportProblem({ node: declaredDependenciesNode, - message: - `React Hook ${getSourceCode().getText(reactiveHook)} was passed a ` + - 'dependency list that is not an array literal. This means we ' + - "can't statically verify whether you've passed the correct " + - 'dependencies.', + messageId: 'depsNotArray', + data: { + reactiveHookName: getSourceCode().getText(reactiveHook), + }, }); } else { const arrayExpression = isTSAsArrayExpression @@ -784,22 +843,20 @@ const rule = { if (declaredDependencyNode.type === 'SpreadElement') { reportProblem({ node: declaredDependencyNode, - message: - `React Hook ${getSourceCode().getText(reactiveHook)} has a spread ` + - "element in its dependency array. This means we can't " + - "statically verify whether you've passed the " + - 'correct dependencies.', + messageId: 'spreadInDeps', + data: { + reactiveHookName: getSourceCode().getText(reactiveHook), + }, }); return; } if (useEffectEventVariables.has(declaredDependencyNode)) { reportProblem({ node: declaredDependencyNode, - message: - 'Functions returned from `useEffectEvent` must not be included in the dependency array. ' + - `Remove \`${getSourceCode().getText( - declaredDependencyNode, - )}\` from the list.`, + messageId: 'useEffectEventInDeps', + data: { + depName: getSourceCode().getText(declaredDependencyNode), + }, suggest: [ { desc: `Remove the dependency \`${getSourceCode().getText( @@ -832,26 +889,28 @@ const rule = { ) { reportProblem({ node: declaredDependencyNode, - message: - `The ${declaredDependencyNode.raw} literal is not a valid dependency ` + - `because it never changes. ` + - `Did you mean to include ${declaredDependencyNode.value} in the array instead?`, + messageId: 'literalInDepsWithSuggestion', + data: { + literalRaw: String(declaredDependencyNode.raw), + literalValue: String(declaredDependencyNode.value), + }, }); } else { reportProblem({ node: declaredDependencyNode, - message: - `The ${declaredDependencyNode.raw} literal is not a valid dependency ` + - 'because it never changes. You can safely remove it.', + messageId: 'literalInDeps', + data: { + literalRaw: String(declaredDependencyNode.raw), + }, }); } } else { reportProblem({ node: declaredDependencyNode, - message: - `React Hook ${getSourceCode().getText(reactiveHook)} has a ` + - `complex expression in the dependency array. ` + - 'Extract it to a separate variable so it can be statically checked.', + messageId: 'complexExpressionInDeps', + data: { + reactiveHookName: getSourceCode().getText(reactiveHook), + }, }); } @@ -935,11 +994,6 @@ const rule = { ? 'could make' : 'makes'; - const message = - `The '${construction.name.name}' ${depType} ${causation} the dependencies of ` + - `${reactiveHookName} Hook (at line ${declaredDependenciesNode.loc?.start.line}) ` + - `change on every render. ${advice}`; - let suggest: Rule.ReportDescriptor['suggest']; // Only handle the simple case of variable assignments. // Wrapping function declarations can mess up hoisting. @@ -977,7 +1031,17 @@ const rule = { reportProblem({ // TODO: Why not report this at the dependency site? node: construction.node, - message, + messageId: 'constructionChangesEveryRender', + data: { + constructionName: construction.name.name, + depType, + causation, + reactiveHookName, + line: String( + declaredDependenciesNode.loc?.start.line, + ), + advice, + }, suggest, }); }, @@ -1289,23 +1353,32 @@ const rule = { reportProblem({ node: declaredDependenciesNode, - message: - `React Hook ${getSourceCode().getText(reactiveHook)} has ` + + messageId: 'depsChanged', + data: { + reactiveHookName: getSourceCode().getText(reactiveHook), // To avoid a long message, show the next actionable item. - (getWarningMessage(missingDependencies, 'a', 'missing', 'include') || - getWarningMessage( - unnecessaryDependencies, - 'an', - 'unnecessary', - 'exclude', - ) || + problem: String( getWarningMessage( - duplicateDependencies, + missingDependencies, 'a', - 'duplicate', - 'omit', - )) + + 'missing', + 'include', + ) || + getWarningMessage( + unnecessaryDependencies, + 'an', + 'unnecessary', + 'exclude', + ) || + getWarningMessage( + duplicateDependencies, + 'a', + 'duplicate', + 'omit', + ), + ), extraWarning, + }, suggest: [ { desc: `Update the dependencies array to be: [${suggestedDeps @@ -1348,9 +1421,8 @@ const rule = { if (!callback) { reportProblem({ node: reactiveHook, - message: - `React Hook ${reactiveHookName} requires an effect callback. ` + - `Did you forget to pass a callback to the hook?`, + messageId: 'noCallbackProvided', + data: {reactiveHookName}, }); return; } @@ -1358,9 +1430,8 @@ const rule = { if (!maybeNode && isEffect && options.requireExplicitEffectDeps) { reportProblem({ node: reactiveHook, - message: - `React Hook ${reactiveHookName} always requires dependencies. ` + - `Please add a dependency array or an explicit \`undefined\``, + messageId: 'requiresExplicitDeps', + data: {reactiveHookName}, }); } @@ -1385,10 +1456,8 @@ const rule = { // TODO: Can this have a suggestion? reportProblem({ node: reactiveHook, - message: - `React Hook ${reactiveHookName} does nothing when called with ` + - `only one argument. Did you forget to pass an array of ` + - `dependencies?`, + messageId: 'noDepsForOptimization', + data: {reactiveHookName}, }); } return; @@ -1452,7 +1521,8 @@ const rule = { if (def.type === 'Parameter') { reportProblem({ node: reactiveHook, - message: getUnknownDependenciesMessage(reactiveHookName), + messageId: 'unknownDependencies', + data: {reactiveHookName}, }); return; } @@ -1500,7 +1570,8 @@ const rule = { // useEffect(generateEffectBody(), []); reportProblem({ node: reactiveHook, - message: getUnknownDependenciesMessage(reactiveHookName), + messageId: 'unknownDependencies', + data: {reactiveHookName}, }); return; // Handled } @@ -1508,9 +1579,11 @@ const rule = { // Something unusual. Fall back to suggesting to add the body itself as a dep. reportProblem({ node: reactiveHook, - message: - `React Hook ${reactiveHookName} has a missing dependency: '${callback.name}'. ` + - `Either include it or remove the dependency array.`, + messageId: 'missingDependencyFallback', + data: { + reactiveHookName, + callbackName: callback.name, + }, suggest: [ { desc: `Update the dependencies array to be: [${callback.name}]`, @@ -2128,11 +2201,5 @@ function isUseEffectEventIdentifier(node: Node): boolean { return node.type === 'Identifier' && node.name === 'useEffectEvent'; } -function getUnknownDependenciesMessage(reactiveHookName: string): string { - return ( - `React Hook ${reactiveHookName} received a function whose dependencies ` + - `are unknown. Pass an inline function instead.` - ); -} export default rule; diff --git a/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts b/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts index ca82c99e2f55..a09ed00de96f 100644 --- a/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts +++ b/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts @@ -171,21 +171,6 @@ function isUseEffectEventIdentifier(node: Node): boolean { return node.type === 'Identifier' && node.name === 'useEffectEvent'; } -function useEffectEventError(fn: string | null, called: boolean): string { - // no function identifier, i.e. it is not assigned to a variable - if (fn === null) { - return ( - `React Hook "useEffectEvent" can only be called at the top level of your component.` + - ` It cannot be passed down.` - ); - } - - return ( - `\`${fn}\` is a function created with React Hook "useEffectEvent", and can only be called from ` + - 'Effects and Effect Events in the same component.' + - (called ? '' : ' It cannot be assigned to a variable or passed down.') - ); -} function isUseIdentifier(node: Node): boolean { return isReactFunction(node, 'use'); @@ -199,6 +184,44 @@ const rule = { recommended: true, url: 'https://react.dev/reference/rules/rules-of-hooks', }, + messages: { + useInTryCatch: + 'React Hook "{{hookName}}" cannot be called in a try/catch block.', + executedMoreThanOnce: + 'React Hook "{{hookName}}" 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.', + calledInAsyncFunction: + 'React Hook "{{hookName}}" cannot be called in an async function.', + calledConditionally: + 'React Hook "{{hookName}}" is called conditionally. React Hooks must be ' + + 'called in the exact same order in every component render.' + + '{{earlyReturnHint}}', + calledInClass: + 'React Hook "{{hookName}}" cannot be called in a class component. React Hooks ' + + 'must be called in a React function component or a custom React ' + + 'Hook function.', + calledInInvalidFunction: + 'React Hook "{{hookName}}" is called in function "{{functionName}}" 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".', + calledAtTopLevel: + 'React Hook "{{hookName}}" cannot be called at the top level. React Hooks ' + + 'must be called in a React function component or a custom React ' + + 'Hook function.', + calledInsideCallback: + 'React Hook "{{hookName}}" cannot be called inside a callback. React Hooks ' + + 'must be called in a React function component or a custom React ' + + 'Hook function.', + useEffectEventTopLevel: + 'React Hook "useEffectEvent" can only be called at the top level of your component.' + + ' It cannot be passed down.', + useEffectEventInvalidUsage: + '`{{functionName}}` is a function created with React Hook "useEffectEvent", and can only be called from ' + + 'Effects and Effect Events in the same component.' + + '{{passDownHint}}', + }, schema: [ { type: 'object', @@ -621,9 +644,8 @@ const rule = { if (isUseIdentifier(hook) && isInsideTryCatch(hook)) { context.report({ node: hook, - message: `React Hook "${getSourceCode().getText( - hook, - )}" cannot be called in a try/catch block.`, + messageId: 'useInTryCatch', + data: {hookName: getSourceCode().getText(hook)}, }); } @@ -635,13 +657,8 @@ const rule = { ) { context.report({ node: hook, - message: - `React Hook "${getSourceCode().getText( - 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.', + messageId: 'executedMoreThanOnce', + data: {hookName: getSourceCode().getText(hook)}, }); } @@ -657,9 +674,8 @@ const rule = { if (isAsyncFunction) { context.report({ node: hook, - message: - `React Hook "${getSourceCode().getText(hook)}" cannot be ` + - 'called in an async function.', + messageId: 'calledInAsyncFunction', + data: {hookName: getSourceCode().getText(hook)}, }); } @@ -673,15 +689,17 @@ const rule = { !isUseIdentifier(hook) && // `use(...)` can be called conditionally. !isInsideDoWhileLoop(hook) // wrapping do/while loops are checked separately. ) { - const message = - `React Hook "${getSourceCode().getText(hook)}" is called ` + - 'conditionally. React Hooks must be called in the exact ' + - 'same order in every component render.' + - (possiblyHasEarlyReturn - ? ' Did you accidentally call a React Hook after an' + - ' early return?' - : ''); - context.report({node: hook, message}); + context.report({ + node: hook, + messageId: 'calledConditionally', + data: { + hookName: getSourceCode().getText(hook), + earlyReturnHint: possiblyHasEarlyReturn + ? ' Did you accidentally call a React Hook after an' + + ' early return?' + : '', + }, + }); } } else if ( codePathNode.parent != null && @@ -692,32 +710,28 @@ const rule = { codePathNode.parent.value === codePathNode ) { // Custom message for hooks inside a class - const message = - `React Hook "${getSourceCode().getText( - hook, - )}" cannot be called ` + - 'in a class component. React Hooks must be called in a ' + - 'React function component or a custom React Hook function.'; - context.report({node: hook, message}); + context.report({ + node: hook, + messageId: 'calledInClass', + data: {hookName: getSourceCode().getText(hook)}, + }); } else if (codePathFunctionName) { // Custom message if we found an invalid function name. - const message = - `React Hook "${getSourceCode().getText(hook)}" is called in ` + - `function "${getSourceCode().getText(codePathFunctionName)}" ` + - '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".'; - context.report({node: hook, message}); + context.report({ + node: hook, + messageId: 'calledInInvalidFunction', + data: { + hookName: getSourceCode().getText(hook), + functionName: getSourceCode().getText(codePathFunctionName), + }, + }); } else if (codePathNode.type === 'Program') { // These are dangerous if you have inline requires enabled. - const message = - `React Hook "${getSourceCode().getText( - hook, - )}" cannot be called ` + - 'at the top level. React Hooks must be called in a ' + - 'React function component or a custom React Hook function.'; - context.report({node: hook, message}); + context.report({ + node: hook, + messageId: 'calledAtTopLevel', + data: {hookName: getSourceCode().getText(hook)}, + }); } else { // Assume in all other cases the user called a hook in some // random function callback. This should usually be true for @@ -726,13 +740,11 @@ const rule = { // uncommon cases doesn't matter. // `use(...)` can be called in callbacks. if (isSomewhereInsideComponentOrHook && !isUseIdentifier(hook)) { - const message = - `React Hook "${getSourceCode().getText( - hook, - )}" cannot be called ` + - 'inside a callback. React Hooks must be called in a ' + - 'React function component or a custom React Hook function.'; - context.report({node: hook, message}); + context.report({ + node: hook, + messageId: 'calledInsideCallback', + data: {hookName: getSourceCode().getText(hook)}, + }); } } } @@ -789,11 +801,9 @@ const rule = { // like in other hooks, calling useEffectEvent at component's top level without assignment is valid node.parent?.type !== 'ExpressionStatement' ) { - const message = useEffectEventError(null, false); - context.report({ node, - message, + messageId: 'useEffectEventTopLevel', }); } }, @@ -802,14 +812,16 @@ const rule = { // This identifier resolves to a useEffectEvent function, but isn't being referenced in an // effect or another event function. It isn't being called either. if (lastEffect == null && useEffectEventFunctions.has(node)) { - const message = useEffectEventError( - getSourceCode().getText(node), - node.parent.type === 'CallExpression', - ); - context.report({ node, - message, + messageId: 'useEffectEventInvalidUsage', + data: { + functionName: getSourceCode().getText(node), + passDownHint: + node.parent.type === 'CallExpression' + ? '' + : ' It cannot be assigned to a variable or passed down.', + }, }); } },