Skip to main content

Hydrator

The Hydrator is Archibald's partial-hydration primitive. It wraps a component as an independently hydrated island: the server renders its markup, and the client only boots the island's React tree when it is needed (by default once it scrolls into view). This keeps the initial JavaScript on a page small and spreads hydration work across the session instead of paying for it all up front.

import { withHydration } from '@archibald/storefront';

export default withHydration({
component: ProductTeaser,
hydrationTrigger: 'visible' // 'visible' (default) | 'click' | 'hover' | 'inactive'
});

There is also withComponentHydration (also exported from @archibald/storefront), a variant used for CMS components that hydrates a component into a plain div container and is registered through the CMS registry.

Props

Both HOCs accept the HydrationProps fields:

NameTypeOptionalDescription
idstringUnique id of the island; also used as the id of the server-rendered container.
componentComponentTypeThe component to wrap as an island.
heightstringReserved min-height of the container before hydration (avoids layout shift).
widthstringWidth of the container before hydration.
contentVisibilityCSSProperties['contentVisibility']content-visibility value applied to the container.
autoHeightbooleanLet the container size itself after hydration instead of keeping the reserved height.
hydrationTrigger'visible' | 'click' | 'hover' | 'inactive'When the island hydrates. Defaults to 'visible'.
onlyClientbooleanRender the component on the client only.
onlyServerbooleanRender the component on the server only.

withHydration and withComponentHydration additionally accept:

NameTypeOptionalDescription
containerStyleCSSPropertiesExtra styles for the island container element.
fatalError(props: FallbackProps) => ReactElementFallback render for the island's error boundary.
DecoratorElementType<DefaultDecoratorProps>Wrapper component rendered around the island's tree (e.g. to re-apply contexts).

Each island owns the DOM container the server rendered into. While the island is not hydrated, that container belongs to the main (page) tree; once hydrated, the island's React root takes ownership of it. This single-ownership rule is the key invariant behind the teardown behavior described below.

Deferred island teardown on SPA navigation

When a client-side (SPA) navigation happens, every island on the outgoing page reacts to the router's history change. Tearing those islands down naively is expensive, so the Hydrator defers the work.

Why teardown is deferred

On a navigation, the previous page's islands are about to be discarded. Two naive behaviors used to make this slow:

  • Wasted preloads. Islands that were never hydrated would still preload their component chunk on the history change, downloading code for a page that is already being destroyed.
  • One long unmount task. Each island root was unmounted inside its own requestAnimationFrame callback, and all of a frame's rAF callbacks run in a single task. With many islands on a page this batched into one long main-thread task, and because the containers were still attached to the document, React's deletion commit forced a large style recalculation on the live tree mid-transition.

On a content-heavy homepage this could amount to a multi-second freeze during the route transition.

What the Hydrator does instead

Once the SPA has taken over, the Hydrator short-circuits the outgoing islands:

  1. No preload. The component chunk is not fetched for an island that is going away.
  2. Idle, detached unmount. The island root is unmounted from a requestIdleCallback (falling back to a timer where requestIdleCallback is unavailable), and only after the container has left the document. Unmounting a detached container keeps React's deletion commit off the live tree, so it no longer forces a style recalculation during the transition.
  3. Coupled container handoff. The container is handed back to the main tree only after the island root has released it. Releasing it earlier would let both the main tree and the island root write into the same element on same-route history changes, which is the failure mode this ordering exists to prevent.

Persistent islands

Some islands (for example a header that is shared across routes) stay mounted across a navigation, so their container never detaches. To avoid polling forever, the teardown caps the number of idle checks (20 by default). Once the cap is reached, the container is handed back regardless, so a persistent island transitions cleanly without blocking.

Known limitation

Same-route history changes that only alter the query parameters are covered by the single-ownership design (the coupled handoff above), but this path is harder to exercise in isolation. Treat it as the area to watch when changing teardown behavior.

  • Loadable - lazy loading / code splitting for components, often used together with the Hydrator for partial hydration.