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

@doync/query-virtualizer

v0.4.0

Published

doync React binding: virtualized infinite lists over Subscriptions via @rocicorp/zero-virtual/core

Readme

@doync/query-virtualizer

React binding for virtualized infinite lists over doync Subscriptions. Stage factories return a Bound query; the hook keeps three live useQuery slots (main page, after page, single-row) filled as you scroll, and renders only the visible window. Built on @rocicorp/zero-virtual /core only — consumers do not declare or import zero-virtual themselves. This package's root export is the stable surface; deeper experimental core symbols stay on @rocicorp/zero-virtual/core and may move with that pin.

Use it under a @doync/react <DoyncProvider> with registered queries that support keyset (or equivalent) pagination.

Install

pnpm add @doync/query-virtualizer

| Package | Role | | --- | --- | | @doync/query-virtualizer | This binding | | @doync/react, @doync/core | Direct dependencies (pulled in transitively — import DoyncProvider / hooks from @doync/react as usual) | | react | Peer>=18 |

@rocicorp/zero-virtual is a direct dependency of this package pinned for you; do not add it to the app unless you intentionally opt into /core experimental APIs.

Quick sketch

An overflow-element list. Factories that do not close over props can live at module scope so their identity stays stable across renders (a fresh estimateSize / getRowKey / factory each render resets paging).

import {
  rowAttributes,
  useQueryVirtualizer,
  type VirtualRow,
} from '@doync/query-virtualizer'
import { queries } from './shared/data'
import { useCallback, useRef } from 'react'

type Task = { id: string; title: string; modified: number }
type TaskStart = { id: string; modified: number }

const estimateSize = () => 48
const getRowKey = (row: Task) => row.id
const toStartRow = (row: Task): TaskStart => ({
  id: row.id,
  modified: row.modified,
})
const getSingleQuery = ({ id }: { id: string; settled: boolean }) => ({
  query: queries.taskById(id),
})

export function TaskList({ ownerId }: { ownerId: string }) {
  const listRef = useRef<HTMLDivElement>(null)
  const getScrollElement = useCallback(() => listRef.current, [])

  const getPageQuery = useCallback(
    ({
      limit,
      start,
      dir,
    }: {
      limit: number
      start: TaskStart | null
      dir: 'forward' | 'backward'
      settled: boolean
    }) => ({
      // Bound query — registry leaf + args. Optional `options` (ttl, skip, …)
      // is the per-stage UseQueryOptions bag.
      query: queries.taskPage({ ownerId, limit, start, dir }),
    }),
    [ownerId],
  )

  const { items, spaceBefore, spaceAfter, status } = useQueryVirtualizer<
    string,
    Task,
    TaskStart
  >({
    listContextParams: ownerId, // any stable context; change resets the list
    getScrollElement,
    estimateSize,
    getRowKey,
    getPageQuery,
    getSingleQuery,
    toStartRow,
  })

  return (
    <div
      ref={listRef}
      style={{ height: 480, overflow: 'auto', position: 'relative' }}
    >
      {/* Spacers are sibling elements — not padding on the scroller —
          so native scroll anchoring can compensate. */}
      <div style={{ height: spaceBefore }} />
      {items.map((item) => (
        <TaskRow key={item.key} item={item} />
      ))}
      <div style={{ height: spaceAfter }} />
      {status.status !== 'complete' ? <div>Loading…</div> : null}
    </div>
  )
}

function TaskRow({ item }: { item: VirtualRow<Task> }) {
  const { index, key, row } = item
  if (row === undefined) {
    return (
      <div {...rowAttributes(index, key)} style={{ height: 48 }}>
        …
      </div>
    )
  }
  return (
    <div {...rowAttributes(index, key)} style={{ height: 48 }}>
      {row.title}
    </div>
  )
}

Window-scrolled lists use useQueryWindowVirtualizer with the same options: getScrollElement then returns the element rows render into (normal page flow); the window is the scroll container.

Public API

Hooks

useQueryVirtualizer(options) → QueryVirtualizerResult

Virtualized, infinitely-paginated list that scrolls inside an overflow element. getScrollElement returns that element (also where rows render).

useQueryWindowVirtualizer(options) → QueryVirtualizerResult

Same contract, window as the scroll container.

Both take UseQueryVirtualizerOptions<TListContextParams, TRow, TStartRow>:

| Option | Required | Notes | | --- | --- | --- | | listContextParams | yes | Opaque list context. A value change (by the core's comparison) resets paging / window. | | getScrollElement | yes | () => HTMLElement \| null — overflow scroller, or the rows host for the window variant. | | estimateSize | yes | (index) => number — estimated row height in px. Keep referentially stable. | | getRowKey | yes | (row) => RowKey — stable id per data row. | | getPageQuery | yes | GetPageQuery — see Stage factories. | | getSingleQuery | yes | GetSingleQuery — one-row Bound query for permalink / anchor rows. | | toStartRow | yes | (row) => TStartRow — maps a data row to the keyset cursor getPageQuery receives as start. | | overscan | no | Extra rows rendered past the viewport (default from zero-virtual core). | | count | no | Known total row count when the app already has one; otherwise the composition estimates. | | anchoring | no | AnchoringMode — e.g. 'native' for comment-style natural-height lists. | | minPageSize | no | Floor on rows requested per page (default 50; lower for tall cards). | | settleTime | no | Idle ms before the list is considered settled (default 2000). | | onSettled | no | Fired when the list has been idle for settleTime. | | permalinkID | no | Row id to scroll into view once present. | | scrollState / onScrollStateChange | no | Persist / restore window (ScrollHistoryState). Pair with useRestoreScrollState or your router. | | observeElementRect / observeElementOffset | no | Override default observers (element vs window defaults are filled in per entry). |

QueryVirtualizerResult<TRow> is the zero-virtual binding result without the upstream complete boolean, plus aggregated doync status:

| Field | Notes | | --- | --- | | items | VirtualRow<TRow>[] currently on screen (and overscan). row may be undefined while a slot is still loading. | | spaceBefore / spaceAfter | Spacer heights in px — render as sibling elements, not scroller padding. | | status | Aggregated ViewStatus from live stages (unknowncomplete / error). Replaces upstream complete. | | rowsEmpty | True when the composition has no data rows. | | permalinkNotFound | True when a permalinkID could not be resolved. | | estimatedTotal / total | Size hints from the composition. | | settled | True after the list has been idle for settleTime. | | scrollElement | Live HTMLElement \| null — the resolved scroller (null until mounted). | | options | Bound scroll/observer accessors (used by usePinToBottom). | | rowAt | (index) => TRow \| undefined — data row at a virtual index. |

usePinToBottom(virtualizer, options?)

Chat/log follow: when content grows, keep the viewport pinned to the bottom — only if the user was already parked there. Pass the result of useQueryVirtualizer / useQueryWindowVirtualizer.

import {
  usePinToBottom,
  useQueryWindowVirtualizer,
} from '@doync/query-virtualizer'

const result = useQueryWindowVirtualizer({/* … */})
usePinToBottom(result)
// usePinToBottom(result, { enabled: true, slack: 8 })

PinToBottomOptions: { enabled?: boolean; slack?: number }.

useRestoreScrollState(key?) → [scrollState, setScrollState]

Persist virtualizer scroll under a key in window.history.state (Navigation API). Pass the tuple to scrollState / onScrollStateChange. Default key is "scrollState". Requires the Navigation API (Firefox 147+); older browsers should wire a different persist layer (zbugs uses wouter glue).

const [scrollState, setScrollState] = useRestoreScrollState<TaskStart>('tasks')

useQueryVirtualizer({
  /* … */
  scrollState,
  onScrollStateChange: setScrollState,
})

Stage factories

Each stage returns a QueryResult — a Bound query plus optional per-stage useQuery options (ttl, consumer skip, …). Args ride inside the bound value. A null/absent stage from the core is expressed as a falsy query into useQuery, not a placeholder swap.

import type {
  GetPageQuery,
  GetSingleQuery,
  QueryResult,
} from '@doync/query-virtualizer'

// GetPageQueryOptions: { limit, start, dir, settled }
const getPageQuery: GetPageQuery<Task, TaskStart> = ({
  limit,
  start,
  dir,
}) => ({
  query: queries.taskPage({ limit, start, dir }),
  // options: { ttl: 'none' },
})

// GetSingleQueryOptions: { id, settled }
const getSingleQuery: GetSingleQuery<Task> = ({ id }) => ({
  query: queries.taskById(id),
})

| Type | Shape | | --- | --- | | QueryResult<Row, One> | { query: BoundQuery<Row, One>; options?: UseQueryOptions } | | GetPageQuery<TRow, TStartRow> | (GetPageQueryOptions<TStartRow>) => QueryResult<TRow, false> | | GetSingleQuery<TRow> | (GetSingleQueryOptions) => QueryResult<TRow, true> | | GetPageQueryOptions / GetSingleQueryOptions | Re-exported from @rocicorp/zero-virtual/core |

Helpers and shared types

| Symbol | Role | | --- | --- | | rowAttributes(index, key) | Spread onto each virtual row element so measurement / anchoring can find it. | | VirtualRow<TRow> | { index, key, row?: TRow, … } — one entry in items. | | RowKey | Stable row identity type (string \| number). | | AnchoringMode | Scroll-anchoring strategy. | | ScrollHistoryState<TStartRow> | Persisted scroll blob for restore. | | ViewStatus | Re-export from @doync/react — aggregated stage status on the result. |

Public surface

The main entry (.) is governed by semver. Documented public symbols:

  • Runtime: useQueryVirtualizer, useQueryWindowVirtualizer, usePinToBottom, useRestoreScrollState, rowAttributes
  • Types: UseQueryVirtualizerOptions, QueryVirtualizerResult, QueryResult, GetPageQuery, GetSingleQuery, GetPageQueryOptions, GetSingleQueryOptions, PinToBottomOptions, VirtualRow, RowKey, AnchoringMode, ScrollHistoryState, ViewStatus

Internal (@doync/query-virtualizer/internal)

Anything imported from @doync/query-virtualizer/internal is not part of the semver surface. It may change or disappear in any release, including patches, without notice. Sibling @doync/* packages reach internals through that specifier when they must; application code should not. Import the virtualizer hooks from @doync/query-virtualizer.