Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/transition-phase-leave-render.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@react-spring/core': patch
---

Expose `phase === 'leave'` to the `useTransition` render function during the render that runs the leave animation. Previously `t.phase` was only updated to the new phase in a layout effect after render, so the render fn always saw the previous phase (`'enter'`) while a leaving item animated out — and by the time a follow-up render could surface `'leave'`, the transition had expired and been pruned. The render fn now receives a state whose `phase` matches the upcoming animation, so consumers can reliably branch on `state.phase === 'leave'`. Closes #1654.
30 changes: 30 additions & 0 deletions packages/core/src/hooks/useTransition.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,36 @@ describe('useTransition', () => {
expect(rendered).toEqual([1])
})

it('exposes phase === "leave" to the render fn during a normal leave', async () => {
// Regression test for https://github.com/pmndrs/react-spring/issues/1654
// The render function receives the transition state `t` as its third
// argument; consumers read `t.phase` to branch on lifecycle.
const phaseLog: string[] = []
const update = createUpdater(({ args }) => {
const t = toArray(useTransition(...args))[0] as TransitionFn
rendered = t((_, item, state) => {
phaseLog.push(`${item}:${state.phase}`)
return item
}).props.children
return null
})

const props = {
from: { n: 0 },
enter: { n: 1 },
leave: { n: 0 },
}

await update(true, props)
await global.advanceUntilIdle()

phaseLog.length = 0
await update(false, props)
await global.advanceUntilIdle()

expect(phaseLog.some(entry => entry.endsWith(':leave'))).toBe(true)
})

it('should work with both exitBeforeEnter and trail together', async () => {
const props = {
from: { t: 0 },
Expand Down
9 changes: 7 additions & 2 deletions packages/core/src/hooks/useTransition.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@

// Return a `SpringRef` if a deps array was passed.
const ref = useMemo(
() => (propsFn || arguments.length == 3 ? SpringRef() : void 0),

Check warning on line 93 in packages/core/src/hooks/useTransition.tsx

View workflow job for this annotation

GitHub Actions / Style Checks

react-hooks(exhaustive-deps)

React Hook useMemo has a missing dependency: 'propsFn'
[]
)

Expand Down Expand Up @@ -427,8 +427,13 @@
const renderTransitions: TransitionFn = render => (
<>
{transitions.map((t, i) => {
const { springs } = changes.get(t) || t.ctrl
const elem: any = render({ ...springs }, t.item, t, i)
const change = changes.get(t) || exitingTransitions.current.get(t)
const { springs } = change || t.ctrl
// Expose the phase that will be assigned in the upcoming layout
// effect so the render fn sees `leave` during the leave animation,
// not the stale phase from the previous render. See #1654.
const state = change ? { ...t, phase: change.phase } : t
const elem: any = render({ ...springs }, t.item, state, i)

const key = is.str(t.key) || is.num(t.key) ? t.key : t.ctrl.id
const isLegacyReact = React.version < '19.0.0'
Expand Down
Loading