diff --git a/packages/framer-motion/src/motion/utils/__tests__/use-motion-ref.test.tsx b/packages/framer-motion/src/motion/utils/__tests__/use-motion-ref.test.tsx
new file mode 100644
index 0000000000..1c361f59db
--- /dev/null
+++ b/packages/framer-motion/src/motion/utils/__tests__/use-motion-ref.test.tsx
@@ -0,0 +1,149 @@
+import * as React from "react"
+import { useRef } from "react"
+import { motion } from "../../.."
+import { render } from "../../../jest.setup"
+
+describe("useMotionRef", () => {
+ it("should call external ref callback with element on mount", () => {
+ const refCallback = jest.fn()
+
+ const Component = () => {
+ return
+ }
+
+ render()
+
+ expect(refCallback).toHaveBeenCalledTimes(1)
+ expect(refCallback).toHaveBeenCalledWith(expect.any(HTMLElement))
+ })
+
+ it("should call external ref callback with null on unmount (React 18 behavior)", () => {
+ const refCallback = jest.fn()
+
+ const Component = () => {
+ return
+ }
+
+ const { unmount } = render()
+
+ // Clear previous calls
+ refCallback.mockClear()
+
+ unmount()
+
+ expect(refCallback).toHaveBeenCalledTimes(1)
+ expect(refCallback).toHaveBeenCalledWith(null)
+ })
+
+ it("should support React 19 cleanup function pattern (forward compatibility)", () => {
+ // This test verifies that when a ref callback returns a cleanup function,
+ // our code properly stores it and calls it on unmount instead of calling ref(null).
+ // This works in both React 18 and React 19 without warnings.
+ const cleanup = jest.fn()
+ const refCallback = jest.fn(() => cleanup)
+
+ const Component = () => {
+ return
+ }
+
+ const { unmount } = render()
+
+ // Verify mount called correctly
+ expect(refCallback).toHaveBeenCalledTimes(1)
+ expect(refCallback).toHaveBeenCalledWith(expect.any(HTMLElement))
+
+ // Clear previous calls to focus on unmount behavior
+ refCallback.mockClear()
+ cleanup.mockClear()
+
+ unmount()
+
+ // With our new approach: cleanup function should be called
+ // and ref should NOT be called with null
+ expect(cleanup).toHaveBeenCalledTimes(1)
+ expect(refCallback).not.toHaveBeenCalledWith(null)
+ })
+
+ it("should handle RefObject refs correctly", () => {
+ const Component = () => {
+ const ref = useRef(null)
+ return
+ }
+
+ const { unmount } = render()
+
+ // Should not throw on mount or unmount
+ expect(() => unmount()).not.toThrow()
+ })
+
+ it("should handle mixed ref types in motion components", () => {
+ const refCallback = jest.fn()
+
+ const Component = ({ useCallback }: { useCallback: boolean }) => {
+ const refObject = useRef(null)
+ return
+ }
+
+ const { rerender } = render()
+
+ expect(refCallback).toHaveBeenCalledWith(expect.any(HTMLElement))
+
+ // Should handle transition between ref types without errors
+ expect(() => rerender()).not.toThrow()
+ })
+
+ it("should handle visual element cleanup correctly with React 19 pattern", () => {
+ const cleanup = jest.fn()
+ const refCallback = jest.fn(() => cleanup)
+
+ const Component = () => {
+ return (
+
+ )
+ }
+
+ const { unmount } = render()
+
+ // Clear previous calls
+ refCallback.mockClear()
+ cleanup.mockClear()
+
+ unmount()
+
+ // Both external ref cleanup and visual element unmount should happen
+ expect(cleanup).toHaveBeenCalledTimes(1)
+ expect(refCallback).not.toHaveBeenCalledWith(null)
+ })
+
+ it("should work with forwardRef components and React 19 cleanup pattern", () => {
+ const cleanup = jest.fn()
+ const refCallback = jest.fn(() => cleanup)
+
+ const ForwardedComponent = React.forwardRef(
+ (props, ref) => {
+ return
+ }
+ )
+
+ const Component = () => {
+ return
+ }
+
+ const { unmount } = render()
+
+ expect(refCallback).toHaveBeenCalledWith(expect.any(HTMLElement))
+
+ // Clear previous calls
+ refCallback.mockClear()
+ cleanup.mockClear()
+
+ unmount()
+
+ expect(cleanup).toHaveBeenCalledTimes(1)
+ expect(refCallback).not.toHaveBeenCalledWith(null)
+ })
+})
diff --git a/packages/framer-motion/src/motion/utils/use-motion-ref.ts b/packages/framer-motion/src/motion/utils/use-motion-ref.ts
index 58db52036a..e756b04b8c 100644
--- a/packages/framer-motion/src/motion/utils/use-motion-ref.ts
+++ b/packages/framer-motion/src/motion/utils/use-motion-ref.ts
@@ -1,11 +1,24 @@
"use client"
import * as React from "react"
-import { useCallback } from "react"
+import { useCallback, useRef } from "react"
import type { VisualElement } from "../../render/VisualElement"
import { isRefObject } from "../../utils/is-ref-object"
import { VisualState } from "./use-visual-state"
+/**
+ * Set a given ref to a given value
+ * This utility takes care of different types of refs: callback refs and RefObject(s)
+ * Returns a cleanup function if the ref callback returns one (React 19 feature)
+ */
+function setRef(ref: React.Ref, value: T): void | (() => void) {
+ if (typeof ref === "function") {
+ return ref(value)
+ } else if (isRefObject(ref)) {
+ ;(ref as any).current = value
+ }
+}
+
/**
* Creates a ref function that, when called, hydrates the provided
* external ref and VisualElement.
@@ -15,6 +28,9 @@ export function useMotionRef(
visualElement?: VisualElement | null,
externalRef?: React.Ref
): React.Ref {
+ // Store the cleanup function from external ref if it returns one
+ const externalRefCleanupRef = useRef<(() => void) | null>(null)
+
return useCallback(
(instance: Instance) => {
if (instance) {
@@ -30,17 +46,27 @@ export function useMotionRef(
}
if (externalRef) {
- if (typeof externalRef === "function") {
- externalRef(instance)
- } else if (isRefObject(externalRef)) {
- ;(externalRef as any).current = instance
+ if (instance) {
+ // Mount: call the external ref and store any cleanup function
+ const cleanup = setRef(externalRef, instance)
+ if (typeof cleanup === "function") {
+ externalRefCleanupRef.current = cleanup
+ }
+ } else {
+ // Unmount: call stored cleanup function if available, otherwise call ref with null
+ if (externalRefCleanupRef.current) {
+ externalRefCleanupRef.current()
+ externalRefCleanupRef.current = null
+ } else {
+ // Fallback to React <19 behavior for refs that don't return cleanup
+ setRef(externalRef, instance)
+ }
}
}
},
/**
- * Include externalRef in dependencies to ensure the callback updates
- * when the ref changes, allowing proper ref forwarding.
+ * Include all dependencies to ensure the callback updates correctly
*/
- [visualElement]
+ [visualElement, visualState, externalRef]
)
}