in packages/eslint-plugin-react-hooks/src/ExhaustiveDeps.js [1152:1306]
function visitCallExpression(node) {
const callbackIndex = getReactiveHookCallbackIndex(node.callee, options);
if (callbackIndex === -1) {
// Not a React Hook call that needs deps.
return;
}
const callback = node.arguments[callbackIndex];
const reactiveHook = node.callee;
const reactiveHookName = getNodeWithoutReactNamespace(reactiveHook).name;
const declaredDependenciesNode = node.arguments[callbackIndex + 1];
const isEffect = /Effect($|[^a-z])/g.test(reactiveHookName);
// Check whether a callback is supplied. If there is no callback supplied
// then the hook will not work and React will throw a TypeError.
// So no need to check for dependency inclusion.
if (!callback) {
reportProblem({
node: reactiveHook,
message:
`React Hook ${reactiveHookName} requires an effect callback. ` +
`Did you forget to pass a callback to the hook?`,
});
return;
}
// Check the declared dependencies for this reactive hook. If there is no
// second argument then the reactive callback will re-run on every render.
// So no need to check for dependency inclusion.
if (!declaredDependenciesNode && !isEffect) {
// These are only used for optimization.
if (
reactiveHookName === 'useMemo' ||
reactiveHookName === 'useCallback'
) {
// 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?`,
});
}
return;
}
switch (callback.type) {
case 'FunctionExpression':
case 'ArrowFunctionExpression':
visitFunctionWithDependencies(
callback,
declaredDependenciesNode,
reactiveHook,
reactiveHookName,
isEffect,
);
return; // Handled
case 'Identifier':
if (!declaredDependenciesNode) {
// No deps, no problems.
return; // Handled
}
// The function passed as a callback is not written inline.
// But perhaps it's in the dependencies array?
if (
declaredDependenciesNode.elements &&
declaredDependenciesNode.elements.some(
el => el && el.type === 'Identifier' && el.name === callback.name,
)
) {
// If it's already in the list of deps, we don't care because
// this is valid regardless.
return; // Handled
}
// We'll do our best effort to find it, complain otherwise.
const variable = context.getScope().set.get(callback.name);
if (variable == null || variable.defs == null) {
// If it's not in scope, we don't care.
return; // Handled
}
// The function passed as a callback is not written inline.
// But it's defined somewhere in the render scope.
// We'll do our best effort to find and check it, complain otherwise.
const def = variable.defs[0];
if (!def || !def.node) {
break; // Unhandled
}
if (def.type !== 'Variable' && def.type !== 'FunctionName') {
// Parameter or an unusual pattern. Bail out.
break; // Unhandled
}
switch (def.node.type) {
case 'FunctionDeclaration':
// useEffect(() => { ... }, []);
visitFunctionWithDependencies(
def.node,
declaredDependenciesNode,
reactiveHook,
reactiveHookName,
isEffect,
);
return; // Handled
case 'VariableDeclarator':
const init = def.node.init;
if (!init) {
break; // Unhandled
}
switch (init.type) {
// const effectBody = () => {...};
// useEffect(effectBody, []);
case 'ArrowFunctionExpression':
case 'FunctionExpression':
// We can inspect this function as if it were inline.
visitFunctionWithDependencies(
init,
declaredDependenciesNode,
reactiveHook,
reactiveHookName,
isEffect,
);
return; // Handled
}
break; // Unhandled
}
break; // Unhandled
default:
// useEffect(generateEffectBody(), []);
reportProblem({
node: reactiveHook,
message:
`React Hook ${reactiveHookName} received a function whose dependencies ` +
`are unknown. Pass an inline function instead.`,
});
return; // Handled
}
// 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.`,
suggest: [
{
desc: `Update the dependencies array to be: [${callback.name}]`,
fix(fixer) {
return fixer.replaceText(
declaredDependenciesNode,
`[${callback.name}]`,
);
},
},
],
});
}