Skip to content

fix: Render the Portal probe span during SSR - #249

Merged
NathanZlion merged 1 commit into
cloudscape-design:mainfrom
TrevorBurnham:fix-portal-ssr-hydration
Aug 18, 2026
Merged

fix: Render the Portal probe span during SSR#249
NathanZlion merged 1 commit into
cloudscape-design:mainfrom
TrevorBurnham:fix-portal-ssr-hydration

Conversation

@TrevorBurnham

@TrevorBurnham TrevorBurnham commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Description

Fixes AWSUI-62219.

Portal renders a hidden probe span on its first render, so the layout effect can read ref.current.ownerDocument and find the right document inside an iframe. That span was added in #214 and gated on typeof document !== 'undefined':

if (!activeContainer && typeof document !== 'undefined') {
  return <span ref={ref} style={{ display: 'none' }} />;
}

activeContainer is populated in a layout effect, so it's null on the first render. On the client that returns the span, but during SSR the guard fails and the component returns null. That causes React to emit th eerror

Hydration failed because the server rendered HTML didn't match the client

and discard and re-renders the subtree.

This affects every SSR consumer that renders a portal, including ones with no application-level workaround. Two paths in @cloudscape-design/components:

  • A Modal with visible={false}, which many apps mount unconditionally.
  • A Table with resizableColumns, via StickyHeaderResizerDragHandleWrapperPortalOverlayPortal.

Before #214, the only return was activeContainer && createPortal(children, activeContainer), which yielded null on both passes.

The fix here drops the typeof document condition so the span renders during SSR too, and both passes agree. The cost is one display: none span in the server HTML, replaced by the portal on the first commit. The iframe behavior #214 added is unchanged: the effect still reads the ref.

How has this been reproduced and tested?

With any React 18+ SSR setup:

// server
import { renderToString } from 'react-dom/server';
import Modal from '@cloudscape-design/components/modal';

renderToString(<Modal visible={false} header="Title">Body</Modal>);

// client — hydrating that markup
hydrateRoot(container, <Modal visible={false} header="Title">Body</Modal>);

Before, React logs the mismatch and shows an added hidden span under Portal in the diff. After, there's no mismatch.

This PR adds unit test coverage to prevent regressions: src/internal/portal/__tests__/portal.ssr.test.tsx.

The probe span added in cloudscape-design#214 was gated on `typeof document !== 'undefined'`,
so the server rendered nothing while the client's first render rendered the
span. React reports that as a hydration mismatch and discards the subtree.

Rendering the span on both passes makes the markup agree. It is display:none
and is replaced by the portal on the first commit.

Fixes AWSUI-62219
@TrevorBurnham
TrevorBurnham marked this pull request as ready for review August 15, 2026 22:13
@TrevorBurnham
TrevorBurnham requested a review from a team as a code owner August 15, 2026 22:13
@TrevorBurnham
TrevorBurnham requested review from NathanZlion and removed request for a team August 15, 2026 22:13
@TrevorBurnham

Copy link
Copy Markdown
Contributor Author

cloudscape-design/components#4908 should be merged first to prevent tests from breaking downstream.

@codecov

codecov Bot commented Aug 17, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.09%. Comparing base (d7cc26a) to head (98ff8b6).

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #249   +/-   ##
=======================================
  Coverage   98.09%   98.09%           
=======================================
  Files          56       56           
  Lines        1787     1787           
  Branches      517      495   -22     
=======================================
  Hits         1753     1753           
  Misses         34       34           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@TrevorBurnham

Copy link
Copy Markdown
Contributor Author

I believe the dry run failure ("Components unit tests") is because cloudscape-design/components#4908 hasn't been merged yet.

@NathanZlion
NathanZlion enabled auto-merge August 18, 2026 15:55
@NathanZlion
NathanZlion added this pull request to the merge queue Aug 18, 2026
Merged via the queue into cloudscape-design:main with commit 744ad19 Aug 18, 2026
115 of 118 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants