Skip to main content

SSR safety

Kalyx is designed for server rendering. Every picker has a renderToString test in the component test suite, and the published entries carry the 'use client' directive for the Next.js App Router and other RSC setups.

What Kalyx does​

  • Stable IDs via React's useId(), so they match between server and client.
  • No window/document at module scope. All DOM access is inside useEffect.
  • No useLayoutEffect in hot paths, which avoids the SSR warning.
  • Floating UI handles positioning with its SSR-safe useFloating hook.

In short: importing @kalyx/react on a server is safe.

Usage with Next.js App Router​

Kalyx components use React Context and interactivity, so they must render in a client component.

// app/booking/date-field.tsx
'use client';

import { useState } from 'react';
import { DatePicker, type ISODateString } from '@kalyx/react';

export function DateField() {
const [date, setDate] = useState<ISODateString | null>(null);

return (
<DatePicker value={date} onChange={setDate}>
<DatePicker.Input />
<DatePicker.Popover>
<DatePicker.Calendar />
</DatePicker.Popover>
</DatePicker>
);
}

Then consume it from a server component:

// app/booking/page.tsx
import { DateField } from './date-field';

export default function Page() {
return (
<main>
<h1>Book a date</h1>
<DateField />
</main>
);
}

RSC (React Server Components)​

Kalyx components are client components, and the published bundle already carries the 'use client' directive. packages/react/tsup.config.ts prepends it to every built entry (dist/index.js, dist/index.cjs, dist/headless.js, dist/headless.cjs). You can import a picker directly inside a Server Component file's module graph without writing a boundary of your own.

A wrapper module is therefore optional. It is still worth writing when you want to co-locate app-specific defaults:

// components/ui/date-field.tsx
'use client';
export { DatePicker } from '@kalyx/react';

Uncontrolled rendering on the server​

In SSR, the initial render has no user interaction yet. Start with either defaultValue or value={null}:

{/* Uncontrolled: good for forms */}
<DatePicker defaultValue="2026-04-15T00:00:00.000Z">
<DatePicker.Input name="checkIn" />
</DatePicker>

{/* Controlled with null: good for optional fields */}
<DatePicker value={null} onChange={setDate}>
<DatePicker.Input />
</DatePicker>

Hydration caveats​

If your displayFormat or locale differ between server and client (for example, reading navigator.language at render time), you'll see hydration mismatches. Determine the locale before rendering:

// Read locale from cookies / session server-side
<DatePicker locale={cookieLocale} value={iso} onChange={setIso}>
...
</DatePicker>

Avoid reading navigator, window.matchMedia, or Intl.DateTimeFormat().resolvedOptions() in the render body. Read them inside useEffect and defer rendering if needed.

Testing SSR​

Add a smoke test that mirrors production rendering:

import { renderToString } from 'react-dom/server';
import { DatePicker } from '@kalyx/react';

it('renders on the server without throwing', () => {
const html = renderToString(
<DatePicker defaultValue="2026-04-15T00:00:00.000Z">
<DatePicker.Input />
<DatePicker.Popover>
<DatePicker.Calendar />
</DatePicker.Popover>
</DatePicker>,
);
expect(html).toContain('input');
});

Next​