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:
| Name | Type | Optional | Description |
|---|---|---|---|
| id | string | Unique id of the island; also used as the id of the server-rendered container. | |
| component | ComponentType | The component to wrap as an island. | |
| height | string | ✓ | Reserved min-height of the container before hydration (avoids layout shift). |
| width | string | ✓ | Width of the container before hydration. |
| contentVisibility | CSSProperties['contentVisibility'] | ✓ | content-visibility value applied to the container. |
| autoHeight | boolean | ✓ | Let the container size itself after hydration instead of keeping the reserved height. |
| hydrationTrigger | 'visible' | 'click' | 'hover' | 'inactive' | ✓ | When the island hydrates. Defaults to 'visible'. |
| onlyClient | boolean | ✓ | Render the component on the client only. |
| onlyServer | boolean | ✓ | Render the component on the server only. |
withHydration and withComponentHydration additionally accept:
| Name | Type | Optional | Description |
|---|---|---|---|
| containerStyle | CSSProperties | ✓ | Extra styles for the island container element. |
| fatalError | (props: FallbackProps) => ReactElement | ✓ | Fallback render for the island's error boundary. |
| Decorator | ElementType<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
requestAnimationFramecallback, and all of a frame'srAFcallbacks 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:
- No preload. The component chunk is not fetched for an island that is going away.
- Idle, detached unmount. The island root is unmounted from a
requestIdleCallback(falling back to a timer whererequestIdleCallbackis 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. - 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.
Related
Loadable- lazy loading / code splitting for components, often used together with the Hydrator for partial hydration.