Skip to content

Commit 1b3837b

Browse files
Fix React <Transition> flicker issue (#1118)
* Fix React transition bug * use a ref instead of a useCallback (#1108) This allows us to guarantee that the ref is always referencing the latest callback. This also allows us to re-run fewer effects because we don't really care about intermediate callback values, just the last one. * Fix tests * Update changelog Co-authored-by: Robin Malfait <[email protected]>
1 parent a63ca93 commit 1b3837b

File tree

2 files changed

+43
-46
lines changed

2 files changed

+43
-46
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414
- Improve SSR for `Tab` component ([#1155](https://github.com/tailwindlabs/headlessui/pull/1155))
1515
- Fix `hover` scroll ([#1161](https://github.com/tailwindlabs/headlessui/pull/1161))
1616
- Guarantee DOM sort order when performing actions ([#1168](https://github.com/tailwindlabs/headlessui/pull/1168))
17+
- Fix `<Transition>` flickering issue ([#1118](https://github.com/tailwindlabs/headlessui/pull/1118))
1718

1819
## [Unreleased - @headlessui/vue]
1920

packages/@headlessui-react/src/components/transitions/transition.tsx

Lines changed: 42 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import React, {
22
Fragment,
33
createContext,
4-
useCallback,
54
useContext,
65
useEffect,
76
useMemo,
@@ -32,6 +31,7 @@ import { Reason, transition } from './utils/transition'
3231
import { OpenClosedProvider, State, useOpenClosed } from '../../internal/open-closed'
3332
import { useServerHandoffComplete } from '../../hooks/use-server-handoff-complete'
3433
import { useSyncRefs } from '../../hooks/use-sync-refs'
34+
import { useLatestValue } from '../../hooks/use-latest-value'
3535

3636
type ID = ReturnType<typeof useId>
3737

@@ -103,8 +103,8 @@ function useParentNesting() {
103103

104104
interface NestingContextValues {
105105
children: MutableRefObject<{ id: ID; state: TreeStates }[]>
106-
register: (id: ID) => () => void
107-
unregister: (id: ID, strategy?: RenderStrategy) => void
106+
register: MutableRefObject<(id: ID) => () => void>
107+
unregister: MutableRefObject<(id: ID, strategy?: RenderStrategy) => void>
108108
}
109109

110110
let NestingContext = createContext<NestingContextValues | null>(null)
@@ -118,48 +118,38 @@ function hasChildren(
118118
}
119119

120120
function useNesting(done?: () => void) {
121-
let doneRef = useRef(done)
121+
let doneRef = useLatestValue(done)
122122
let transitionableChildren = useRef<NestingContextValues['children']['current']>([])
123123
let mounted = useIsMounted()
124124

125-
useEffect(() => {
126-
doneRef.current = done
127-
}, [done])
128-
129-
let unregister = useCallback(
130-
(childId: ID, strategy = RenderStrategy.Hidden) => {
131-
let idx = transitionableChildren.current.findIndex(({ id }) => id === childId)
132-
if (idx === -1) return
133-
134-
match(strategy, {
135-
[RenderStrategy.Unmount]() {
136-
transitionableChildren.current.splice(idx, 1)
137-
},
138-
[RenderStrategy.Hidden]() {
139-
transitionableChildren.current[idx].state = TreeStates.Hidden
140-
},
141-
})
142-
143-
if (!hasChildren(transitionableChildren) && mounted.current) {
144-
doneRef.current?.()
145-
}
146-
},
147-
[doneRef, mounted, transitionableChildren]
148-
)
125+
let unregister = useLatestValue((childId: ID, strategy = RenderStrategy.Hidden) => {
126+
let idx = transitionableChildren.current.findIndex(({ id }) => id === childId)
127+
if (idx === -1) return
128+
129+
match(strategy, {
130+
[RenderStrategy.Unmount]() {
131+
transitionableChildren.current.splice(idx, 1)
132+
},
133+
[RenderStrategy.Hidden]() {
134+
transitionableChildren.current[idx].state = TreeStates.Hidden
135+
},
136+
})
149137

150-
let register = useCallback(
151-
(childId: ID) => {
152-
let child = transitionableChildren.current.find(({ id }) => id === childId)
153-
if (!child) {
154-
transitionableChildren.current.push({ id: childId, state: TreeStates.Visible })
155-
} else if (child.state !== TreeStates.Visible) {
156-
child.state = TreeStates.Visible
157-
}
158-
159-
return () => unregister(childId, RenderStrategy.Unmount)
160-
},
161-
[transitionableChildren, unregister]
162-
)
138+
if (!hasChildren(transitionableChildren) && mounted.current) {
139+
doneRef.current?.()
140+
}
141+
})
142+
143+
let register = useLatestValue((childId: ID) => {
144+
let child = transitionableChildren.current.find(({ id }) => id === childId)
145+
if (!child) {
146+
transitionableChildren.current.push({ id: childId, state: TreeStates.Visible })
147+
} else if (child.state !== TreeStates.Visible) {
148+
child.state = TreeStates.Visible
149+
}
150+
151+
return () => unregister.current(childId, RenderStrategy.Unmount)
152+
})
163153

164154
return useMemo(
165155
() => ({
@@ -226,6 +216,7 @@ let TransitionChild = forwardRefWithAs(function TransitionChild<
226216

227217
let { show, appear, initial } = useTransitionContext()
228218
let { register, unregister } = useParentNesting()
219+
let prevShow = useRef(undefined)
229220

230221
let id = useId()
231222

@@ -236,14 +227,14 @@ let TransitionChild = forwardRefWithAs(function TransitionChild<
236227
// transitioning ourselves. Otherwise we would unmount before the transitions are finished.
237228
if (!isTransitioning.current) {
238229
setState(TreeStates.Hidden)
239-
unregister(id)
230+
unregister.current(id)
240231
events.current.afterLeave()
241232
}
242233
})
243234

244235
useIsoMorphicEffect(() => {
245236
if (!id) return
246-
return register(id)
237+
return register.current(id)
247238
}, [register, id])
248239

249240
useIsoMorphicEffect(() => {
@@ -258,8 +249,8 @@ let TransitionChild = forwardRefWithAs(function TransitionChild<
258249
}
259250

260251
match(state, {
261-
[TreeStates.Hidden]: () => unregister(id),
262-
[TreeStates.Visible]: () => register(id),
252+
[TreeStates.Hidden]: () => unregister.current(id),
253+
[TreeStates.Visible]: () => register.current(id),
263254
})
264255
}, [state, id, register, unregister, show, strategy])
265256

@@ -290,6 +281,7 @@ let TransitionChild = forwardRefWithAs(function TransitionChild<
290281
let node = container.current
291282
if (!node) return
292283
if (skip) return
284+
if (show === prevShow.current) return
293285

294286
isTransitioning.current = true
295287

@@ -323,7 +315,7 @@ let TransitionChild = forwardRefWithAs(function TransitionChild<
323315
// ourselves.
324316
if (!hasChildren(nesting)) {
325317
setState(TreeStates.Hidden)
326-
unregister(id)
318+
unregister.current(id)
327319
events.current.afterLeave()
328320
}
329321
}
@@ -345,6 +337,10 @@ let TransitionChild = forwardRefWithAs(function TransitionChild<
345337
leaveToClasses,
346338
])
347339

340+
useIsoMorphicEffect(() => {
341+
prevShow.current = show
342+
}, [show])
343+
348344
let propsWeControl = { ref: transitionRef }
349345
let passthroughProps = rest
350346

0 commit comments

Comments
 (0)