Skip to content

Commit

Permalink
Fix Portal SSR hydration mismatches (#2700)
Browse files Browse the repository at this point in the history
* Register portal based on element presence in the DOM

This always coincides with `onMounted` currently but that’s about to change

* Mount element lazily for portals

This prevent’s SSR hydration issues and matches the behavior of React’s `<Portal>` element

* Fix portal tests

* Update comment

* Update changelog
  • Loading branch information
thecrypticace committed Aug 23, 2023
1 parent 5a3d556 commit 6444e01
Show file tree
Hide file tree
Showing 3 changed files with 25 additions and 5 deletions.
1 change: 1 addition & 0 deletions packages/@headlessui-vue/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed

- Don't call `<Dialog>`'s `onClose` twice on mobile devices ([#2690](https://github.com/tailwindlabs/headlessui/pull/2690))
- Fix Portal SSR hydration mismatches ([#2700](https://github.com/tailwindlabs/headlessui/pull/2700))

## [1.7.16] - 2023-08-17

Expand Down
10 changes: 8 additions & 2 deletions packages/@headlessui-vue/src/components/portal/portal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ it('SSR-rendering a Portal should not error', async () => {
expect(result).toBe(html`<main id="parent"><!----></main>`)
})

it('should be possible to use a Portal', () => {
it('should be possible to use a Portal', async () => {
expect(getPortalRoot()).toBe(null)

renderTemplate(
Expand All @@ -112,6 +112,8 @@ it('should be possible to use a Portal', () => {
`
)

await nextTick()

let parent = document.getElementById('parent')
let content = document.getElementById('content')

Expand All @@ -125,7 +127,7 @@ it('should be possible to use a Portal', () => {
expect(content).toHaveTextContent('Contents...')
})

it('should be possible to use multiple Portal elements', () => {
it('should be possible to use multiple Portal elements', async () => {
expect(getPortalRoot()).toBe(null)

renderTemplate(
Expand All @@ -142,6 +144,8 @@ it('should be possible to use multiple Portal elements', () => {
`
)

await nextTick()

let parent = document.getElementById('parent')
let content1 = document.getElementById('content1')
let content2 = document.getElementById('content2')
Expand Down Expand Up @@ -284,6 +288,8 @@ it('should be possible to render multiple portals at the same time', async () =>
},
})

await nextTick()

expect(getPortalRoot()).not.toBe(null)
expect(getPortalRoot().children).toHaveLength(3)

Expand Down
19 changes: 16 additions & 3 deletions packages/@headlessui-vue/src/components/portal/portal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
InjectionKey,
PropType,
Ref,
watch,
} from 'vue'
import { render } from '../../utils/render'
import { usePortalRoot } from '../../internal/portal-force-root'
Expand Down Expand Up @@ -63,19 +64,30 @@ export let Portal = defineComponent({
: groupContext.resolveTarget()
)

let ready = ref(false)
onMounted(() => {
ready.value = true
})

watchEffect(() => {
if (forcePortalRoot) return
if (groupContext == null) return
myTarget.value = groupContext.resolveTarget()
})

let parent = inject(PortalParentContext, null)
onMounted(() => {

// Since the element is mounted lazily (because of SSR hydration)
// We use `watch` on `element` + a local var rather than
// `onMounted` to ensure registration only happens once
let didRegister = false
watch(element, () => {
if (didRegister) return
if (!parent) return
let domElement = dom(element)
if (!domElement) return
if (!parent) return

onUnmounted(parent.register(domElement))
didRegister = true
})

onUnmounted(() => {
Expand All @@ -89,6 +101,7 @@ export let Portal = defineComponent({
})

return () => {
if (!ready.value) return null
if (myTarget.value === null) return null

let ourProps = {
Expand Down

2 comments on commit 6444e01

@vercel
Copy link

@vercel vercel bot commented on 6444e01 Aug 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

headlessui-vue – ./packages/playground-vue

headlessui-vue-git-main-tailwindlabs.vercel.app
headlessui-vue.vercel.app
headlessui-vue-tailwindlabs.vercel.app

@vercel
Copy link

@vercel vercel bot commented on 6444e01 Aug 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

headlessui-react – ./packages/playground-react

headlessui-react-tailwindlabs.vercel.app
headlessui-react.vercel.app
headlessui-react-git-main-tailwindlabs.vercel.app

Please sign in to comment.