Skip to main content

Server-Side Rendering

Guidelines and patterns for SSR and hydration safety in Moraine and SolidStart.

View as Markdown

Moraine supports SolidJS server rendering and hydration. Your application must still render the same initial state on the server and client, defer browser-only work, and create conditional JSX only when its branch renders.

This guide covers IDs, portals, responsive state, and content ownership.

Deterministic Hydration IDs#

Accessible components require matching IDs between elements (for example, connecting a trigger’s aria-controls to a content panel’s id). When server rendering, randomly generated IDs could differ between server and client, causing hydration mismatches.

Use createId from moraine/utils to generate matching IDs across server rendering and client hydration:

import { createId } from 'moraine/utils'

function CustomField() {
  const id = createId() // Generates stable, synchronized IDs across SSR and hydration

  return (
    <div>
      <label for={id()}>Username</label>
      <input id={id()} type="text" />
    </div>
  )
}

Hydration-Safe Portals#

Floating overlays—such as Dialog, Sheet, Popover, Tooltip, and ContextMenu—frequently render into document.body via a Solid <Portal>.

In Moraine:

  • Overlays default to closed states, rendering nothing or minimal placeholders during initial server rendering.
  • When an overlay opens, Moraine delays portal insertion until hydration completes in the browser, preventing server/client DOM mismatch.
  • Read browser APIs in onMount or event handlers. For browser-only content, keep the server and initial client tree identical, then reveal that content after mounting.
import { Button, Dialog } from 'moraine'

export function UserDialog() {
  return (
    <Dialog>
      <Dialog.Trigger as={Button}>Edit Profile</Dialog.Trigger>
      {/* Content safely portals to body on client without hydration error */}
      <Dialog.Content title="Edit Profile">
        <p>Update your profile details below.</p>
      </Dialog.Content>
    </Dialog>
  )
}

Conditional JSX and item content#

Create JSX inside the branch that renders it. For collection items with optional panels, use a content getter so an inactive panel does not consume hydration IDs before it mounts:

import { Tabs } from 'moraine'

export function SettingsTabs() {
  return (
    <Tabs
      items={[
        {
          value: 'account',
          label: 'Account',
          get content() {
            return <p>Account settings</p>
          },
        },
        {
          value: 'notifications',
          label: 'Notifications',
          get content() {
            return <p>Notification settings</p>
          },
        },
      ]}
    />
  )
}

The same rule applies to JSX panel data in Accordion and Stepper. Keep server and client item order and initial selection equal.

Responsive Queries in SSR#

Browser APIs like window.matchMedia do not exist on the server. If a component conditionally renders layout based on screen width, it must provide a deterministic server default.

Moraine’s createMediaQuery accepts a defaultValue to define the fallback state during SSR:

import { createMediaQuery } from 'moraine/utils'
import { Show } from 'solid-js'

export function Navigation() {
  // Defaults to false on the server; synchronizes with matchMedia on the client
  const isDesktop = createMediaQuery('(min-width: 1024px)', false)

  return (
    <nav>
      <Show when={isDesktop()} fallback={<MobileDrawer />}>
        <DesktopNav />
      </Show>
    </nav>
  )
}

Transition Presence & Exit Lifecycles#

SolidJS conditional <Show> primitives unmount elements immediately when their condition becomes false. Moraine components use createTransitionPresence internally to keep exiting overlays mounted while CSS exit animations run.

During server rendering:

  • Hidden or transitioning elements maintain deterministic closed states.
  • Style attributes and animation variables (such as --mo-collapsible-content-height) resolve gracefully to safe initial values without triggering layout thrashing or browser DOM measurement calls.

Best Practices for SSR Safety#

  1. Avoid window or document in Component Body: Read window dimensions, localStorage, or DOM properties inside onMount or inside user event handlers (onClick, onInput), never at component execution time.
  2. Deterministic Initial State: For controlled signals, initialize with static defaults that the server can evaluate consistently.
  3. Use Moraine Primitives for Overlays: Built-in primitives already manage portal scopes, scroll locks, and focus traps so you do not need custom SSR guard wrappers.