fix(toast): replace useMediaQuery with SSR-safe pattern to avoid hydration mismatch#6513
fix(toast): replace useMediaQuery with SSR-safe pattern to avoid hydration mismatch#6513aosmcleod wants to merge 2 commits into
Conversation
`useMediaQuery` returns `false` during SSR but `true` on mobile clients, causing a hydration mismatch when the action button renders in different DOM positions based on viewport width. Replace with a `useState(false)` + `useEffect` pattern that: - Always returns `false` on the server and first client render (matching SSR output) - Updates to the correct value after hydration via useEffect - Listens for viewport changes via MediaQueryList.addEventListener Fixes heroui-inc#6509
|
@aosmcleod is attempting to deploy a commit to the HeroUI Inc Team on Vercel. A member of the Team first needs to authorize it. |
commit: |
wingkwong
left a comment
There was a problem hiding this comment.
In the narrow sense, it will remove the hydration warnings but it trades with a visual flash. I think CSS approach would be preferable.
Render the action button in both DOM positions and toggle visibility with CSS modifier classes (toast__action--mobile, toast__action--desktop) instead of branching on a JS isMobile state. Eliminates the SSR hydration mismatch and the post-hydration visual flash that the useState/useEffect approach introduced. Aligns the action placement breakpoint with the existing CSS sm: (640px) used by .toast__action and .toast__close-button responsive rules.
|
Please click re-request review once it is ready for review. Thanks. |
|
@wingkwong addressed your feedback — agree the previous Now renders the action button in both DOM positions and uses two new modifier classes ( ```css CSS decides which copy is visible, so the server and client render identically — no hydration mismatch — and there's no post-hydration flash because there's no JS state to wait for. Aligns the breakpoint with the existing CSS ( Ready for another look. |
| /* Responsive placement: action button is rendered in two DOM positions | ||
| * (inside .toast__content for stacked-below layout, and as a sibling of | ||
| * .toast__content for inline layout). CSS — not JS — picks which one is | ||
| * visible, so there is no SSR/CSR mismatch and no post-hydration flash. */ | ||
| .toast__action--mobile { | ||
| @apply sm:hidden; | ||
| } | ||
|
|
||
| .toast__action--desktop { | ||
| @apply max-sm:hidden; | ||
| } |
There was a problem hiding this comment.
The previous JS used (max-width: 768px) while the new CSS uses sm: (640px). Devices in the 640–767px range (tablets in portrait, smaller Chromebooks) used to get the mobile stacked layout.
| </ToastContent> | ||
| {!isMobile && actionProps?.children ? ( | ||
| <ToastActionButton {...actionProps}>{actionProps.children}</ToastActionButton> | ||
| {actionProps?.children ? ( |
There was a problem hiding this comment.
actionProps is spread onto two DOM nodes. There will be some issues like two elements share one id, ref called for both mounts but the second one always wins etc.
You may consider a single-node CSS layout. I think that can be achieved with the existing wrapper plus flex direction + order so that we can just have a single <ToastActionButton> as a sibling of <ToastContent> and don't need --mobile / --desktop modifiers.
Closes #6509
📝 Description
ToastProviderusesuseMediaQuery("(max-width: 768px)")to conditionally render the action button in different DOM positions (insideToastContenton mobile, outside on desktop). During SSR,useMediaQueryreturnsfalsebecausewindow.matchMediais unavailable. On mobile clients, the first render returnstrue, creating a hydration mismatch.⛳️ Current behavior (updates)
useMediaQueryhook is used directly — returnsfalseon server, potentiallytrueon clientToastContentToastContent🚀 New behavior
useMediaQuerywith auseState(false)+useEffectpatternfalseon server AND first client render (matching SSR output)useEffectMediaQueryList.addEventListenerfor responsive behavioruseMediaQueryimport (no longer needed in this file)💣 Is this a breaking change (Yes/No):
No — the action button position may briefly render in desktop layout on mobile before the effect fires, but this is a single-frame flash that's preferable to a hydration error.
📝 Additional Information
A more comprehensive fix would render both positions and use CSS
hidden/sm:flexclasses to toggle visibility without changing DOM structure. That approach avoids even the single-frame flash but requires changes to the toast CSS. This PR takes the minimal approach; happy to switch to the CSS approach if preferred.