Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .changeset/pre.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"mode": "pre",
"tag": "rc",
"initialVersions": {
"@tanstack/virtual-benchmarks": "0.0.26",
"@tanstack/angular-virtual": "6.1.9",
"@tanstack/lit-virtual": "3.15.0",
"@tanstack/marko-virtual": "3.16.9",
"@tanstack/react-virtual": "3.14.23",
"@tanstack/solid-virtual": "3.13.50",
"@tanstack/svelte-virtual": "3.13.49",
"@tanstack/virtual-core": "3.19.8",
"@tanstack/vue-virtual": "3.13.49"
},
"changesets": []
}
9 changes: 9 additions & 0 deletions .changeset/solid-virtual-solid-2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@tanstack/solid-virtual': major
---

Support Solid 2. `@tanstack/solid-virtual` now requires `solid-js` 2 (`>=2.0.0-rc.19 <3.0.0`); stay on 3.x for Solid 1.

- `createVirtualizer` and `createWindowVirtualizer` keep their API. Option changes are tracked by a render effect, and the virtualizer mounts in `onSettled`.
- `getVirtualItems()` returns a store reconciled by `index`, so rows keep their identity while scrolling and measuring, and `getTotalSize()` stays a reactive read.
- As with every Solid 2 signal, values read right after a write outside a reactive scope update after the next flush (`flush()` from `solid-js`, or the next microtask).
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: Release

on:
push:
branches: [main, alpha, beta]
branches: [main, alpha, beta, '*-pre']

concurrency:
group: ${{ github.workflow }}-${{ github.event.number || github.ref }}
Expand Down
2 changes: 2 additions & 0 deletions docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ npm install @tanstack/react-virtual
npm install @tanstack/solid-virtual
```

`@tanstack/solid-virtual` 4.x works with Solid 2. For Solid 1, install `@tanstack/solid-virtual@3`.

## Svelte Virtual

```bash
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"test:pr": "nx affected --targets=test:sherif,test:knip,test:docs,test:eslint,test:lib,test:e2e,test:types,test:build,build",
"test:ci": "nx run-many --targets=test:sherif,test:knip,test:docs,test:eslint,test:lib,test:e2e,test:types,test:build,build",
"test:eslint": "nx affected --target=test:eslint",
"test:sherif": "sherif",
"test:sherif": "sherif -i vite -i @tanstack/vite-config",
"test:lib": "nx affected --target=test:lib --exclude=examples/**",
"test:lib:dev": "pnpm run test:lib && nx watch --all -- pnpm run test:lib",
"test:build": "nx affected --target=test:build --exclude=examples/**",
Expand Down
10 changes: 7 additions & 3 deletions packages/solid-virtual/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,14 @@
"@tanstack/virtual-core": "workspace:*"
},
"devDependencies": {
"solid-js": "^1.9.7",
"vite-plugin-solid": "^2.11.6"
"@solidjs/vite-plugin": "^3.0.0-next.51",
"@solidjs/web": "^2.0.0-rc.19",
"@tanstack/vite-config": "0.6.0",
"solid-js": "^2.0.0-rc.19",
"vite": "^8.3.0",
"vitest": "^4.1.4"
},
"peerDependencies": {
"solid-js": "^1.3.0"
"solid-js": ">=2.0.0-rc.19 <3.0.0"
}
}
87 changes: 52 additions & 35 deletions packages/solid-virtual/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@ import {
} from '@tanstack/virtual-core'

import {
createComputed,
createMemo,
createRenderEffect,
createSignal,
mergeProps,
onCleanup,
onMount,
createStore,
merge,
onSettled,
untrack,
} from 'solid-js'
import { createStore, reconcile } from 'solid-js/store'
import type { PartialKeys, VirtualizerOptions } from '@tanstack/virtual-core'

export * from '@tanstack/virtual-core'
Expand All @@ -27,16 +28,35 @@ function createVirtualizerBase<
options: VirtualizerOptions<TScrollElement, TItemElement>,
): Virtualizer<TScrollElement, TItemElement> {
const resolvedOptions: VirtualizerOptions<TScrollElement, TItemElement> =
mergeProps(options)
merge(options)

const instance = new Virtualizer<TScrollElement, TItemElement>(
resolvedOptions,
// The constructor reads every option once; the options effect below is
// what tracks them.
const instance = untrack(
() => new Virtualizer<TScrollElement, TItemElement>(resolvedOptions),
)

const [virtualItems, setVirtualItems] = createStore(
instance.getVirtualItems(),
// The virtualizer reports changes from scroll and resize observers and from
// inside the options effect below; each report bumps `version`, which the
// item projection (reconciled by index) and the total size re-read.
const [version, setVersion] = createSignal(0, {
equals: false,
ownedWrite: true,
})
const [virtualItems] = createStore(
() => {
version()
return instance.getVirtualItems()
},
[],
{ key: 'index' },
)
const [totalSize, setTotalSize] = createSignal(instance.getTotalSize())
const totalSize = createMemo(() => {
version()
return instance.getTotalSize()
})

const sync = () => setVersion(0)

const handler = {
get(
Expand All @@ -55,35 +75,32 @@ function createVirtualizerBase<
}

const virtualizer = new Proxy(instance, handler)
virtualizer.setOptions(resolvedOptions)

onMount(() => {
const cleanup = virtualizer._didMount()
virtualizer._willUpdate()
onCleanup(cleanup)
})

createComputed(() => {
virtualizer.setOptions(
mergeProps(resolvedOptions, options, {
// Reading every option here tracks them; the effect half then applies the
// snapshot to the core virtualizer.
createRenderEffect(
() => ({ ...resolvedOptions }),
(snapshot) => {
virtualizer.setOptions({
...snapshot,
onChange: (
instance: Virtualizer<TScrollElement, TItemElement>,
sync: boolean,
isSync: boolean,
) => {
instance._willUpdate()
setVirtualItems(
reconcile(instance.getVirtualItems(), {
key: 'index',
}),
)
setTotalSize(instance.getTotalSize())
options.onChange?.(instance, sync)
sync()
snapshot.onChange?.(instance, isSync)
},
}),
)
})
virtualizer._willUpdate()
sync()
},
)

onSettled(() => {
const cleanup = virtualizer._didMount()
virtualizer._willUpdate()
setVirtualItems(reconcile(instance.getVirtualItems(), { key: 'index' }))
setTotalSize(instance.getTotalSize())
return cleanup
})

return virtualizer
Expand All @@ -99,7 +116,7 @@ export function createVirtualizer<
>,
): Virtualizer<TScrollElement, TItemElement> {
return createVirtualizerBase<TScrollElement, TItemElement>(
mergeProps(
merge(
{
observeElementRect: observeElementRect,
observeElementOffset: observeElementOffset,
Expand All @@ -120,7 +137,7 @@ export function createWindowVirtualizer<TItemElement extends Element>(
>,
): Virtualizer<Window, TItemElement> {
return createVirtualizerBase<Window, TItemElement>(
mergeProps(
merge(
{
getScrollElement: () =>
typeof document !== 'undefined' ? window : null,
Expand Down
66 changes: 53 additions & 13 deletions packages/solid-virtual/tests/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,68 @@
import { expect, test } from 'vitest'
import { createRoot, createSignal } from 'solid-js'
import { createRoot, createSignal, flush } from 'solid-js'

import { createVirtualizer } from '../src/index'

test('preserves measured sizes when reactive options change', () => {
createRoot((dispose) => {
const [count, setCount] = createSignal(2)
const virtualizer = createVirtualizer<HTMLDivElement, HTMLDivElement>({
const [count, setCount] = createSignal(2)
const { virtualizer, dispose } = createRoot((dispose) => ({
dispose,
virtualizer: createVirtualizer<HTMLDivElement, HTMLDivElement>({
get count() {
return count()
},
getScrollElement: () => null,
estimateSize: () => 60,
initialRect: { width: 800, height: 600 },
})
}),
}))
flush()

expect(virtualizer.getTotalSize()).toBe(120)
virtualizer.resizeItem(0, 100)
expect(virtualizer.getTotalSize()).toBe(160)
expect(virtualizer.getTotalSize()).toBe(120)
virtualizer.resizeItem(0, 100)
flush()
expect(virtualizer.getTotalSize()).toBe(160)

setCount(3)
setCount(3)
flush()

expect(virtualizer.itemSizeCache.get(0)).toBe(100)
expect(virtualizer.getTotalSize()).toBe(220)
dispose()
})
expect(virtualizer.itemSizeCache.get(0)).toBe(100)
expect(virtualizer.getTotalSize()).toBe(220)
dispose()
})

test('reconciles virtual items by index', () => {
const [count, setCount] = createSignal(2)
const { virtualizer, dispose } = createRoot((dispose) => ({
dispose,
virtualizer: createVirtualizer<HTMLDivElement, HTMLDivElement>({
get count() {
return count()
},
getScrollElement: () => null,
estimateSize: () => 60,
initialRect: { width: 800, height: 600 },
}),
}))
flush()

const items = virtualizer.getVirtualItems()
const first = items[0]
expect(items.map((item) => item.index)).toEqual([0, 1])

setCount(3)
flush()

expect(virtualizer.getVirtualItems().map((item) => item.index)).toEqual([
0, 1, 2,
])
expect(virtualizer.getVirtualItems()[0]).toBe(first)

virtualizer.resizeItem(0, 100)
flush()

expect(virtualizer.getVirtualItems()[0]).toBe(first)
expect(first.size).toBe(100)
expect(virtualizer.getVirtualItems()[1]!.start).toBe(100)
dispose()
})
2 changes: 1 addition & 1 deletion packages/solid-virtual/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"extends": "../../tsconfig.json",
"compilerOptions": {
"jsx": "preserve",
"jsxImportSource": "solid-js"
"jsxImportSource": "@solidjs/web"
},
"include": ["src", "vite.config.ts"]
}
2 changes: 1 addition & 1 deletion packages/solid-virtual/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { defineConfig, mergeConfig } from 'vitest/config'
import { tanstackViteConfig } from '@tanstack/vite-config'
import solid from 'vite-plugin-solid'
import solid from '@solidjs/vite-plugin'

const config = defineConfig({
plugins: [solid()],
Expand Down
Loading