Revert "Fix hydration of components inside <Suspense> (#2633)"
This reverts commit 35012893e9.
This commit is contained in:
@@ -11,7 +11,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
- Use correct value when resetting `<Listbox multiple>` and `<Combobox multiple>` ([#2626](https://github.com/tailwindlabs/headlessui/pull/2626))
|
||||
- Render `<MainTreeNode />` in `Popover.Group` component only ([#2634](https://github.com/tailwindlabs/headlessui/pull/2634))
|
||||
- Fix hydration of components inside `<Suspense>` ([#2633](https://github.com/tailwindlabs/headlessui/pull/2633))
|
||||
|
||||
## [1.7.16] - 2023-07-27
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ import { Portal, useNestedPortals } from '../../components/portal/portal'
|
||||
import { ForcePortalRoot } from '../../internal/portal-force-root'
|
||||
import { ComponentDescription, Description, useDescriptions } from '../description/description'
|
||||
import { useOpenClosed, State } from '../../internal/open-closed'
|
||||
import { useServerHandoffComplete } from '../../hooks/use-server-handoff-complete'
|
||||
import { StackProvider, StackMessage } from '../../internal/stack-context'
|
||||
import { useOutsideClick } from '../../hooks/use-outside-click'
|
||||
import { useOwnerDocument } from '../../hooks/use-owner'
|
||||
@@ -204,7 +205,8 @@ function DialogFn<TTag extends ElementType = typeof DEFAULT_DIALOG_TAG>(
|
||||
|
||||
let setTitleId = useEvent((id: string | null) => dispatch({ type: ActionTypes.SetTitleId, id }))
|
||||
|
||||
let enabled = __demoMode ? false : dialogState === DialogStates.Open
|
||||
let ready = useServerHandoffComplete()
|
||||
let enabled = ready ? (__demoMode ? false : dialogState === DialogStates.Open) : false
|
||||
let hasNestedDialogs = nestedDialogCount > 1 // 1 is the current dialog
|
||||
let hasParentDialog = useContext(DialogContext) !== null
|
||||
let [portals, PortalWrapper] = useNestedPortals()
|
||||
@@ -214,7 +216,7 @@ function DialogFn<TTag extends ElementType = typeof DEFAULT_DIALOG_TAG>(
|
||||
MainTreeNode,
|
||||
} = useRootContainers({
|
||||
portals,
|
||||
defaultContainers: [() => state.panelRef.current ?? internalDialogRef.current],
|
||||
defaultContainers: [state.panelRef.current ?? internalDialogRef.current],
|
||||
})
|
||||
|
||||
// If there are multiple dialogs, then you can be the root, the leaf or one
|
||||
|
||||
@@ -10,6 +10,7 @@ import React, {
|
||||
|
||||
import { Props } from '../../types'
|
||||
import { forwardRefWithAs, HasDisplayName, RefProp, render } from '../../utils/render'
|
||||
import { useServerHandoffComplete } from '../../hooks/use-server-handoff-complete'
|
||||
import { useSyncRefs } from '../../hooks/use-sync-refs'
|
||||
import { Features as HiddenFeatures, Hidden } from '../../internal/hidden'
|
||||
import { focusElement, focusIn, Focus, FocusResult } from '../../utils/focus-management'
|
||||
@@ -81,6 +82,10 @@ function FocusTrapFn<TTag extends ElementType = typeof DEFAULT_FOCUS_TRAP_TAG>(
|
||||
let focusTrapRef = useSyncRefs(container, ref)
|
||||
let { initialFocus, containers, features = Features.All, ...theirProps } = props
|
||||
|
||||
if (!useServerHandoffComplete()) {
|
||||
features = Features.None
|
||||
}
|
||||
|
||||
let ownerDocument = useOwnerDocument(container)
|
||||
|
||||
useRestoreFocus({ ownerDocument }, Boolean(features & Features.RestoreFocus))
|
||||
|
||||
@@ -19,6 +19,7 @@ import { Props } from '../../types'
|
||||
import { forwardRefWithAs, RefProp, HasDisplayName, render } from '../../utils/render'
|
||||
import { useIsoMorphicEffect } from '../../hooks/use-iso-morphic-effect'
|
||||
import { usePortalRoot } from '../../internal/portal-force-root'
|
||||
import { useServerHandoffComplete } from '../../hooks/use-server-handoff-complete'
|
||||
import { optionalRef, useSyncRefs } from '../../hooks/use-sync-refs'
|
||||
import { useOnUnmount } from '../../hooks/use-on-unmount'
|
||||
import { useOwnerDocument } from '../../hooks/use-owner'
|
||||
@@ -90,6 +91,7 @@ function PortalFn<TTag extends ElementType = typeof DEFAULT_PORTAL_TAG>(
|
||||
env.isServer ? null : ownerDocument?.createElement('div') ?? null
|
||||
)
|
||||
let parent = useContext(PortalParentContext)
|
||||
let ready = useServerHandoffComplete()
|
||||
|
||||
useIsoMorphicEffect(() => {
|
||||
if (!target || !element) return
|
||||
@@ -121,9 +123,7 @@ function PortalFn<TTag extends ElementType = typeof DEFAULT_PORTAL_TAG>(
|
||||
}
|
||||
})
|
||||
|
||||
let [isFirstRender, setIsFirstRender] = useState(true)
|
||||
useEffect(() => setIsFirstRender(false), [])
|
||||
if (isFirstRender) return null
|
||||
if (!ready) return null
|
||||
|
||||
let ourProps = { ref: portalRef }
|
||||
|
||||
|
||||
@@ -155,6 +155,27 @@ describe('Setup API', () => {
|
||||
</span>
|
||||
`)
|
||||
})
|
||||
|
||||
it(
|
||||
'should yell at us when we forget to forward the ref when using a render prop',
|
||||
suppressConsoleLogs(() => {
|
||||
expect.assertions(1)
|
||||
|
||||
function Dummy(props: any) {
|
||||
return <span {...props}>Children</span>
|
||||
}
|
||||
|
||||
expect(() => {
|
||||
render(
|
||||
<Transition show={true} as={Fragment}>
|
||||
{() => <Dummy />}
|
||||
</Transition>
|
||||
)
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Did you forget to passthrough the \`ref\` to the actual DOM node?"`
|
||||
)
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
describe('nested', () => {
|
||||
@@ -328,6 +349,58 @@ describe('Setup API', () => {
|
||||
</div>
|
||||
`)
|
||||
})
|
||||
|
||||
it(
|
||||
'should yell at us when we forgot to forward the ref on one of the Transition.Child components',
|
||||
suppressConsoleLogs(() => {
|
||||
expect.assertions(1)
|
||||
|
||||
function Dummy(props: any) {
|
||||
return <div {...props} />
|
||||
}
|
||||
|
||||
expect(() => {
|
||||
render(
|
||||
<div className="My Page">
|
||||
<Transition show={true}>
|
||||
<Transition.Child as={Fragment}>{() => <Dummy>Sidebar</Dummy>}</Transition.Child>
|
||||
<Transition.Child as={Fragment}>{() => <Dummy>Content</Dummy>}</Transition.Child>
|
||||
</Transition>
|
||||
</div>
|
||||
)
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Did you forget to passthrough the \`ref\` to the actual DOM node?"`
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
it(
|
||||
'should yell at us when we forgot to forward a ref on the Transition component',
|
||||
suppressConsoleLogs(() => {
|
||||
expect.assertions(1)
|
||||
|
||||
function Dummy(props: any) {
|
||||
return <div {...props} />
|
||||
}
|
||||
|
||||
expect(() => {
|
||||
render(
|
||||
<div className="My Page">
|
||||
<Transition show={true} as={Fragment}>
|
||||
{() => (
|
||||
<Dummy>
|
||||
<Transition.Child>{() => <aside>Sidebar</aside>}</Transition.Child>
|
||||
<Transition.Child>{() => <section>Content</section>}</Transition.Child>
|
||||
</Dummy>
|
||||
)}
|
||||
</Transition>
|
||||
</div>
|
||||
)
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Did you forget to passthrough the \`ref\` to the actual DOM node?"`
|
||||
)
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
describe('transition classes', () => {
|
||||
|
||||
@@ -27,6 +27,7 @@ import { match } from '../../utils/match'
|
||||
import { useIsMounted } from '../../hooks/use-is-mounted'
|
||||
import { useIsoMorphicEffect } from '../../hooks/use-iso-morphic-effect'
|
||||
import { useLatestValue } from '../../hooks/use-latest-value'
|
||||
import { useServerHandoffComplete } from '../../hooks/use-server-handoff-complete'
|
||||
import { useSyncRefs } from '../../hooks/use-sync-refs'
|
||||
import { useTransition } from '../../hooks/use-transition'
|
||||
import { useEvent } from '../../hooks/use-event'
|
||||
@@ -347,10 +348,19 @@ function TransitionChildFn<TTag extends ElementType = typeof DEFAULT_TRANSITION_
|
||||
afterLeave,
|
||||
})
|
||||
|
||||
let ready = useServerHandoffComplete()
|
||||
|
||||
useEffect(() => {
|
||||
if (ready && state === TreeStates.Visible && container.current === null) {
|
||||
throw new Error('Did you forget to passthrough the `ref` to the actual DOM node?')
|
||||
}
|
||||
}, [container, state, ready])
|
||||
|
||||
// Skipping initial transition
|
||||
let skip = initial && !appear
|
||||
|
||||
let transitionDirection = (() => {
|
||||
if (!ready) return 'idle'
|
||||
if (skip) return 'idle'
|
||||
if (prevShow.current === show) return 'idle'
|
||||
return show ? 'enter' : 'leave'
|
||||
@@ -470,6 +480,9 @@ function TransitionRootFn<TTag extends ElementType = typeof DEFAULT_TRANSITION_C
|
||||
let internalTransitionRef = useRef<HTMLElement | null>(null)
|
||||
let transitionRef = useSyncRefs(internalTransitionRef, ref)
|
||||
|
||||
// The TransitionChild will also call this hook, and we have to make sure that we are ready.
|
||||
useServerHandoffComplete()
|
||||
|
||||
let usesOpenClosedState = useOpenClosed()
|
||||
|
||||
if (show === undefined && usesOpenClosedState !== null) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react'
|
||||
import { useIsoMorphicEffect } from './use-iso-morphic-effect'
|
||||
import { useServerHandoffComplete } from './use-server-handoff-complete'
|
||||
import { env } from '../utils/env'
|
||||
|
||||
// We used a "simple" approach first which worked for SSR and rehydration on the client. However we
|
||||
@@ -22,26 +23,3 @@ export let useId =
|
||||
|
||||
return id != null ? '' + id : undefined
|
||||
}
|
||||
|
||||
// NOTE: Do NOT use this outside of the `useId` hook
|
||||
// It is not compatible with `<Suspense>` (which is in React 18 which has its own `useId` hook)
|
||||
function useServerHandoffComplete() {
|
||||
let [complete, setComplete] = React.useState(env.isHandoffComplete)
|
||||
|
||||
if (complete && env.isHandoffComplete === false) {
|
||||
// This means we are in a test environment and we need to reset the handoff state
|
||||
// This kinda breaks the rules of React but this is only used for testing purposes
|
||||
// And should theoretically be fine
|
||||
setComplete(false)
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
if (complete === true) return
|
||||
setComplete(true)
|
||||
}, [complete])
|
||||
|
||||
// Transition from pending to complete (forcing a re-render when server rendering)
|
||||
React.useEffect(() => env.handoff(), [])
|
||||
|
||||
return complete
|
||||
}
|
||||
|
||||
@@ -3,15 +3,12 @@ import { Hidden, Features as HiddenFeatures } from '../internal/hidden'
|
||||
import { useEvent } from './use-event'
|
||||
import { useOwnerDocument } from './use-owner'
|
||||
|
||||
type Container = HTMLElement | null | MutableRefObject<HTMLElement | null>
|
||||
type MaybeContainerFn = Container | (() => Container)
|
||||
|
||||
export function useRootContainers({
|
||||
defaultContainers = [],
|
||||
portals,
|
||||
mainTreeNodeRef: _mainTreeNodeRef,
|
||||
}: {
|
||||
defaultContainers?: MaybeContainerFn[]
|
||||
defaultContainers?: (HTMLElement | null | MutableRefObject<HTMLElement | null>)[]
|
||||
portals?: MutableRefObject<HTMLElement[]>
|
||||
mainTreeNodeRef?: MutableRefObject<HTMLElement | null>
|
||||
} = {}) {
|
||||
@@ -24,10 +21,6 @@ export function useRootContainers({
|
||||
|
||||
// Resolve default containers
|
||||
for (let container of defaultContainers) {
|
||||
if (typeof container === 'function') {
|
||||
container = container()
|
||||
}
|
||||
|
||||
if (container === null) continue
|
||||
if (container instanceof HTMLElement) {
|
||||
containers.push(container)
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { env } from '../utils/env'
|
||||
|
||||
export function useServerHandoffComplete() {
|
||||
let [complete, setComplete] = useState(env.isHandoffComplete)
|
||||
|
||||
if (complete && env.isHandoffComplete === false) {
|
||||
// This means we are in a test environment and we need to reset the handoff state
|
||||
// This kinda breaks the rules of React but this is only used for testing purposes
|
||||
// And should theoretically be fine
|
||||
setComplete(false)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (complete === true) return
|
||||
setComplete(true)
|
||||
}, [complete])
|
||||
|
||||
// Transition from pending to complete (forcing a re-render when server rendering)
|
||||
useEffect(() => env.handoff(), [])
|
||||
|
||||
return complete
|
||||
}
|
||||
Reference in New Issue
Block a user