Skip to content

Commit 842890d

Browse files
authored
Ensure appear works using the Transition component (even when used with SSR) (#2646)
* ensure `appear` works in combination with SSR * add appear transition example * update changelog * add scale to appear example * trigger immediate transition once the DOM is ready * ensure React doesn't change the `className` underneath us * handle all base classes We are bypassing React when handling classes in the Transition component. Let's ensure the base classes from the prop are also added correctly. * add missing `base` to tests * simplify `useTransition` hook * add react-hot-toast example * make TS happy * ensure the `classNames` are unique * remove classNames if it results in an empty string This will ensure that we don't end up with `class=""` in the DOM * ensure `unmount` is defaulting to `true` * do not read from `prevShow` in render After fixing the other bugs, this part only caused bugs right now. Even when re-rendering the Transition component while transitioning. Dropping this fixes that behaviour. * extend `appear` demo with appear, show, unmount booleans + a `lazily` one to mimic a conditional render on the client instead of a fresh page refresh.
1 parent 88a0138 commit 842890d

File tree

10 files changed

+395
-22
lines changed

10 files changed

+395
-22
lines changed

packages/@headlessui-react/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
- Disable smooth scrolling when opening/closing `Dialog` components on iOS ([#2635](https://github.com/tailwindlabs/headlessui/pull/2635))
1515
- Don't assume `<Tab />` components are available when setting the next index ([#2642](https://github.com/tailwindlabs/headlessui/pull/2642))
1616
- Fix incorrectly focused `Combobox.Input` component on page load ([#2654](https://github.com/tailwindlabs/headlessui/pull/2654))
17+
- Ensure `appear` works using the `Transition` component (even when used with SSR) ([#2646](https://github.com/tailwindlabs/headlessui/pull/2646))
1718

1819
## [1.7.16] - 2023-07-27
1920

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

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -302,15 +302,14 @@ function TransitionChildFn<TTag extends ElementType = typeof DEFAULT_TRANSITION_
302302
} = props as typeof props
303303
let container = useRef<HTMLElement | null>(null)
304304
let transitionRef = useSyncRefs(container, ref)
305-
let strategy = rest.unmount ? RenderStrategy.Unmount : RenderStrategy.Hidden
305+
let strategy = rest.unmount ?? true ? RenderStrategy.Unmount : RenderStrategy.Hidden
306306

307307
let { show, appear, initial } = useTransitionContext()
308308

309309
let [state, setState] = useState(show ? TreeStates.Visible : TreeStates.Hidden)
310310

311311
let parentNesting = useParentNesting()
312312
let { register, unregister } = parentNesting
313-
let prevShow = useRef<boolean | null>(null)
314313

315314
useEffect(() => register(container), [register, container])
316315

@@ -332,6 +331,7 @@ function TransitionChildFn<TTag extends ElementType = typeof DEFAULT_TRANSITION_
332331
}, [state, container, register, unregister, show, strategy])
333332

334333
let classes = useLatestValue({
334+
base: splitClasses(rest.className),
335335
enter: splitClasses(enter),
336336
enterFrom: splitClasses(enterFrom),
337337
enterTo: splitClasses(enterTo),
@@ -358,11 +358,11 @@ function TransitionChildFn<TTag extends ElementType = typeof DEFAULT_TRANSITION_
358358

359359
// Skipping initial transition
360360
let skip = initial && !appear
361+
let immediate = appear && show && initial
361362

362363
let transitionDirection = (() => {
363364
if (!ready) return 'idle'
364365
if (skip) return 'idle'
365-
if (prevShow.current === show) return 'idle'
366366
return show ? 'enter' : 'leave'
367367
})() as TransitionDirection
368368

@@ -404,6 +404,7 @@ function TransitionChildFn<TTag extends ElementType = typeof DEFAULT_TRANSITION_
404404
}, parentNesting)
405405

406406
useTransition({
407+
immediate,
407408
container,
408409
classes,
409410
direction: transitionDirection,
@@ -422,25 +423,23 @@ function TransitionChildFn<TTag extends ElementType = typeof DEFAULT_TRANSITION_
422423
}),
423424
})
424425

425-
useEffect(() => {
426-
if (!skip) return
427-
428-
if (strategy === RenderStrategy.Hidden) {
429-
prevShow.current = null
430-
} else {
431-
prevShow.current = show
432-
}
433-
}, [show, skip, state])
434-
435426
let theirProps = rest
436427
let ourProps = { ref: transitionRef }
437428

438-
if (appear && show && initial) {
429+
if (immediate) {
439430
theirProps = {
440431
...theirProps,
441432
// Already apply the `enter` and `enterFrom` on the server if required
442433
className: classNames(rest.className, ...classes.current.enter, ...classes.current.enterFrom),
443434
}
435+
} else {
436+
// When we re-render while we are in the middle of the transition, then we should take the
437+
// incoming className and the current classes that are applied.
438+
//
439+
// This is a bit dirty, but we need to make sure React is not applying changes to the class
440+
// attribute while we are transitioning.
441+
theirProps.className = classNames(rest.className, container.current?.className)
442+
if (theirProps.className === '') delete theirProps.className
444443
}
445444

446445
return (
@@ -476,7 +475,7 @@ function TransitionRootFn<TTag extends ElementType = typeof DEFAULT_TRANSITION_C
476475
ref: Ref<HTMLElement>
477476
) {
478477
// @ts-expect-error
479-
let { show, appear = false, unmount, ...theirProps } = props as typeof props
478+
let { show, appear = false, unmount = true, ...theirProps } = props as typeof props
480479
let internalTransitionRef = useRef<HTMLElement | null>(null)
481480
let transitionRef = useSyncRefs(internalTransitionRef, ref)
482481

packages/@headlessui-react/src/components/transitions/utils/transition.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ it('should be possible to transition', async () => {
3030
transition(
3131
element,
3232
{
33+
base: [],
3334
enter: ['enter'],
3435
enterFrom: ['enterFrom'],
3536
enterTo: ['enterTo'],
@@ -87,6 +88,7 @@ it('should wait the correct amount of time to finish a transition', async () =>
8788
transition(
8889
element,
8990
{
91+
base: [],
9092
enter: ['enter'],
9193
enterFrom: ['enterFrom'],
9294
enterTo: ['enterTo'],
@@ -156,6 +158,7 @@ it('should keep the delay time into account', async () => {
156158
transition(
157159
element,
158160
{
161+
base: [],
159162
enter: ['enter'],
160163
enterFrom: ['enterFrom'],
161164
enterTo: ['enterTo'],

packages/@headlessui-react/src/components/transitions/utils/transition.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ function waitForTransition(node: HTMLElement, done: () => void) {
7777
export function transition(
7878
node: HTMLElement,
7979
classes: {
80+
base: string[]
8081
enter: string[]
8182
enterFrom: string[]
8283
enterTo: string[]
@@ -116,6 +117,7 @@ export function transition(
116117

117118
removeClasses(
118119
node,
120+
...classes.base,
119121
...classes.enter,
120122
...classes.enterTo,
121123
...classes.enterFrom,
@@ -124,15 +126,15 @@ export function transition(
124126
...classes.leaveTo,
125127
...classes.entered
126128
)
127-
addClasses(node, ...base, ...from)
129+
addClasses(node, ...classes.base, ...base, ...from)
128130

129131
d.nextFrame(() => {
130-
removeClasses(node, ...from)
131-
addClasses(node, ...to)
132+
removeClasses(node, ...classes.base, ...base, ...from)
133+
addClasses(node, ...classes.base, ...base, ...to)
132134

133135
waitForTransition(node, () => {
134-
removeClasses(node, ...base)
135-
addClasses(node, ...classes.entered)
136+
removeClasses(node, ...classes.base, ...base)
137+
addClasses(node, ...classes.base, ...classes.entered)
136138

137139
return _done()
138140
})

packages/@headlessui-react/src/hooks/use-transition.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,11 @@ import { useIsoMorphicEffect } from './use-iso-morphic-effect'
99
import { useLatestValue } from './use-latest-value'
1010

1111
interface TransitionArgs {
12+
immediate: boolean
1213
container: MutableRefObject<HTMLElement | null>
1314
classes: MutableRefObject<{
15+
base: string[]
16+
1417
enter: string[]
1518
enterFrom: string[]
1619
enterTo: string[]
@@ -26,12 +29,25 @@ interface TransitionArgs {
2629
onStop: MutableRefObject<(direction: TransitionArgs['direction']) => void>
2730
}
2831

29-
export function useTransition({ container, direction, classes, onStart, onStop }: TransitionArgs) {
32+
export function useTransition({
33+
immediate,
34+
container,
35+
direction,
36+
classes,
37+
onStart,
38+
onStop,
39+
}: TransitionArgs) {
3040
let mounted = useIsMounted()
3141
let d = useDisposables()
3242

3343
let latestDirection = useLatestValue(direction)
3444

45+
useIsoMorphicEffect(() => {
46+
if (!immediate) return
47+
48+
latestDirection.current = 'enter'
49+
}, [immediate])
50+
3551
useIsoMorphicEffect(() => {
3652
let dd = disposables()
3753
d.add(dd.dispose)
Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,15 @@
11
export function classNames(...classes: (false | null | undefined | string)[]): string {
2-
return classes.filter(Boolean).join(' ')
2+
return Array.from(
3+
new Set(
4+
classes.flatMap((value) => {
5+
if (typeof value === 'string') {
6+
return value.split(' ')
7+
}
8+
9+
return []
10+
})
11+
)
12+
)
13+
.filter(Boolean)
14+
.join(' ')
315
}

packages/playground-react/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"react": "^18.0.0",
2828
"react-dom": "^18.0.0",
2929
"react-flatpickr": "^3.10.9",
30+
"react-hot-toast": "2.3.0",
3031
"tailwindcss": "^3.2.7"
3132
},
3233
"devDependencies": {

0 commit comments

Comments
 (0)