Next.js and server rendering
Fetch events in a Server Component and render the same calendar on the server and in the browser.
Time works with any React 19 framework that renders on the server. This guide uses the Next.js App Router. The same rules apply elsewhere.
Calendars are Client Components
Calendar state lives in React state, so createCalendar, defineCalendarView, and the components
are client-only. Put your calendar, its views, and Root in files that start with "use client",
as the Quick start does. Server Components can render those components and pass
them props.
The rest of @moltenship/calendar works on the server too. A Server Component can import the
Temporal helpers or build a snapshot, for example to filter events before sending them.
Send strings, not Temporal objects
Props from a Server Component to a Client Component must be serializable. Temporal objects aren't, so send ISO 8601 strings and turn them into Temporal values in the client:
// app/calendar/team-calendar.tsx
"use client";
import { useMemo } from "react";
import { Calendar, weekRules, type Meeting } from "./quick-start";
// A Meeting with its times as ISO 8601 strings.
export type MeetingJson = Omit<Meeting, "start" | "end"> & {
readonly start: string;
readonly end: string;
};
interface TeamCalendarProps {
readonly meetings: readonly MeetingJson[];
readonly now: string;
readonly timeZone: string;
}
export function TeamCalendar({ meetings, now, timeZone }: TeamCalendarProps) {
const events = useMemo(
() =>
meetings.map((meeting): Meeting => ({
...meeting,
start: Temporal.Instant.from(meeting.start),
end: Temporal.Instant.from(meeting.end),
})),
[meetings],
);
const nowInstant = Temporal.Instant.from(now);
return (
<Calendar.Root
events={events}
now={nowInstant}
defaultAnchorDate={nowInstant.toZonedDateTimeISO(timeZone).toPlainDate()}
defaultTimeZone={timeZone}
locale="en-US"
weekRules={weekRules}
>
<Calendar.Frame>
<Calendar.Toolbar>
<Calendar.PreviousTrigger>Previous</Calendar.PreviousTrigger>
<Calendar.TodayTrigger>Today</Calendar.TodayTrigger>
<Calendar.NextTrigger>Next</Calendar.NextTrigger>
<Calendar.Title />
</Calendar.Toolbar>
<Calendar.Viewport>
<Calendar.ActiveView />
</Calendar.Viewport>
</Calendar.Frame>
</Calendar.Root>
);
}useMemo keeps the converted array stable between renders, so the calendar only re-reads events
when meetings actually changes.
The page fetches the data on the server:
// app/calendar/page.tsx (a Server Component)
import { TeamCalendar, type MeetingJson } from "./team-calendar";
async function getMeetings(): Promise<readonly MeetingJson[]> {
// Replace with your database or API call. Return ISO 8601 strings, not Temporal objects.
return [
{
id: "standup",
title: "Standup",
color: "blue",
start: "2026-10-08T07:00:00Z",
end: "2026-10-08T07:15:00Z",
},
];
}
export default async function CalendarPage() {
const meetings = await getMeetings();
return (
<TeamCalendar
meetings={meetings}
now={Temporal.Now.instant().toString()}
timeZone="Europe/Berlin"
/>
);
}Match the server and the browser
During server rendering and hydration, Time doesn't read the clock, the host time zone, or window.
Everything that affects the first render comes from props, so the server and the browser produce the
same markup as long as they get the same props. In practice:
- Compute "now" once on the server and pass it down, as
nowabove. CallingTemporal.Now.instant()in a Client Component gives the server and the browser different values. Withoutnow, anything that depends on the current time (a view'stoday,useNow()) is empty on the server and fills in after hydration. - Choose the time zone on the server, for example from the user's profile or a cookie. The server's own zone is rarely the user's.
- Pass the same
localeto both. Dates and times are formatted withIntl, which depends on it.
If you only learn the user's time zone in the browser, render the calendar after hydration, or pass
the zone as a controlled timeZone and update it once you know it.
Element IDs
Root derives element IDs from React's useId, so they match between server and browser. If you
render several separate React roots on one page, give each a distinct identifierPrefix when you
create it (on both server and client), or pass an explicit id to each Root.
Last updated on