Revert "Fix hydration of components inside <Suspense> (#2633)"

This reverts commit 35012893e9.
This commit is contained in:
Jordan Pittman
2023-08-02 10:35:32 -04:00
parent 35012893e9
commit 34275dae74
9 changed files with 123 additions and 37 deletions
-1
View File
@@ -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 -23
View File
@@ -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
}