{"id":48541237,"url":"https://github.com/rocicorp/zero-virtual","last_synced_at":"2026-04-08T04:30:49.651Z","repository":{"id":340992743,"uuid":"1167433688","full_name":"rocicorp/zero-virtual","owner":"rocicorp","description":"Infinite Virtual Scroller for Zero","archived":false,"fork":false,"pushed_at":"2026-03-27T14:15:59.000Z","size":249,"stargazers_count":5,"open_issues_count":0,"forks_count":1,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-27T22:50:16.032Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/rocicorp.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-02-26T09:38:33.000Z","updated_at":"2026-03-27T14:12:06.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/rocicorp/zero-virtual","commit_stats":null,"previous_names":["rocicorp/zero-virtual"],"tags_count":10,"template":false,"template_full_name":null,"purl":"pkg:github/rocicorp/zero-virtual","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rocicorp%2Fzero-virtual","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rocicorp%2Fzero-virtual/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rocicorp%2Fzero-virtual/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rocicorp%2Fzero-virtual/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/rocicorp","download_url":"https://codeload.github.com/rocicorp/zero-virtual/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rocicorp%2Fzero-virtual/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31540824,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-07T16:28:08.000Z","status":"online","status_checked_at":"2026-04-08T02:00:06.127Z","response_time":54,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-04-08T04:30:49.522Z","updated_at":"2026-04-08T04:30:49.605Z","avatar_url":"https://github.com/rocicorp.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# zero-virtual\n\nInfinite virtual scroller for [Zero](https://zero.rocicorp.dev/). Built on top of [Tanstack Virtual](https://tanstack.com/virtual/latest).\n\nFeatures:\n\n- Bidirectional infinite scrolling (load more items at top or bottom)\n- Permalink support (jump to and highlight a specific item by ID)\n- State persistence (restore scroll position across navigation)\n- Dynamic page sizing based on viewport\n\n## Restrictions\n\n- Only fixed row heights are currently supported.\n\n## Usage\n\nThis guide explains how to add `@rocicorp/zero-virtual` to your own Zero app, using the [demo](demo/) as a reference.\n\n### Prerequisites\n\nA working Zero setup. See [Hello Zero](https://github.com/rocicorp/hello-zero) for a minimal starting point.\n\n### Setup\n\n**1. Install**\n\n```sh\nnpm install @rocicorp/zero-virtual\n```\n\n**2. Define your page and single-row queries**\n\n`useZeroVirtualizer` fetches rows in pages and can also look up a single row by ID for permalink support. Define these using Zero's `defineQuery` / `defineQueries` helpers. See [demo/queries.ts](demo/queries.ts) for an example:\n\n```ts\nimport {defineQueries, defineQuery} from '@rocicorp/zero';\nimport {zql} from './schema.ts';\n\nexport type ItemStart = Pick\u003cItem, 'id' | 'created'\u003e;\n\nexport const queries = defineQueries({\n  item: {\n    // Fetches a single item by ID (used for permalink resolution)\n    getSingleQuery: defineQuery(({args: {id}}: {args: {id: string}}) =\u003e\n      zql.item.where('id', id).one(),\n    ),\n\n    // Fetches a page of items given pagination parameters\n    getPageQuery: defineQuery(\n      ({\n        args: {limit, start, dir},\n      }: {\n        args: {\n          limit: number;\n          start: ItemStart | null;\n          dir: 'forward' | 'backward';\n        };\n      }) =\u003e {\n        let q = zql.item\n          .limit(limit)\n          .orderBy('created', dir === 'forward' ? 'desc' : 'asc');\n        if (start) {\n          q = q.start(start, {inclusive: false});\n        }\n        return q;\n      },\n    ),\n  },\n});\n```\n\n**3. Use `useZeroVirtualizer` in your component**\n\n```tsx\nimport {\n  useZeroVirtualizer,\n  useHistoryPermalinkState,\n} from '@rocicorp/zero-virtual/react';\nimport {useCallback, useRef} from 'react';\n\nfunction getRowKey(item: Item) {\n  return item.id;\n}\n\nfunction toStartRow(item: Item): ItemStart {\n  return {id: item.id, created: item.created};\n}\n\nexport function ItemList() {\n  const parentRef = useRef\u003cHTMLDivElement\u003e(null);\n  const [permalinkState, setPermalinkState] =\n    useHistoryPermalinkState\u003cItemStart\u003e();\n\n  const {virtualizer, rowAt} = useZeroVirtualizer({\n    listContextParams: {},\n    getScrollElement: useCallback(() =\u003e parentRef.current, []),\n    estimateSize: useCallback(() =\u003e 48, []),\n    getRowKey,\n    toStartRow,\n    getPageQuery: useCallback(\n      ({limit, start, dir}) =\u003e ({\n        query: queries.item.getPageQuery({limit, start, dir}),\n      }),\n      [],\n    ),\n    getSingleQuery: useCallback(\n      ({id}) =\u003e ({\n        query: queries.item.getSingleQuery({id}),\n      }),\n      [],\n    ),\n    permalinkState,\n    onPermalinkStateChange: setPermalinkState,\n  });\n\n  const virtualItems = virtualizer.getVirtualItems();\n\n  return (\n    \u003cdiv ref={parentRef} style={{overflow: 'auto', height: '100vh'}}\u003e\n      \u003cdiv style={{height: virtualizer.getTotalSize(), position: 'relative'}}\u003e\n        {virtualItems.map(virtualRow =\u003e {\n          const row = rowAt(virtualRow.index);\n          return (\n            \u003cdiv\n              key={virtualRow.key}\n              data-index={virtualRow.index}\n              style={{\n                position: 'absolute',\n                transform: `translateY(${virtualRow.start}px)`,\n              }}\n            \u003e\n              {row ? row.title : 'Loading...'}\n            \u003c/div\u003e\n          );\n        })}\n      \u003c/div\u003e\n    \u003c/div\u003e\n  );\n}\n```\n\n### Query functions\n\nQuery functions receive an options object and return a `QueryResult`:\n\n```ts\ntype GetPageQueryOptions\u003cTStartRow\u003e = {\n  limit: number;\n  start: TStartRow | null;\n  dir: 'forward' | 'backward';\n  settled: boolean;\n};\n\ntype GetSingleQueryOptions = {\n  id: string;\n  settled: boolean;\n};\n\ntype QueryResult\u003cTReturn\u003e = {query: ...; options?: UseQueryOptions};\n```\n\nThe `settled` flag indicates whether the list has been idle for `settleTime` ms (default 2000). Use this to vary query options based on scroll state — for example, using a shorter TTL while scrolling and a longer one when settled:\n\n```ts\ngetPageQuery: ({limit, start, dir, settled}) =\u003e ({\n  query: queries.item.getPageQuery({limit, start, dir}),\n  options: {ttl: settled ? '5m' : '10s'},\n}),\n```\n\n### Scroll settling\n\n`useZeroVirtualizer` tracks whether the user has stopped scrolling:\n\n- **`settled`** (returned) — `true` when the list has been idle for `settleTime` ms\n- **`settleTime`** (option) — how long to wait before considering the list settled (default 2000ms)\n- **`onSettled`** (option) — callback fired when `settled` transitions to `true`, useful for deferred side effects like syncing search params to the URL\n\n### `useHistoryPermalinkState`\n\nA ready-made hook that persists virtualizer scroll/pagination state in `window.history.state`, so back/forward navigation restores position automatically:\n\n```ts\nconst [permalinkState, setPermalinkState] =\n  useHistoryPermalinkState\u003cMyStartRow\u003e();\n```\n\nPass a custom `key` if you have multiple virtualizers on the same page:\n\n```ts\nconst [state, setState] = useHistoryPermalinkState\u003cMyStartRow\u003e('myList');\n```\n\nFor a complete working example including sorting, permalinks, and scroll-position persistence, see [demo/App.tsx](demo/App.tsx).\n\n## Running the demo\n\nFirst, install dependencies from the repo root:\n\n```sh\npnpm i\n```\n\nThen `cd` into the demo directory for the remaining steps:\n\n```sh\ncd demo\n```\n\nRun Docker:\n\n```sh\npnpm dev:db-up\n```\n\n**In a second terminal**, run the zero-cache server:\n\n```sh\ncd demo\npnpm dev:zero-cache\n```\n\n**In a third terminal**, run the Vite dev server:\n\n```sh\ncd demo\npnpm dev:ui\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frocicorp%2Fzero-virtual","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frocicorp%2Fzero-virtual","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frocicorp%2Fzero-virtual/lists"}