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-reel/feed

v0.2.3

Published

Virtualized video feed for XHubReel

Readme

@xhub-reel/feed

Virtualized video feed for XHubReel - TikTok-style infinite scroll

Installation

npm install @xhub-reel/feed @xhub-reel/core @xhub-reel/player @xhub-reel/ui
# or
pnpm add @xhub-reel/feed @xhub-reel/core @xhub-reel/player @xhub-reel/ui

Features

  • 📜 3-Node Carousel - Only renders prev/current/next videos (minimal DOM)
  • ♾️ Infinite Scroll - Automatic loading of more content
  • 🎯 Video Activation - Smart play/pause based on viewport
  • Preloading - Preloads next videos for instant playback via @xhub-reel/player-core
  • 🔄 Pull to Refresh - Native-feeling refresh gesture
  • 💾 Memory Efficient - Max 5 videos in DOM at once
  • 🔌 Two Modes - Manual (pass videos) or API (automatic fetching)
  • 🎨 Design System - Uses tokens from @xhub-reel/core
  • 🧩 Composable - VideoOverlay and ActionBar from @xhub-reel/ui

Breaking Changes in v0.0.1

usePreloader Hook

usePreloader now re-exports usePreload from @xhub-reel/player-core:

// Before (v0.0.0)
const { preloadStates, preloadVideo } = usePreloader({ videos, currentIndex })

// After (v0.0.1)
import { usePreload, getPreloadPriorityForFeed } from '@xhub-reel/feed'

const { preload, statuses, isPreloaded } = usePreload({ enabled: true })
const priority = getPreloadPriorityForFeed(index, currentIndex)
preload(video.url, priority, 'segment')

ActionBar Integration

VideoFeedItem now uses ActionBar from @xhub-reel/ui internally. No API changes, but custom styling may behave differently.

Usage

Manual Mode (Pass Videos Directly)

import { VideoFeed } from '@xhub-reel/feed'
import type { Video } from '@xhub-reel/core'

function App() {
  const videos: Video[] = [...]

  return (
    <VideoFeed
      videos={videos}
      onVideoChange={(video, index) => {
        console.log('Now playing:', video.id)
      }}
      onLike={() => console.log('Liked!')}
      onComment={() => console.log('Comment!')}
      onShare={() => console.log('Share!')}
    />
  )
}

API Mode (Automatic Fetching)

Use ConnectedVideoFeed for automatic data fetching from your backend:

import { XHubReelProvider } from '@xhub-reel/core/api'
import { ConnectedVideoFeed } from '@xhub-reel/feed'

function App() {
  return (
    <XHubReelProvider
      config={{
        baseUrl: 'https://api.yoursite.com/v1',
        auth: {
          accessToken: userToken,
          onTokenExpired: async () => {
            const newToken = await refreshToken()
            return { accessToken: newToken }
          },
        },
      }}
    >
      <ConnectedVideoFeed
        userId="user123"
        tag="funny"
        onLike={handleLike}
        onComment={handleComment}
        onShare={handleShare}
      />
    </XHubReelProvider>
  )
}

Using Individual Components

VideoFeedItem now exports VideoOverlay separately for custom layouts:

import { VideoFeedItem, VideoOverlay } from '@xhub-reel/feed'
import { ActionBar } from '@xhub-reel/ui'

function CustomVideoItem({ video }) {
  return (
    <div className="custom-container">
      <video src={video.url} />
      
      {/* Use VideoOverlay */}
      <VideoOverlay
        video={video}
        onAuthorClick={() => navigate(`/user/${video.author.id}`)}
        timelineExpanded={false}
      />
      
      {/* Or use ActionBar directly */}
      <ActionBar
        likeCount={video.stats.likes}
        commentCount={video.stats.comments}
        shareCount={video.stats.shares}
        isLiked={video.isLiked}
        onLike={() => likeVideo(video.id)}
        onComment={() => openComments(video.id)}
        onShare={() => shareVideo(video.id)}
      />
    </div>
  )
}

Using useVideoFeed Hook

For custom implementations, use the useVideoFeed hook:

import { useVideoFeed } from '@xhub-reel/feed'
import { VideoFeed } from '@xhub-reel/feed'

function CustomFeedPage() {
  const {
    videos,
    isLoading,
    hasMore,
    fetchNextPage,
    error,
    refetch,
  } = useVideoFeed({
    config: {
      baseUrl: 'https://api.yoursite.com',
      auth: { accessToken: token },
    },
    userId: 'user123',
    limit: 10,
    onSuccess: (videos) => console.log('Fetched', videos.length),
    onError: (error) => console.error('Error:', error),
  })

  if (isLoading) return <LoadingSpinner />
  if (error) return <ErrorMessage error={error} onRetry={refetch} />

  return (
    <VideoFeed
      videos={videos}
      isLoading={isLoading}
      hasMore={hasMore}
      onLoadMore={fetchNextPage}
    />
  )
}

With Actions

<VideoFeed
  videos={videos}
  onLike={() => likeVideo()}
  onComment={() => openComments()}
  onShare={() => shareVideo()}
  onAuthorClick={() => viewProfile()}
/>

Components

VideoFeed

Main feed component with swipe gestures and virtualization.

| Prop | Type | Default | Description | |------|------|---------|-------------| | videos | Video[] | [] | Array of video objects | | initialIndex | number | 0 | Starting video index | | onVideoChange | (video, index) => void | - | Called when active video changes | | onLoadMore | () => void \| Promise<void> | - | Called when scrolling near end | | onLike | (video) => void | - | Called when like button pressed | | onComment | (video) => void | - | Called when comment button pressed | | onShare | (video) => void | - | Called when share button pressed | | onAuthorClick | (video) => void | - | Called when author clicked | | loadMoreThreshold | number | 3 | Videos from end to trigger load | | transitionDuration | number | 300 | Swipe animation duration (ms) | | swipeThreshold | number | 50 | Swipe threshold (px) | | velocityThreshold | number | 0.3 | Velocity threshold (px/ms) | | gesturesDisabled | boolean | false | Disable swipe gestures | | hapticEnabled | boolean | true | Enable haptic feedback |

VideoFeedItem

Individual video item with built-in controls.

| Prop | Type | Default | Description | |------|------|---------|-------------| | video | Video | required | Video object | | isActive | boolean | false | Whether currently active | | priority | PreloadPriority | 'none' | Preload priority | | showTimeline | boolean | true | Show timeline/seekbar | | onLike | () => void | - | Like handler | | onComment | () => void | - | Comment handler | | onShare | () => void | - | Share handler | | onAuthorClick | () => void | - | Author click handler |

VideoOverlay

Info overlay component (author, caption, hashtags).

| Prop | Type | Default | Description | |------|------|---------|-------------| | video | Video | required | Video object | | onAuthorClick | () => void | - | Author click handler | | timelineExpanded | boolean | false | Adjust padding for timeline |

ConnectedVideoFeed (API Mode)

| Prop | Type | Default | Description | |------|------|---------|-------------| | config | XHubReelConfig | - | API configuration (optional if using XHubReelProvider) | | userId | string | - | User ID for user-specific feed | | tag | string | - | Tag/hashtag filter | | searchQuery | string | - | Search query | | pageSize | number | 10 | Videos per page | | initialVideos | Video[] | - | Initial videos while loading | | onFetchSuccess | (videos) => void | - | Success callback | | onFetchError | (error) => void | - | Error callback | | renderLoading | () => ReactNode | - | Custom loading UI | | renderError | (error, retry) => ReactNode | - | Custom error UI | | renderEmpty | () => ReactNode | - | Custom empty UI |

Note: PullToRefresh component đã được chuyển sang @xhub-reel/ui package. Import từ @xhub-reel/ui thay vì @xhub-reel/feed.

Hooks

useVideoFeed

Fetch videos with infinite scroll support.

const {
  videos,           // Flattened videos array
  isLoading,        // Initial loading state
  isFetchingMore,   // Loading more state
  hasMore,          // Has more to load
  fetchNextPage,    // Load next page
  refetch,          // Refetch all
  error,            // Error if any
  isApiMode,        // Whether API mode active
  totalCount,       // Total count (if provided by API)
} = useVideoFeed({
  config,           // XHubReelConfig (required for API mode)
  userId,           // User ID filter
  tag,              // Tag filter
  searchQuery,      // Search query
  limit,            // Page size
  enabled,          // Enable/disable
  initialVideos,    // Initial data
  staleTime,        // Cache time
  onSuccess,        // Success callback
  onError,          // Error callback
})

useVideoActivation

Control video activation based on visibility.

const {
  isActive,         // Whether video is active
  isVisible,        // Whether video is visible
  visibilityRatio,  // Visibility ratio (0-1)
  activate,         // Manual activate
  deactivate,       // Manual deactivate
} = useVideoActivation({
  containerRef,     // Container element ref
  videoRef,         // Video element ref
  isCurrentVideo,   // Whether current in feed
  onActivate,       // Activate callback
  onDeactivate,     // Deactivate callback
  autoActivate,     // Enable auto-activation
})

usePreload (from @xhub-reel/player-core)

Preload videos with priority queue.

import { usePreload, getPreloadPriorityForFeed } from '@xhub-reel/feed'

const {
  preload,          // Enqueue preload
  preloadMany,      // Enqueue multiple
  cancel,           // Cancel preload
  cancelAll,        // Cancel all
  setPaused,        // Pause/resume
  handleScrollVelocity, // Handle scroll
  isPreloaded,      // Check if preloaded
  getStatus,        // Get status
  preloadedUrls,    // Preloaded URLs
  statuses,         // All statuses
  isPaused,         // Paused state
  manager,          // PreloadManager instance
} = usePreload({
  enabled: true,
  maxConcurrent: 2,
  maxPreloaded: 5,
})

// Get priority for feed
const priority = getPreloadPriorityForFeed(index, currentIndex)
preload(video.url, priority, 'segment')

Helper Functions

import {
  getPreloadPriorityForFeed,  // Get numeric priority
  mapPriorityToNumeric,        // Map enum to number
  getPreloadPriority,          // Get PreloadPriority enum
  preloadThumbnail,            // Preload thumbnail
} from '@xhub-reel/feed'

// Get priority based on distance from current
const priority = getPreloadPriorityForFeed(videoIndex, currentIndex)
// Returns: 1 (current), 3 (adjacent), 5 (near), 7 (far), 10 (dispose)

// Map priority enum to number
const numPriority = mapPriorityToNumeric('high') // 1

// Get enum priority
const enumPriority = getPreloadPriority(videoIndex, currentIndex)
// Returns: 'high' | 'medium' | 'low' | 'metadata' | 'none'

// Preload thumbnail
preloadThumbnail('https://example.com/thumbnail.jpg')

Video Activation Rules

Videos are activated based on viewport visibility:

| Condition | Action | |-----------|--------| | > 50% visible | Play | | < 30% visible | Pause + Reset | | Scroll velocity > 2000px/s | Skip activation | | Scroll stopped > 300ms | Activate nearest |

Memory Management

The feed automatically manages memory:

  • Max 5 videos in DOM at once
  • Max 3 decoded video frames
  • Aggressive cleanup on scroll
  • < 150MB total memory usage

Performance Tips

  1. Use video.id as key - Ensures proper virtualization
  2. Preload thumbnails - Use blur placeholders
  3. Let the system handle preloading - usePreload manages queue automatically
  4. Dispose properly - Memory manager handles cleanup
  5. Use design tokens - All components use @xhub-reel/core tokens

Design System Integration

All components use design tokens from @xhub-reel/core:

import { 
  colors,      // colors.background, colors.accent, etc.
  spacing,     // spacing[1] - spacing[8]
  fontSizes,   // fontSizes.xs, sm, md, lg
  fontWeights, // fontWeights.medium, semibold, bold
  radii,       // radii.sm, md, lg, full
  zIndices,    // zIndices.base, sticky, overlay
  durations,   // durations.fast, normal, slow
  easings,     // easings.xhubReel (cubic-bezier)
} from '@xhub-reel/core'

This ensures consistent styling across all packages.

Migration Guide

From v0.0.0 to v0.0.1

1. Update usePreloader

// Before
import { usePreloader } from '@xhub-reel/feed'
const { preloadStates } = usePreloader({ videos, currentIndex })

// After  
import { usePreload, getPreloadPriorityForFeed } from '@xhub-reel/feed'
const { statuses } = usePreload()
const priority = getPreloadPriorityForFeed(index, currentIndex)

2. ActionBar Styling

If you were overriding ActionBar styles, they may not work anymore since VideoFeedItem now uses @xhub-reel/ui ActionBar component. Use ActionBar directly for custom styling:

import { ActionBar } from '@xhub-reel/ui'

<ActionBar
  likeCount={video.stats.likes}
  // ... props with custom styling
  style={{ right: 24 }}
/>

3. VideoOverlay

If you were accessing internal overlay elements, use the new VideoOverlay component:

import { VideoOverlay } from '@xhub-reel/feed'

<VideoOverlay
  video={video}
  onAuthorClick={handleAuthorClick}
  timelineExpanded={timelineExpanded}
/>

License

MIT