Skip to content

Commit 34275da

Browse files
committed
Revert "Fix hydration of components inside <Suspense> (#2633)"
This reverts commit 3501289.
1 parent 3501289 commit 34275da

File tree

9 files changed

+123
-37
lines changed

9 files changed

+123
-37
lines changed

packages/@headlessui-react/CHANGELOG.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
- Use correct value when resetting `<Listbox multiple>` and `<Combobox multiple>` ([#2626](https://github.com/tailwindlabs/headlessui/pull/2626))
1313
- Render `<MainTreeNode />` in `Popover.Group` component only ([#2634](https://github.com/tailwindlabs/headlessui/pull/2634))
14-
- Fix hydration of components inside `<Suspense>` ([#2633](https://github.com/tailwindlabs/headlessui/pull/2633))
1514

1615
## [1.7.16] - 2023-07-27
1716

packages/@headlessui-react/src/components/dialog/dialog.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import { Portal, useNestedPortals } from '../../components/portal/portal'
3737
import { ForcePortalRoot } from '../../internal/portal-force-root'
3838
import { ComponentDescription, Description, useDescriptions } from '../description/description'
3939
import { useOpenClosed, State } from '../../internal/open-closed'
40+
import { useServerHandoffComplete } from '../../hooks/use-server-handoff-complete'
4041
import { StackProvider, StackMessage } from '../../internal/stack-context'
4142
import { useOutsideClick } from '../../hooks/use-outside-click'
4243
import { useOwnerDocument } from '../../hooks/use-owner'
@@ -204,7 +205,8 @@ function DialogFn<TTag extends ElementType = typeof DEFAULT_DIALOG_TAG>(
204205

205206
let setTitleId = useEvent((id: string | null) => dispatch({ type: ActionTypes.SetTitleId, id }))
206207

207-
let enabled = __demoMode ? false : dialogState === DialogStates.Open
208+
let ready = useServerHandoffComplete()
209+
let enabled = ready ? (__demoMode ? false : dialogState === DialogStates.Open) : false
208210
let hasNestedDialogs = nestedDialogCount > 1 // 1 is the current dialog
209211
let hasParentDialog = useContext(DialogContext) !== null
210212
let [portals, PortalWrapper] = useNestedPortals()
@@ -214,7 +216,7 @@ function DialogFn<TTag extends ElementType = typeof DEFAULT_DIALOG_TAG>(
214216
MainTreeNode,
215217
} = useRootContainers({
216218
portals,
217-
defaultContainers: [() => state.panelRef.current ?? internalDialogRef.current],
219+
defaultContainers: [state.panelRef.current ?? internalDialogRef.current],
218220
})
219221

220222
// If there are multiple dialogs, then you can be the root, the leaf or one

packages/@headlessui-react/src/components/focus-trap/focus-trap.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import React, {
1010

1111
import { Props } from '../../types'
1212
import { forwardRefWithAs, HasDisplayName, RefProp, render } from '../../utils/render'
13+
import { useServerHandoffComplete } from '../../hooks/use-server-handoff-complete'
1314
import { useSyncRefs } from '../../hooks/use-sync-refs'
1415
import { Features as HiddenFeatures, Hidden } from '../../internal/hidden'
1516
import { focusElement, focusIn, Focus, FocusResult } from '../../utils/focus-management'
@@ -81,6 +82,10 @@ function FocusTrapFn<TTag extends ElementType = typeof DEFAULT_FOCUS_TRAP_TAG>(
8182
let focusTrapRef = useSyncRefs(container, ref)
8283
let { initialFocus, containers, features = Features.All, ...theirProps } = props
8384

85+
if (!useServerHandoffComplete()) {
86+
features = Features.None
87+
}
88+
8489
let ownerDocument = useOwnerDocument(container)
8590

8691
useRestoreFocus({ ownerDocument }, Boolean(features & Features.RestoreFocus))

packages/@headlessui-react/src/components/portal/portal.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { Props } from '../../types'
1919
import { forwardRefWithAs, RefProp, HasDisplayName, render } from '../../utils/render'
2020
import { useIsoMorphicEffect } from '../../hooks/use-iso-morphic-effect'
2121
import { usePortalRoot } from '../../internal/portal-force-root'
22+
import { useServerHandoffComplete } from '../../hooks/use-server-handoff-complete'
2223
import { optionalRef, useSyncRefs } from '../../hooks/use-sync-refs'
2324
import { useOnUnmount } from '../../hooks/use-on-unmount'
2425
import { useOwnerDocument } from '../../hooks/use-owner'
@@ -90,6 +91,7 @@ function PortalFn<TTag extends ElementType = typeof DEFAULT_PORTAL_TAG>(
9091
env.isServer ? null : ownerDocument?.createElement('div') ?? null
9192
)
9293
let parent = useContext(PortalParentContext)
94+
let ready = useServerHandoffComplete()
9395

9496
useIsoMorphicEffect(() => {
9597
if (!target || !element) return
@@ -121,9 +123,7 @@ function PortalFn<TTag extends ElementType = typeof DEFAULT_PORTAL_TAG>(
121123
}
122124
})
123125

124-
let [isFirstRender, setIsFirstRender] = useState(true)
125-
useEffect(() => setIsFirstRender(false), [])
126-
if (isFirstRender) return null
126+
if (!ready) return null
127127

128128
let ourProps = { ref: portalRef }
129129

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

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,27 @@ describe('Setup API', () => {
155155
</span>
156156
`)
157157
})
158+
159+
it(
160+
'should yell at us when we forget to forward the ref when using a render prop',
161+
suppressConsoleLogs(() => {
162+
expect.assertions(1)
163+
164+
function Dummy(props: any) {
165+
return <span {...props}>Children</span>
166+
}
167+
168+
expect(() => {
169+
render(
170+
<Transition show={true} as={Fragment}>
171+
{() => <Dummy />}
172+
</Transition>
173+
)
174+
}).toThrowErrorMatchingInlineSnapshot(
175+
`"Did you forget to passthrough the \`ref\` to the actual DOM node?"`
176+
)
177+
})
178+
)
158179
})
159180

160181
describe('nested', () => {
@@ -328,6 +349,58 @@ describe('Setup API', () => {
328349
</div>
329350
`)
330351
})
352+
353+
it(
354+
'should yell at us when we forgot to forward the ref on one of the Transition.Child components',
355+
suppressConsoleLogs(() => {
356+
expect.assertions(1)
357+
358+
function Dummy(props: any) {
359+
return <div {...props} />
360+
}
361+
362+
expect(() => {
363+
render(
364+
<div className="My Page">
365+
<Transition show={true}>
366+
<Transition.Child as={Fragment}>{() => <Dummy>Sidebar</Dummy>}</Transition.Child>
367+
<Transition.Child as={Fragment}>{() => <Dummy>Content</Dummy>}</Transition.Child>
368+
</Transition>
369+
</div>
370+
)
371+
}).toThrowErrorMatchingInlineSnapshot(
372+
`"Did you forget to passthrough the \`ref\` to the actual DOM node?"`
373+
)
374+
})
375+
)
376+
377+
it(
378+
'should yell at us when we forgot to forward a ref on the Transition component',
379+
suppressConsoleLogs(() => {
380+
expect.assertions(1)
381+
382+
function Dummy(props: any) {
383+
return <div {...props} />
384+
}
385+
386+
expect(() => {
387+
render(
388+
<div className="My Page">
389+
<Transition show={true} as={Fragment}>
390+
{() => (
391+
<Dummy>
392+
<Transition.Child>{() => <aside>Sidebar</aside>}</Transition.Child>
393+
<Transition.Child>{() => <section>Content</section>}</Transition.Child>
394+
</Dummy>
395+
)}
396+
</Transition>
397+
</div>
398+
)
399+
}).toThrowErrorMatchingInlineSnapshot(
400+
`"Did you forget to passthrough the \`ref\` to the actual DOM node?"`
401+
)
402+
})
403+
)
331404
})
332405

333406
describe('transition classes', () => {

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { match } from '../../utils/match'
2727
import { useIsMounted } from '../../hooks/use-is-mounted'
2828
import { useIsoMorphicEffect } from '../../hooks/use-iso-morphic-effect'
2929
import { useLatestValue } from '../../hooks/use-latest-value'
30+
import { useServerHandoffComplete } from '../../hooks/use-server-handoff-complete'
3031
import { useSyncRefs } from '../../hooks/use-sync-refs'
3132
import { useTransition } from '../../hooks/use-transition'
3233
import { useEvent } from '../../hooks/use-event'
@@ -347,10 +348,19 @@ function TransitionChildFn<TTag extends ElementType = typeof DEFAULT_TRANSITION_
347348
afterLeave,
348349
})
349350

351+
let ready = useServerHandoffComplete()
352+
353+
useEffect(() => {
354+
if (ready && state === TreeStates.Visible && container.current === null) {
355+
throw new Error('Did you forget to passthrough the `ref` to the actual DOM node?')
356+
}
357+
}, [container, state, ready])
358+
350359
// Skipping initial transition
351360
let skip = initial && !appear
352361

353362
let transitionDirection = (() => {
363+
if (!ready) return 'idle'
354364
if (skip) return 'idle'
355365
if (prevShow.current === show) return 'idle'
356366
return show ? 'enter' : 'leave'
@@ -470,6 +480,9 @@ function TransitionRootFn<TTag extends ElementType = typeof DEFAULT_TRANSITION_C
470480
let internalTransitionRef = useRef<HTMLElement | null>(null)
471481
let transitionRef = useSyncRefs(internalTransitionRef, ref)
472482

483+
// The TransitionChild will also call this hook, and we have to make sure that we are ready.
484+
useServerHandoffComplete()
485+
473486
let usesOpenClosedState = useOpenClosed()
474487

475488
if (show === undefined && usesOpenClosedState !== null) {
Lines changed: 1 addition & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React from 'react'
22
import { useIsoMorphicEffect } from './use-iso-morphic-effect'
3+
import { useServerHandoffComplete } from './use-server-handoff-complete'
34
import { env } from '../utils/env'
45

56
// We used a "simple" approach first which worked for SSR and rehydration on the client. However we
@@ -22,26 +23,3 @@ export let useId =
2223

2324
return id != null ? '' + id : undefined
2425
}
25-
26-
// NOTE: Do NOT use this outside of the `useId` hook
27-
// It is not compatible with `<Suspense>` (which is in React 18 which has its own `useId` hook)
28-
function useServerHandoffComplete() {
29-
let [complete, setComplete] = React.useState(env.isHandoffComplete)
30-
31-
if (complete && env.isHandoffComplete === false) {
32-
// This means we are in a test environment and we need to reset the handoff state
33-
// This kinda breaks the rules of React but this is only used for testing purposes
34-
// And should theoretically be fine
35-
setComplete(false)
36-
}
37-
38-
React.useEffect(() => {
39-
if (complete === true) return
40-
setComplete(true)
41-
}, [complete])
42-
43-
// Transition from pending to complete (forcing a re-render when server rendering)
44-
React.useEffect(() => env.handoff(), [])
45-
46-
return complete
47-
}

packages/@headlessui-react/src/hooks/use-root-containers.tsx

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,12 @@ import { Hidden, Features as HiddenFeatures } from '../internal/hidden'
33
import { useEvent } from './use-event'
44
import { useOwnerDocument } from './use-owner'
55

6-
type Container = HTMLElement | null | MutableRefObject<HTMLElement | null>
7-
type MaybeContainerFn = Container | (() => Container)
8-
96
export function useRootContainers({
107
defaultContainers = [],
118
portals,
129
mainTreeNodeRef: _mainTreeNodeRef,
1310
}: {
14-
defaultContainers?: MaybeContainerFn[]
11+
defaultContainers?: (HTMLElement | null | MutableRefObject<HTMLElement | null>)[]
1512
portals?: MutableRefObject<HTMLElement[]>
1613
mainTreeNodeRef?: MutableRefObject<HTMLElement | null>
1714
} = {}) {
@@ -24,10 +21,6 @@ export function useRootContainers({
2421

2522
// Resolve default containers
2623
for (let container of defaultContainers) {
27-
if (typeof container === 'function') {
28-
container = container()
29-
}
30-
3124
if (container === null) continue
3225
if (container instanceof HTMLElement) {
3326
containers.push(container)
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { useState, useEffect } from 'react'
2+
import { env } from '../utils/env'
3+
4+
export function useServerHandoffComplete() {
5+
let [complete, setComplete] = useState(env.isHandoffComplete)
6+
7+
if (complete && env.isHandoffComplete === false) {
8+
// This means we are in a test environment and we need to reset the handoff state
9+
// This kinda breaks the rules of React but this is only used for testing purposes
10+
// And should theoretically be fine
11+
setComplete(false)
12+
}
13+
14+
useEffect(() => {
15+
if (complete === true) return
16+
setComplete(true)
17+
}, [complete])
18+
19+
// Transition from pending to complete (forcing a re-render when server rendering)
20+
useEffect(() => env.handoff(), [])
21+
22+
return complete
23+
}

0 commit comments

Comments
 (0)