Skip to content

Fix spurious React 18.3 ref warning in AnimatePresence non-popLayout mode#3754

Open
mattgperry wants to merge 1 commit into
mainfrom
fix-3745-popchild-ref-warning
Open

Fix spurious React 18.3 ref warning in AnimatePresence non-popLayout mode#3754
mattgperry wants to merge 1 commit into
mainfrom
fix-3745-popchild-ref-warning

Conversation

@mattgperry

Copy link
Copy Markdown
Collaborator

Bug

AnimatePresence in any mode other than popLayout (i.e. the default sync and wait) emits a React warning when its child has a ref:

Warning: [object Object]: `ref` is not a prop. Trying to access it will result in
`undefined` being returned. ...

Minimal repro:

const ref = useRef(null)

<AnimatePresence>
  {isVisible && (
    <motion.div key="content" ref={ref} exit={{ opacity: 0 }}>
      Hello
    </motion.div>
  )}
</AnimatePresence>

Cause

PopChild read children.props?.ref unconditionally on every render:

const childRef =
    (children.props as { ref?: React.Ref<HTMLElement> })?.ref ??
    (children as unknown as { ref?: React.Ref<HTMLElement> })?.ref

In React 18.3, creating an element with a ref prop installs a warning getter on element.props.ref (defineRefPropWarningGetter). Any access to that getter fires the warning. The composed ref is only ever used in the pop !== false branch (React.cloneElement(children, { ref: composedRef })), so in non-popLayout mode the read is pointless yet still trips the warning.

The [object Object] display name appears because motion.* components are forwardRef objects (not functions), so React's element validator stringifies the raw object as the display name.

This is distinct from the earlier React 19 popLayout ref fixes (v12.23.16, v12.24.5) — it affects non-popLayout mode under React 18.3.

Fix

Only read children.props.ref when the ref is actually used (pop !== false):

const childRef =
    pop !== false
        ? ((children.props as { ref?: React.Ref<HTMLElement> })?.ref ??
          (children as unknown as { ref?: React.Ref<HTMLElement> })?.ref)
        : undefined

popLayout behaviour is unchanged — the ref is still read and composed when popping.

Test

Added pop-child-ref.test.tsx, which renders an AnimatePresence (default mode) around a ref'd motion.div and asserts no `ref` is not a prop warning is logged. It lives in its own file so React's module-level "warned once" flag starts fresh.

  • Fails on main for the right reason (warning fired from PopChildPresenceChildAnimatePresence).
  • Passes with the fix.
  • Full framer-motion client suite passes (776 tests); all AnimatePresence/PopChild/use-presence suites pass.

Fixes #3745

🤖 Generated with Claude Code

…mode

PopChild read children.props.ref unconditionally. In React 18.3, creating
an element with a ref prop installs a warning getter on element.props.ref,
so reading it fired "`ref` is not a prop" even though the composed ref is
only used when popping the child out (pop !== false). Only read it then.

Fixes #3745

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Jun 12, 2026

Copy link
Copy Markdown

Greptile Summary

Guards the children.props.ref read in PopChild behind pop !== false so it only fires in popLayout mode, eliminating a spurious React 18.3 `ref` is not a prop warning in sync/wait modes. A dedicated regression test file is added to catch reintroduction.

  • PopChild.tsx: childRef is now conditionally assigned (pop !== false ? ... : undefined), preventing the React 18.3 warning getter from firing in non-popLayout mode while leaving popLayout behaviour intact.
  • pop-child-ref.test.tsx: Isolated regression test that mounts an AnimatePresence (default mode) around a ref'd motion.div and asserts no console warning is emitted; placed in its own file so React's module-level "warned once" flag starts fresh.

Confidence Score: 5/5

Safe to merge — the change is a single conditional guard with no behavioural impact on popLayout mode and a clear regression test verifying the fix.

The fix is minimal and targeted: it moves the children.props.ref read behind the same pop !== false gate that already controls the cloneElement call and the useInsertionEffect body. useComposedRefs handles undefined gracefully (the PossibleRef type explicitly includes undefined), and the composed ref is never attached to any element in non-popLayout mode both before and after this change. The new test file correctly isolates the module-level warned once flag and directly asserts the failure condition.

No files require special attention.

Important Files Changed

Filename Overview
packages/framer-motion/src/components/AnimatePresence/PopChild.tsx Conditionally reads child ref only in popLayout mode; fix is minimal and correct given that pop is always a boolean from PresenceChild.
packages/framer-motion/src/components/AnimatePresence/tests/pop-child-ref.test.tsx New regression test correctly isolates the React 18.3 warning-getter scenario; spies on console.error and asserts no ref warning fires on initial mount.

Sequence Diagram

sequenceDiagram
    participant AP as AnimatePresence
    participant PC as PresenceChild
    participant Pop as PopChild
    participant React as React 18.3

    AP->>PC: "render child (mode="sync"|"wait")"
    PC->>Pop: "pop={false}"
    Note over Pop: pop !== false → false<br/>childRef = undefined (no props.ref access)
    Pop-->>React: no warning getter triggered
    Pop->>AP: return children directly (no cloneElement)

    AP->>PC: "render child (mode="popLayout")"
    PC->>Pop: "pop={true}"
    Note over Pop: pop !== false → true<br/>childRef = children.props.ref
    Pop->>React: "cloneElement(children, { ref: composedRef })"
Loading

Reviews (1): Last reviewed commit: "Fix spurious React 18.3 ref warning in A..." | Re-trigger Greptile

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.

[BUG] AnimatePresence PopChild unconditionally reads children.props?.ref, triggering React 18.3 warning in non-popLayout mode

1 participant