Skip to content

Commit 1800f34

Browse files
committed
Fixing changed external ref
1 parent 7d11517 commit 1800f34

File tree

3 files changed

+79
-32
lines changed

3 files changed

+79
-32
lines changed

packages/framer-motion/src/motion/__tests__/component.test.tsx

Lines changed: 63 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { fireEvent } from "@testing-library/react"
2-
import { motion } from "framer-motion"
32
import * as React from "react"
3+
import { motion } from "../../"
44
import { render } from "../../jest.setup"
55

66
describe("motion component rendering and styles", () => {
@@ -310,38 +310,78 @@ describe("motion component rendering and styles", () => {
310310
})
311311

312312
it("layout animations interrupt jump", async () => {
313-
const promise = new Promise((r)=>{
314-
const Component = ()=>{
315-
const [open,setOpen] = React.useState(false)
313+
const promise = new Promise((r) => {
314+
const Component = () => {
315+
const [open, setOpen] = React.useState(false)
316316
const divRef = React.useRef<HTMLDivElement>(null)
317-
async function handleLayoutJump(){
317+
async function handleLayoutJump() {
318318
setOpen(true)
319-
await new Promise(resolve => setTimeout(resolve, 1500))
320-
const firstSize = divRef.current?.getBoundingClientRect().width||0
319+
await new Promise((resolve) => setTimeout(resolve, 1500))
320+
const firstSize =
321+
divRef.current?.getBoundingClientRect().width || 0
321322
setOpen(false)
322-
const secondSize = divRef.current?.getBoundingClientRect().width||0
323+
const secondSize =
324+
divRef.current?.getBoundingClientRect().width || 0
323325
r(Math.abs(firstSize - secondSize))
324326
}
325-
React.useEffect(()=>{
327+
React.useEffect(() => {
326328
handleLayoutJump()
327-
},[])
328-
return <motion.div layout="size">
329-
<motion.div
330-
layout="size"
331-
ref={divRef}
332-
style={{ width:open? "200px" : "50px" }}
333-
transition={{
334-
layout: {
335-
duration: 2,
336-
ease: "linear",
337-
},
338-
}}
339-
/>
340-
</motion.div>
329+
}, [])
330+
return (
331+
<motion.div layout="size">
332+
<motion.div
333+
layout="size"
334+
ref={divRef}
335+
style={{ width: open ? "200px" : "50px" }}
336+
transition={{
337+
layout: {
338+
duration: 2,
339+
ease: "linear",
340+
},
341+
}}
342+
/>
343+
</motion.div>
344+
)
341345
}
342346
render(<Component />)
343347
})
344348

345349
expect(promise).resolves.toBeLessThan(50)
346350
})
351+
352+
it("handles ref swapping correctly", () => {
353+
let ref1Element: HTMLDivElement | null = null
354+
let ref2Element: HTMLDivElement | null = null
355+
356+
const Component = ({ useRef2 }: { useRef2: boolean }) => {
357+
const ref1 = React.useRef<HTMLDivElement>(null)
358+
const ref2 = React.useRef<HTMLDivElement>(null)
359+
360+
const currentRef = useRef2 ? ref2 : ref1
361+
362+
React.useEffect(() => {
363+
// Capture ref values after each render
364+
ref1Element = ref1.current
365+
ref2Element = ref2.current
366+
})
367+
368+
return <motion.div ref={currentRef} data-testid="ref-element" />
369+
}
370+
371+
// Initial render with ref1
372+
const { rerender, getByTestId } = render(<Component useRef2={false} />)
373+
374+
// Verify ref1 is populated, ref2 is null
375+
expect(ref1Element).toBeTruthy()
376+
expect(ref2Element).toBeNull()
377+
expect(ref1Element).toBe(getByTestId("ref-element"))
378+
379+
// Re-render with ref2 (swap refs)
380+
rerender(<Component useRef2={true} />)
381+
382+
// Verify ref2 is now populated, ref1 is null
383+
expect(ref1Element).toBeNull()
384+
expect(ref2Element).toBeTruthy()
385+
expect(ref2Element).toBe(getByTestId("ref-element"))
386+
})
347387
})

packages/framer-motion/src/motion/utils/use-motion-ref.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,25 @@ export function useMotionRef<Instance, RenderState>(
1515
visualElement?: VisualElement<Instance> | null,
1616
externalRef?: React.Ref<Instance>
1717
): React.Ref<Instance> {
18+
const currentInstanceRef = React.useRef<Instance | null>(null)
19+
1820
return useCallback(
1921
(instance: Instance) => {
20-
if (instance) {
21-
visualState.onMount && visualState.onMount(instance)
22-
}
22+
const prevInstance = currentInstanceRef.current
23+
currentInstanceRef.current = instance
2324

24-
if (visualElement) {
25+
// Only run mount/unmount logic when the instance actually changes
26+
if (instance !== prevInstance) {
2527
if (instance) {
26-
visualElement.mount(instance)
27-
} else {
28-
visualElement.unmount()
28+
visualState.onMount && visualState.onMount(instance)
29+
}
30+
31+
if (visualElement) {
32+
if (instance) {
33+
visualElement.mount(instance)
34+
} else {
35+
visualElement.unmount()
36+
}
2937
}
3038
}
3139

@@ -41,6 +49,6 @@ export function useMotionRef<Instance, RenderState>(
4149
* Include externalRef in dependencies to ensure the callback updates
4250
* when the ref changes, allowing proper ref forwarding.
4351
*/
44-
[visualElement]
52+
[visualElement, externalRef]
4553
)
4654
}

packages/motion-dom/src/render/dom/__tests__/parse-transform.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,6 @@ describe("parseValueFromTransform", () => {
200200
matrix = new CSSMatrix()
201201
matrix = matrix.rotate(0, 0, 60)
202202

203-
console.log(matrix.toString())
204203
expect(
205204
parseValueFromTransform(matrix.toString(), "rotateZ")
206205
).toBeCloseTo(60)

0 commit comments

Comments
 (0)