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

@xhub-reels/sdk

v0.2.18

Published

High-performance Short Video / Reels SDK for React — optimized for Flutter WebView

Readme

xhub-reels-sdk

High-performance Short Video / Reels SDK for React — optimized for Flutter WebView.

npm version bundle size license

Why?

Built as a leaner, faster alternative focused on solving real-world WebView performance issues:

| Problem | Solution | |---|---| | React state updates during drag → jank | Pointer Events + direct DOM, zero React during drag | | querySelectorAll per animation frame | MutationObserver slot cache, O(1) lookup | | setTimeout polling for sync | Single store.subscribe | | CSS transitions interrupted by React renders | Web Animations API (imperative, cancellable) | | Static class-level global cache | Instance-scoped prefetch cache | | 1153-line Feed component | Split into focused, testable units |

Installation

npm install xhub-reels-sdk

Peer dependencies:

npm install react react-dom  # >=18.0.0

Quick Start

import {
  ReelsProvider,
  MockDataSource,
  MockInteraction,
} from 'xhub-reels-sdk';
import { MyFeed } from './MyFeed';

export function App() {
  return (
    <ReelsProvider
      adapters={{
        dataSource: new MockDataSource(),
        interaction: new MockInteraction(),
      }}
    >
      <MyFeed />
    </ReelsProvider>
  );
}

With Your Own Data Source

import type { IDataSource, FeedPage } from 'xhub-reels-sdk';

class MyAPIDataSource implements IDataSource {
  async fetchFeed(cursor?: string | null): Promise<FeedPage> {
    const res = await fetch(`/api/feed?cursor=${cursor ?? ''}`);
    const data = await res.json();
    return {
      items: data.videos,
      nextCursor: data.nextCursor,
      hasMore: data.hasMore,
    };
  }
}

Hooks

import { useFeed, usePlayer, useResource } from 'xhub-reels-sdk';

function MyFeed() {
  const { items, loading, loadInitial, loadMore } = useFeed();
  const { focusedIndex, setFocusedIndex, shouldRenderVideo } = useResource();
  const { isPlaying, togglePlay, handlers } = usePlayer();

  // ...
}

Thumbnail grid → Modal

<ReelsFeedThumbnail> is a grid component for the pre-player browsing UX (e.g. a community feed that opens the player on tap). It must be mounted inside <ReelsProvider> and reads items from the same shared feed. The host provides card visuals via renderThumbnail.

SDK-owned modal (recommended)

Pair <ReelsFeedThumbnail openOnClick> with <ReelsModal> to let the SDK own the open→play lifecycle end to end. This eliminates the "poster stuck for ~3s" stall: on tap, the SDK mounts the feed offscreen-but-painted, prewarms the focused video and its neighbors synchronously, then slides the panel in — so the first frame is already decoded by the time the animation finishes.

import { ReelsProvider, ReelsFeedThumbnail, ReelsModal } from 'xhub-reels-sdk';

function CommunityPage() {
  return (
    <ReelsProvider adapters={{ dataSource, interaction }}>
      <ReelsFeedThumbnail
        openOnClick
        renderThumbnail={(item) => (
          <article className="aspect-[3/2] rounded-lg overflow-hidden">
            <img src={item.poster} alt="" />
            <p>@{item.author.name}</p>
          </article>
        )}
      />
      <ReelsModal feedProps={{ renderOverlay, renderActions, initialMuted: false }} />
    </ReelsProvider>
  );
}

openOnClick calls navigation.open(index) on tap; <ReelsModal> subscribes to the same navigation store and drives the rest. Because play() now starts inside the tap's transient-activation window, initialMuted={false} unmuted autoplay is far more reliable on mobile WebViews.

Headless callback (back-compat)

The original headless approach still works — drive your own drawer/modal via onThumbnailClick:

function CommunityPage() {
  const [drawerOpen, setDrawerOpen] = useState(false);

  return (
    <ReelsProvider adapters={{ dataSource, interaction }}>
      <ReelsFeedThumbnail
        renderThumbnail={(item) => (
          <article className="aspect-[3/2] rounded-lg overflow-hidden">
            <img src={item.poster} alt="" />
          </article>
        )}
        onThumbnailClick={(id) => {
          setDrawerOpen(true);
          window.history.replaceState(null, '', `#reel_uuid=${id}`);
        }}
      />
      {drawerOpen && <YourDrawer onClose={() => setDrawerOpen(false)} />}
    </ReelsProvider>
  );
}

By default, clicking a card calls setFocusedIndexImmediate(index) on the ResourceGovernor so the player opens instantly without a scroll-to-index. To disable this glue (e.g. if the host manages focus separately), pass setFocusOnClick={false}.

<ReelsFeedThumbnail> props

| Prop | Type | Default | Description | |---|---|---|---| | renderThumbnail | (item, index) => ReactNode | required | Card visual for a single item | | openOnClick | boolean | false | Open the SDK-owned <ReelsModal> via navigation.open(index) on tap | | prewarmOnClick | boolean | true | Fire HLS metadata prefetch on tap (mobile-friendly; runs even without hover) | | onThumbnailClick | (id, item, index) => void | — | Click handler (fires alongside open for back-compat) | | setFocusOnClick | boolean | true | Pre-focus the slot before open/onThumbnailClick fires | | prefetchOnHover | boolean | false | Opt-in HLS metadata prefetch on pointerenter | | renderLoading | () => ReactNode | — | Shown while loading with no items | | renderEmpty | () => ReactNode | — | Shown when feed is empty | | renderError | ({ message, retry }) => ReactNode | — | Shown on error with no items | | className | string | 'grid grid-cols-2 gap-3' | Outer wrapper className | | wrap | boolean | true | Set false to render without an outer <div> | | getKey | (item, index) => string | item.id | Key override for duplicate lists |

<ReelsModal> props

The modal is a customizable shell: the SDK owns timing and prewarm, while the host can override the animation and chrome. Omit any prop to fall back to a sensible default.

| Prop | Type | Default | Description | |---|---|---|---| | feedProps | ReelsFeedProps | — | Render props forwarded to the inner <ReelsFeed> (renderOverlay, renderActions, initialMuted, …) | | animationConfig | { duration?, easing?, direction? } | { 300, cubic-bezier(0.22, 1, 0.36, 1), 'up' } | Slide-in tuning; direction is 'up' \| 'down' \| 'fade' | | renderBackdrop | (state) => ReactNode | dimmed overlay | Custom backdrop; state is { phase, openIndex, close } | | renderCloseButton | (state) => ReactNode | default ✕ button | Custom close affordance | | onOpen | (index) => void | — | Fired when a modal session opens | | onClose | () => void | — | Fired when the modal fully closes | | closeOnBackdropClick | boolean | true | Close when the backdrop is clicked | | closeOnEscape | boolean | true | Close on the Escape key | | lockBodyScroll | boolean | true | Lock <body> scroll while open | | prewarmForward | number | 2 | How many forward neighbors to prewarm on open | | className | string | — | Class on the sliding panel | | zIndex | number | 1000 | Stacking context for the portal | | portalTarget | HTMLElement \| null | document.body | Portal mount node |

<ReelsModal> honors prefers-reduced-motion (skips the slide), traps focus, restores focus to the trigger on close, and renders into a portal.

Hover prefetch

For hover-capable devices, opt into manifest prefetch so opening the player feels instant:

<ReelsFeedThumbnail prefetchOnHover renderThumbnail={...} />

The SDK uses adapters.videoLoader?.preloadMetadata?.(url) and dedupes per item so a card only triggers one prefetch regardless of how many times it's hovered. On touch devices there's no hover, so prefer prewarmOnClick (on by default).

Migrating from the headless callback

If you currently open your own drawer from onThumbnailClick:

  1. Add openOnClick to <ReelsFeedThumbnail> and mount <ReelsModal> as a sibling inside <ReelsProvider>.
  2. Move your drawer's render props (renderOverlay, renderActions, initialMuted, …) onto <ReelsModal feedProps={{ … }}>.
  3. Re-create your drawer's look with animationConfig, renderBackdrop, and renderCloseButton — or drop them to use the defaults.
  4. Remove the host drawerOpen state and your <ReelsDrawer>/modal wrapper.

onThumbnailClick still fires alongside open, so you can migrate incrementally (e.g. keep your URL-hash side effect) before deleting host state.

Gesture Engine

import { usePointerGesture, useSnapAnimation } from 'xhub-reels-sdk';

function SwipeableFeed() {
  const { animateSnap, animateBounceBack } = useSnapAnimation();

  const { bind } = usePointerGesture({
    onDragOffset: (offset) => {
      // Direct DOM — zero React state during drag
      containerRef.current!.style.transform = `translateY(${offset}px)`;
    },
    onSnap: (direction) => {
      const next = direction === 'forward' ? index + 1 : index - 1;
      goToIndex(next);
      animateSnap(targets);
    },
    onBounceBack: () => animateBounceBack(targets),
  });

  return <div {...bind} style={{ touchAction: 'none' }}>...</div>;
}

Architecture

xhub-reels-sdk
├── types/          ← Pure TypeScript interfaces (no deps)
├── domain/         ← Business logic (zustand/vanilla, no React)
│   ├── PlayerEngine      — State machine + Circuit Breaker
│   ├── FeedManager       — Pagination, LRU, SWR, dedup
│   ├── OptimisticManager — Debounced like/follow + rollback
│   └── ResourceGovernor  — Max 3 video DOM nodes
├── gesture/        ← Pointer Events + Web Animations API
│   ├── usePointerGesture  — Zero React during drag
│   └── useSnapAnimation   — Cancellable snap animation
├── components/     ← React components
│   └── ReelsProvider     — Context + DI container
├── hooks/          ← useSyncExternalStore-based hooks
│   ├── useFeed
│   ├── usePlayer
│   └── useResource
└── adapters/mock/  ← Development mocks

Performance Targets

| Metric | Target | |---|---| | Bundle size | < 35KB gzip | | First video load | < 1s on WiFi | | Scroll FPS | 60fps | | Max video DOM nodes | 3 | | React renders per swipe | 1 (on snap only) |

Bundle Size Limits

| Package | Limit | |---|---| | xhub-reels-sdk | 35KB gzip |

Development

# Install
npm install

# Build package
npm run build

# Run demo app
npm run dev

# Tests
npm run test

# Type check
npm run typecheck

# Lint + format
npm run lint:fix

Release

# Bump version
npm version patch  # or minor / major

# Tag and push (triggers GitHub Actions publish)
git tag v0.1.1 && git push --tags

License

MIT