From 3497306f83a299b2c4505f3da73069520153803d Mon Sep 17 00:00:00 2001 From: lennondotw Date: Fri, 3 Apr 2026 15:10:32 +0800 Subject: [PATCH] Fix AnimatePresence: apply object-form initial on re-entry The re-entry logic added in 6a8d3abb9 only handles string variant labels. Object-form initial values (e.g., initial={{ opacity: 0.5 }}) are skipped, so the component animates from the exit end value instead of jumping to the specified initial. resolveVariant already supports both strings and objects, so extending the type guard is all that's needed. --- CHANGELOG.md | 6 ++ .../__tests__/AnimatePresence.test.tsx | 66 +++++++++++++++++++ .../src/motion/features/animation/exit.ts | 7 +- 3 files changed, 78 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1448129559..25b889718c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ Motion adheres to [Semantic Versioning](http://semver.org/). Undocumented APIs should be considered internal and may change without warning. +## [Unreleased] + +### Fixed + +- `AnimatePresence`: Fix object-form `initial` values not applied on re-entry after exit completes. + ## [12.38.0] 2026-03-16 ### Added diff --git a/packages/framer-motion/src/components/AnimatePresence/__tests__/AnimatePresence.test.tsx b/packages/framer-motion/src/components/AnimatePresence/__tests__/AnimatePresence.test.tsx index cfeda6a164..29a357b154 100644 --- a/packages/framer-motion/src/components/AnimatePresence/__tests__/AnimatePresence.test.tsx +++ b/packages/framer-motion/src/components/AnimatePresence/__tests__/AnimatePresence.test.tsx @@ -1502,6 +1502,72 @@ describe("AnimatePresence with custom components", () => { expect(enterCustomValues).toContain(1) }) + test("Re-entering child with object-form initial resets to initial values when exit was complete", async () => { + const opacity = motionValue(1) + const opacityChanges: number[] = [] + + opacity.on("change", (v) => { + opacityChanges.push(v) + }) + + const Component = ({ + showA, + showB, + }: { + showA: boolean + showB: boolean + }) => { + return ( + + {showA && ( + + )} + {showB && ( + + )} + + ) + } + + const { rerender } = render() + await act(async () => { + await nextFrame() + }) + + await act(async () => { + rerender() + }) + await act(async () => { + await nextFrame() + }) + + expect(opacity.get()).toBe(0) + + opacityChanges.length = 0 + await act(async () => { + rerender() + }) + await act(async () => { + await nextFrame() + }) + + expect(opacityChanges.length).toBeGreaterThan(0) + expect(opacityChanges[0]).toBe(0.5) + }) + test("Does not get stuck when state changes cause rapid key alternation in mode='wait'", async () => { /** * Reproduction from #3141: A loading/loaded pattern where diff --git a/packages/framer-motion/src/motion/features/animation/exit.ts b/packages/framer-motion/src/motion/features/animation/exit.ts index 5872216ced..a5d3c8b058 100644 --- a/packages/framer-motion/src/motion/features/animation/exit.ts +++ b/packages/framer-motion/src/motion/features/animation/exit.ts @@ -25,7 +25,12 @@ export class ExitAnimationFeature extends Feature { if (this.isExitComplete) { const { initial, custom } = this.node.getProps() - if (typeof initial === "string") { + if ( + typeof initial === "string" || + (typeof initial === "object" && + initial !== null && + !Array.isArray(initial)) + ) { const resolved = resolveVariant( this.node, initial,