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

@audemodo/responsive-keepalive

v0.1.0

Published

Render genuinely different component trees per breakpoint without losing state — React 19.2 Activity keep-alive, with anti-thrash and IME-safe switching.

Readme

responsive-keepalive

Render genuinely different component trees per breakpoint — without losing state. Powered by React 19.2's <Activity>, with anti-thrash and IME-safe switching.

npm version license types included

When your mobile and desktop views are different component trees, switching between them at a breakpoint normally unmounts one and mounts the other — wiping scroll position, form input, open menus, and any local state. responsive-keepalive keeps the inactive tree mounted but hidden via React's <Activity>, so state survives every switch.

// The same counter's value survives switching mobile ⇄ desktop.
<Responsive
  variant={variant}
  variants={{
    mobile: () => <MobileLayout />,
    desktop: () => <DesktopLayout />,
  }}
/>

Features

  • 🌳 Structural responsiveness — swap whole trees per breakpoint, not just CSS.
  • 💾 State preserved across switches (scroll, input, selection) — keep-alive by default.
  • 📐 Viewport, container (ResizeObserver), and value-level responsiveness.
  • 🧩 A typed factory (createResponsive) — one config, inferred keys, composable gates.
  • 🔁 Shared state across layouts without lifting (useSharedState).
  • 🖥️ SSR-safe (useSyncExternalStore) with an ssr fallback variant.
  • ⌨️ IME-safe switching (deferWhileComposing) and anti-thrash (settleMs).
  • 🪶 Zero runtime deps, tree-shakeable, dual ESM/CJS + types.
  • 🛟 Graceful fallback to swap on React < 19.2.

Install

npm i @audemodo/responsive-keepalive

Requires React 19.2+ (peer dependency) — <Activity> is a React 19.2 feature.

Quick start

Define your breakpoints once with the factory:

// responsive.ts
import { createResponsive } from '@audemodo/responsive-keepalive';

export const { Responsive, Provider } = createResponsive(
  { mobile: 0, desktop: 768 }, // integer px breakpoints
  { ssr: 'mobile' },
);

Then render one prop per breakpoint — the active view is resolved from the viewport:

// App.tsx
import { Provider, Responsive } from './responsive';

export function App() {
  return (
    <Provider>
      <Responsive mobile={() => <MobileLayout />} desktop={() => <DesktopLayout />} />
    </Provider>
  );
}

Resize across 768px: the layout switches, but each layout keeps its state.

Core concepts

| Term | Meaning | | ------------ | -------------------------------------------------------------------------------- | | variant | The active breakpoint key, e.g. 'mobile' \| 'desktop'. | | variants | A map of key → the tree (or () => tree) to render. | | strategy | keepAlive (default, state preserved via Activity) or swap (unmount → reset). | | mount | lazy (default, mount on first activation) or eager (mount all upfront). | | ssr | The variant rendered on the server and before hydration. |

API

createResponsive(breakpoints, defaults?)

Builds a self-resolving <Responsive>, a root Provider, composable Match gates, and pre-bound hooks — all typed to your config. Breakpoints are integer px min-widths.

export const { Responsive, Provider, Match, useVariant, useResponsiveValue } = createResponsive(
  { mobile: 0, desktop: 768 },
  { ssr: 'mobile' },
);

export const { Mobile, Desktop } = Match;
// Composable gates — drop them anywhere under <Provider>.
<Provider>
  <Desktop>
    <DesktopNav />
  </Desktop>
  <Mobile>
    <MobileTabBar />
  </Mobile>
</Provider>;

// Pre-bound hooks — no queries to repeat.
function Toolbar() {
  const variant = useVariant(); // 'mobile' | 'desktop'
  const gap = useResponsiveValue({ mobile: 8, desktop: 24 });
  return <div style={{ gap }}>…</div>;
}

Returns { Responsive, Provider, Match, useVariant, useResponsiveValue, breakpoints }.

<Responsive>

The controlled primitive — you supply the active variant and a variants map.

import { Responsive, useMediaVariant } from '@audemodo/responsive-keepalive';

const variant = useMediaVariant({ mobile: 0, desktop: 768 }, { ssr: 'mobile' });

<Responsive
  variant={variant}
  variants={{
    mobile: () => <MobileLayout />,
    desktop: () => <DesktopLayout />,
  }}
  strategy="keepAlive" // default
  mount="lazy" // default
/>;

| Prop | Type | Default | Description | | ---------- | ----------------------------------------- | ------------- | ---------------------------------------------- | | variant | K | — | The active variant key (you control this). | | variants | Record<K, ReactNode \| () => ReactNode> | — | Map of key → content. () => defers creation. | | strategy | 'keepAlive' \| 'swap' | 'keepAlive' | Preserve inactive state, or unmount it. | | mount | 'lazy' \| 'eager' | 'lazy' | Mount on first activation, or all upfront. |

useMediaVariant(breakpoints, options?)

Resolves a variant key from the viewport. Accepts integer px breakpoints (recommended) or raw media-query strings (for non-width features like orientation).

const variant = useMediaVariant(
  { mobile: 0, desktop: 768 },
  { ssr: 'mobile', settleMs: 150, deferWhileComposing: true },
);

// raw queries for non-width features
const scheme = useMediaVariant({
  light: '(prefers-color-scheme: light)',
  dark: '(prefers-color-scheme: dark)',
});

useResponsiveValue(breakpoints, values, options?)

Picks a plain value per breakpoint — for column counts, gaps, or copy where a whole tree is overkill.

const columns = useResponsiveValue(
  { mobile: 0, tablet: 600, desktop: 1024 },
  { mobile: 1, tablet: 2, desktop: 3 },
  { ssr: 'mobile' },
);

useContainerVariant(ref, breakpoints, options?)

Like useMediaVariant, but resolves from the element's own width (via ResizeObserver) — so the same card can be a row in a wide column and a stack in a narrow one. Breakpoints are integer min content-widths.

const ref = useRef<HTMLDivElement>(null);
const variant = useContainerVariant(ref, { stack: 0, row: 420, wide: 680 }, { ssr: 'stack' });

return <div ref={ref}>{/* branch on variant */}</div>;

useSharedState(key, initialValue) & <SharedStateScope>

Share state by key across sibling layouts without lifting it to a parent. Because the store lives in a scope ancestor, the value also survives swap and remounts.

function SearchField() {
  const [query, setQuery] = useSharedState('search', '');
  return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
}

<Responsive> and the factory <Provider> mount a scope automatically, so inside them you can call useSharedState directly. Use <SharedStateScope> only for siblings that are under neither.

Options

All resolution hooks accept:

| Option | Type | Description | | --------------------- | --------- | -------------------------------------------------------------- | | ssr | K | Variant returned on the server and before hydration. | | settleMs | number | Anti-thrash: commit a change only after it is stable for N ms. | | deferWhileComposing | boolean | Hold switches during IME composition (media hooks only). |

Exported types

Strategy, Mount, VariantMap, ResponsiveProps, MediaInput, MediaVariantOptions, ResponsiveValueOptions, ContainerVariantOptions, BreakpointConfig, CreateResponsiveOptions, ConfiguredResponsive, ConfiguredResponsiveProps, ResponsiveProviderProps, MatchProps, SetSharedState.

Recipes

Next.js / SSR — feed a request-derived variant so the server renders the right tree:

// app/layout.tsx (server)
<Provider ssr={isMobileUA(headers()) ? 'mobile' : 'desktop'}>{children}</Provider>

Shared search across layouts — the mobile and desktop search boxes stay in sync:

<Provider>
  <Mobile>
    <SearchField />
  </Mobile>
  <Desktop>
    <SearchField />
  </Desktop>
</Provider>

Behavior notes

  • SSR renders the ssr variant on the server; after hydration it resolves to the real viewport, which may cause a one-time layout shift (not continuous flicker).
  • React < 19.2 has no <Activity>, so the library falls back to swap (state is not preserved) and warns once in development.
  • strategy and mount are independent: mount="eager" only matters under keepAlive.

License

MIT © vi-wolhwa