Timers & time ago
Intervals are famously hard to get right with hooks. A naive useEffect(() => setInterval(...)) fails one of two ways. With empty deps, the callback goes stale. With the callback in deps, the timer resets on every render.
Getting it right takes the useInterval hook from Dan Abramov’s Making setInterval Declarative with React Hooks . It needs a ref to smuggle the latest callback past the effect’s dependency check:
With observables there is no mismatch to bridge. Time is just another stream. The delay and the running flag are inputs to the pipe. switchMap swaps the interval whenever either changes. scan keeps the count across swaps. Nothing to get subtly wrong:
A smarter time-ago
Every app with a feed has “42 seconds ago” labels. The naive version re-renders every label every second, forever.
Streams let you say precisely when a re-render is warranted:
- One shared clock ticks every second.
mapturns each tick into structured parts:{value, unit}. The observable emits data. Formatting stays in JSX, here viaIntl.RelativeTimeFormat.distinctUntilChangeddrops every tick that wouldn’t change the label.
Watch the render counters below. The fresh message re-renders every second. Once a label reads “1 minute ago” it re-renders once a minute. The second message goes quiet right after it flips:
One more thing to notice: all three labels share a single interval. The clock is one module-scoped stream, and react-rx shares one subscription per observable.
Server rendering & hydration
Time is the classic hydration hazard. The server renders at one Date.now(). The client hydrates seconds later at another. React logs a mismatch.
The stream version avoids it by making the initial value data passed from the server:
- The server serializes its clock (
serverNow) next to the HTML. - The client’s hydration render computes the exact same parts from it. The markup matches byte for byte. Zero mismatches.
- The static HTML already shows the right label before any JavaScript loads.
- The live clock takes over right after hydration.
This sandbox simulates the whole journey. renderToString produces the “server” HTML. It sits on screen for three seconds (“the bundle is downloading”). Then hydrateRoot attaches, with onRecoverableError counting any mismatches. Watch the label flip to “1 minute ago” shortly after hydration:
The mechanism is useObservable’s SSR contract. On the server it renders what the client’s first paint will show: a synchronous emission if there is one, else the initialValue. Deriving that initial value from serialized server data instead of Date.now() is all it takes.