Dev.to WebDev 🛠 Dev 👁 0 📖 6 min read

Our three clock components return null on the server on purpose, and one of the three has no importers

Nakodo emails creators on a brand's behalf, and it sends those emails in the creator's working hours, not the brand's. That is published on how it works: weekdays between 9am and 7:30pm where the recipient is. So a scree

Nakodo emails creators on a brand's behalf, and it sends those emails in the creator's working hours, not the brand's. That is published on how it works: weekdays between 9am and 7:30pm where the recipient is. So a screen in the app has to answer a question with two correct answers in it. When is this going out? Tomorrow evening for you, early afternoon for them.

Which means the server cannot render that sentence, because the server does not know what time it is where you are.

src/components/local-time.tsx is 78 lines and three components for exactly this: a time, a send time, and a countdown. This post is all three, including the one that nothing imports.

The pattern: a store that never changes

Every one of the three is this shape:

const noSubscribe = () => () => {};

export function LocalTime({ iso, fallback }: { iso: string; fallback: string }) {
  const text = useSyncExternalStore(
    noSubscribe,
    () => {
      const d = new Date(iso);
      return new Intl.DateTimeFormat(undefined, {
        weekday: sameDay(d, new Date()) ? undefined : "short",
        hour: "numeric",
        minute: "2-digit",
      }).format(d);
    },
    () => null,
  );
  return <time dateTime={iso}>{text ?? fallback}</time>;
}

useSyncExternalStore takes a subscribe function, a client snapshot and a server snapshot. The usual reason to reach for it is a store outside React. Here there is no store at all: noSubscribe subscribes to nothing and returns a no-op unsubscribe. What I actually want is the third argument.

The server snapshot is () => null, so the server renders fallback and the browser renders the real thing. React treats the two renders as deliberately different instead of a hydration mismatch, which is the whole trick. The alternatives I have written before, in order of how much I regret them:

  • suppressHydrationWarning, which silences the warning rather than answering it, and which silences the next one too.
  • useState(null) plus a useEffect that sets the real value, which works and costs an extra render and four more lines.
  • Rendering the time on the server in UTC and hoping nobody notices. Somebody notices.

Intl.DateTimeFormat(undefined, ...) with no locale is the other half: the reader's browser decides whether 19:30 is 19:30 or 7:30 PM, and we never store a preference for that anywhere.

The element is a <time dateTime={iso}>, so the machine-readable instant is in the markup either way, including in the server HTML a crawler sees.

The sentence that names two time zones

SendTime is the one that earns the file. It takes the instant and the recipient's IANA zone, and prints one sentence that is right in both places:

function sendTime(iso: string, there: string): string {
  const d = new Date(iso);
  const day = calendarDay(d);
  const days = Math.round((Date.parse(day) - Date.parse(calendarDay(new Date()))) / 86_400_000);
  const time = clock().format(d);
  const when =
    days <= 0
      ? `at ${time}`
      : days === 1
        ? `tomorrow at ${time}`
        : `on ${new Intl.DateTimeFormat(undefined, days < 7 ? { weekday: "long" } : { day: "numeric", month: "short" }).format(d)} at ${time}`;

calendarDay formats with en-CA, which gives 2026-11-07. That is not a stylistic choice: two of those strings can be subtracted through Date.parse to get a whole number of days without either one being affected by daylight saving, and comparing them as strings answers "is this the same day" with no arithmetic at all. "Tomorrow" has to mean the next calendar day, not 24 hours away, and on the Sunday a clock goes back those are not the same question.

Then the second zone, only when it would differ:

  const theirDay = calendarDay(d, there);
  const theirTime = clock(there).format(d);
  if (theirDay === day && theirTime === time) return when;
  const weekday = theirDay === day ? "" : `${new Intl.DateTimeFormat(undefined, { timeZone: there, weekday: "short" }).format(d)} `;
  const place = there === "UTC" ? "UTC" : `in ${there.split("/").at(-1)!.replaceAll("_", " ")}`;
  return `${when} (${weekday}${theirTime} ${place})`;
}

Three things worth pointing at.

The equality check is on the rendered strings, not on offsets. If the reader and the recipient see the same wall clock and the same calendar day, there is nothing to add, and that is true for a brand in London writing to someone in Lisbon even though the zones differ.

The place name comes out of the zone id: America/New_York becomes in New York, Europe/Paris becomes in Paris, and UTC is special-cased because "in UTC" reads like a place that does not exist. It is a cheap trick and it fails for America/Argentina/Buenos_Aires, which becomes in Buenos Aires, which is fine, and for Asia/Kolkata, which is correct but will look odd to anyone who still types Calcutta.

The other weekday only appears when the days disagree. Goes out tomorrow at 7:30 PM (1:30 PM in New York) has one weekday implied; Goes out tomorrow at 7:30 PM (Sun 3:30 AM in Tokyo) needs the other one spelled out or it is a lie by omission.

The countdown that refreshes the page for all the others

The third component counts down to an instant, and when it gets there it asks the page to refetch itself, so whatever was waiting shows its new state.

const everyFewSeconds = (tick: () => void) => {
  const t = setInterval(tick, 10_000);
  return () => clearInterval(t);
};
let refreshedAt = 0;

export function Countdown({ iso, fallback }: { iso: string; fallback: string }) {
  const router = useRouter();
  const text = useSyncExternalStore(everyFewSeconds, () => timeUntil(iso), () => null);
  const due = text === "a moment";
  useEffect(() => {
    if (!due || Date.now() - refreshedAt < 5_000) return;
    refreshedAt = Date.now();
    router.refresh();
  }, [due, router]);
  return <time dateTime={iso}>{text ?? fallback}</time>;
}

Here the subscribe function does something: a 10 second interval, which is the resolution of a label that reads 42 min, an hour, 2 hours. timeUntil is the formatter that produces those, and it returns the string "a moment" for anything in the past, which is why the trigger here is a string comparison rather than a date comparison.

refreshedAt is the interesting line. It is a module-level variable, so it is shared by every mounted Countdown in the page. The screen these appear on can have a dozen of them, and they are all counting down to batches of work that tend to come due together. Twelve components each calling router.refresh() within the same second is twelve server renders of the same page. One variable and a 5 second window turns that into one.

A context or a ref would be the React-shaped answer. A module variable is correct here for the same reason it is usually wrong: there is deliberately one of it per page load, it is never read during render, and nothing about it needs to survive anything.

Three components, two importers, one dead export

The honest accounting:

$ grep -rn "LocalTime\|SendTime\|Countdown" src --include="*.tsx" | grep -v components/local-time
src/app/(app)/campaigns/[id]/outreach/page.tsx:9:import { Countdown, SendTime } from "@/components/local-time";

One file imports two of them. In that file they are wrapped in two three-line components, When and Left, used at four and three call sites, which is how the page ends up reading like English:

Approved in <Left until={thread.approvedAt!} />, then it goes out <When at={thread.nextActionAt!} there={there} />.

And LocalTime, the first and simplest of the three, is imported by nothing. It was written first, as the general case, and then every actual use turned out to need either the second zone or the countdown. It survives because it is fourteen lines and it is the clearest statement of the pattern in the repo, which is a sentimental reason to keep code. The unsentimental version is that it should go, and the pattern should be written down somewhere that cannot drift out of date.

So: a deliberate null from the server, two Intl formatters and no date library, one module variable doing a job a context would do worse, and one export to delete. The fallbacks are the only part a visitor without JavaScript ever sees, and they are the server's best honest answer: on 7 Nov instead of tomorrow at 7:30 PM.

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.