npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@nextjs-bridges/hydration

v1.0.0

Published

Declarative hydration boundaries for Next.js 15 — defer Client Component hydration until a chosen strategy fires

Readme

@nextjs-bridges/hydration

Declarative hydration boundaries for Next.js 15 — defer Client Component hydration until a chosen strategy fires.

License: MIT

Part of the bridges monorepo — see the root README for how this fits alongside @nextjs-bridges/lazy-handler and @nextjs-bridges/share, and apps/web's /demo/ecommerce page for all three working together.

pnpm add @nextjs-bridges/hydration

The problem

Once a subtree is marked 'use client', React hydrates the whole thing immediately, including heavy widgets far below the fold. There is no declarative way to say "hydrate this when visible" or "hydrate on first hover."

How it works

<HydrationBoundary> wraps any subtree and gates rendering behind a boolean hydrated state. Until that state flips, the boundary renders the fallback prop (skeleton, spinner, or nothing). Hydration is triggered by a chosen strategy — viewport visibility, browser idle time, first pointer contact, or a manual imperative call.

This is purely a React-level deferral. The component JS is still bundled; hydration just determines when React mounts and makes the subtree interactive.

API

<HydrationBoundary>

import { HydrationBoundary } from '@nextjs-bridges/hydration';

<HydrationBoundary
  strategy="visible"
  fallback={<Skeleton />}
  rootMargin="200px"
  onHydrate={() => analytics.track('widget-hydrated')}
>
  <HeavyWidget />
</HydrationBoundary>;

Props

| Prop | Type | Default | Description | | ------------ | --------------------------------------- | ----------- | --------------------------------------------------------- | | strategy | HydrationStrategy \| HydrationStrategy[] | 'visible' | When to hydrate (see table below). An array arms multiple strategies at once — whichever fires first wins. | | fallback | ReactNode | null | Rendered while dehydrated | | threshold | number \| number[] | 0.1 | IntersectionObserver threshold — visible strategy only | | rootMargin | string | '200px' | IntersectionObserver rootMargin — visible strategy only | | interactionEvents | Array<keyof HTMLElementEventMap> | ['pointerenter', 'focusin', 'touchstart'] | DOM events that trigger hydration — interaction strategy only | | onHydrate | () => void | — | Fired once when hydration occurs | | errorFallback | ReactNode \| ((error: Error) => ReactNode) | — | Rendered instead of crashing the tree if loader rejects |

Strategies

| Strategy | Triggers hydration when… | | ------------- | ---------------------------------------------------------------------- | | eager | Immediately (same as no boundary) | | visible | The boundary enters the viewport (IntersectionObserver) | | idle | The browser is idle (requestIdleCallback / setTimeout(0) fallback) | | interaction | The user moves a pointer over, tabs into, or touches the boundary | | manual | hydrateNow() is called imperatively |

Combining strategies

Pass an array to hydrate on whichever strategy fires first — an OR condition, evaluated by one boundary instead of nesting several:

<HydrationBoundary strategy={['visible', 'idle']} fallback={<Skeleton />}>
  <HeavyWidget />
</HydrationBoundary>

This hydrates as soon as the boundary scrolls into view or the browser goes idle, whichever happens first — the other strategy's listener is torn down once one of them fires. This differs from nesting boundaries, which is an AND condition (each layer must hydrate before the next can start listening).

Narrowing the interaction strategy

By default strategy="interaction" hydrates on any pointer contact — pointerenter, focusin, or touchstart. Pass interactionEvents to require a specific event instead:

<HydrationBoundary strategy="interaction" interactionEvents={['click']} fallback={<Skeleton />}>
  <HeavyWidget />
</HydrationBoundary>

Handling a failed loader

A rejected loader promise throws during render, same as any React.lazy component. Pass errorFallback to handle it declaratively instead of crashing the tree (or supplying your own error boundary):

<HydrationBoundary
  strategy="visible"
  fallback={<Skeleton />}
  loader={() => import('./HeavyWidget')}
  errorFallback={(error) => <p>Couldn't load: {error.message}</p>}
/>

useHydrationState()

Reads the nearest boundary's hydration state and imperative trigger. Must be called inside a <HydrationBoundary> — most useful inside a fallback slot to place a "Load now" button.

import { useHydrationState } from '@nextjs-bridges/hydration';

function ManualFallback() {
  const { hydrated, hydrateNow } = useHydrationState();
  return <div>{hydrated ? null : <button onClick={hydrateNow}>Load widget</button>}</div>;
}

<HydrationBoundary strategy="manual" fallback={<ManualFallback />}>
  <HeavyWidget />
</HydrationBoundary>;

withHydrationBoundary(Component, options?)

HOC version. Wraps a component in a boundary without changing its usage.

import { withHydrationBoundary } from '@nextjs-bridges/hydration';

const LazyChart = withHydrationBoundary(Chart, {
  strategy: 'visible',
  fallback: <ChartSkeleton />,
});

Nesting

Boundaries nest independently. Each has its own trigger and state.

<HydrationBoundary strategy="idle">
  {/* hydrates after page load when browser is idle */}
  <HydrationBoundary strategy="interaction" fallback={<Skeleton />}>
    {/* additionally requires hover/focus before mounting */}
    <VeryHeavyWidget />
  </HydrationBoundary>
</HydrationBoundary>

Gotchas

  • This is render-time deferral, not code-splitting. The JS bundle for <HeavyWidget> is still sent to the browser — it just isn't executed until the strategy triggers. Combine with React.lazy + next/dynamic for actual code splitting.
  • strategy="eager" is a no-op. The boundary renders children immediately, same as removing it. Useful for toggling deferral in development without changing JSX.
  • No SSR hydration mismatch. The boundary renders fallback on the server as well, so the initial HTML always matches the dehydrated state.
  • IntersectionObserver fallback. When unavailable (Node, jsdom), strategy="visible" falls back to hydrating immediately.
  • onHydrate stability. The callback ref is always kept current without causing effects to re-run; you can pass an inline function safely.

License

MIT