Skip to content

fix: detect useCallback methods in imperative handles - #1139

Open
askalf wants to merge 6 commits into
reactjs:mainfrom
askalf:fix/imperative-handle-callback-methods
Open

askalf wants to merge 6 commits into
reactjs:mainfrom
askalf:fix/imperative-handle-callback-methods

Conversation

@askalf

@askalf askalf commented Sep 20, 2026

Copy link
Copy Markdown

Fixes #856

Summary

  • Resolve the function wrapped by React useCallback when documenting component methods.
  • Use that shared resolution for both method admission and signature extraction.
  • Add coverage across imperative handles, object/statics/class/assignment surfaces and negative controls.

Decisions

Resolution intentionally unwraps one React useCallback layer only; recursively unwrapping or treating other React builtins as methods would broaden behavior beyond this defect. The package suite and formatting checks passed.

AI assistance: this bug was found and the fix and tests were drafted with AI tooling in my workflow; the tests and checks above were executed as pasted. I'm responsible for the change and will handle review feedback.

askalf and others added 5 commits September 19, 2026 05:11
The handler recognised a useCallback-wrapped property as a method, but
the documentation was built from the call expression, so the method
came out with no parameters and no return type. resolveToMethodFunction
now resolves a value to the function it documents, unwrapping a React
useCallback call, and both the handler's method test and the
documentation builder use it. The tests assert the wrapped function's
signature and cover a local function named useCallback, a call with no
arguments and a non-function argument.
The handler's unwrapping now runs for every method node path, so add the
surfaces it reaches beyond the imperative handle identifier: a callback
written inline in the handle object, an ObjectExpression component, a
statics object, a class property and a Component.foo assignment, plus the
docblock and the async/generator modifiers of the wrapped function, a
renamed useCallback import and a handle that precedes the declaration.
The controls pin the guards the unwrapping keeps: a local useCallback, a
missing or non-function argument, a spread argument, a nested call and
useMemo.
Every positive callback fixture carried a typed parameter and a return
annotation, so the empty-signature path the boundary ledger claims was
never exercised. Admit useCallback(() => {}, []) through
useImperativeHandle and assert an empty params list and a null return.
@changeset-bot

changeset-bot Bot commented Sep 20, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: a9605a4

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
react-docgen Patch
@react-docgen/cli Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@netlify

netlify Bot commented Sep 20, 2026

Copy link
Copy Markdown

Deploy Preview for react-docgen canceled.

Name Link
🔨 Latest commit a9605a4
🔍 Latest deploy log https://app.netlify.com/projects/react-docgen/deploys/6aaf375b91c0cf000803866b

@danez danez left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR.

The core useImperativeHandle + useCallback support makes sense, but I think the tests and implementation should be tightened before merging. In particular, please cover the exact issue reproducer and either narrow or consistently implement the additional method surfaces.

])(
'extracts a method wrapped with $name',
({ imports, setup, value, imperativeHandle }) => {
const definition = parse.statementLast<FunctionDeclaration>(`

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we add a regression test that runs the exact shape from #856 through the normal parser/resolver: memo(forwardRef(...)), a top-level useCallback, and shorthand exposure through useImperativeHandle? These tests invoke the handler directly with a plain function, so they do not pin the wrapper-resolution path from the reported bug.

AI disclosure: This comment was added by ChatGPT 5.6 Sol.


isProbablyMethod = value.isFunction();
isProbablyMethod =
resolveToMethodFunction(path.get('value') as NodePath) !== null;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This changes generic object and class-property method detection, although #856 only asks about methods exposed through useImperativeHandle. It also adds support for useCallback in class fields/statics, where Hooks are not valid React usage. Could we keep the unwrapping local to the imperative-handle path unless there is a separate use case for broadening every method surface?

AI disclosure: This comment was added by ChatGPT 5.6 Sol.

.statementLast<ExpressionStatement>(
`import { useCallback } from 'react';
const Foo = () => {}
Foo.foo = useCallback((bar: number): number => bar, [])

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test suggests that Component.foo = useCallback(...) is supported, but findAssignedMethods still filters assignments with resolveToValue(right).isFunction(), so the public handler never passes this assignment to getMethodDocumentation. Please either update the handler consistently and test it through componentMethodsHandler, or remove this out-of-scope assignment case.

AI disclosure: This comment was added by ChatGPT 5.6 Sol.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

useCallback + useImperativeHandle methods

2 participants