Store Pattern Migration:
- Created createVirtualizerStore using Svelte stores (writable/derived)
- Replaced useVirtualList hook with createVirtualizerStore
- Matches existing store patterns (createFilterStore, createControlStore)
- More Svelte-idiomatic than React-inspired hook pattern
Component Refactoring:
- Renamed FontVirtualList.svelte → VirtualList.svelte
- Moved component from shared/virtual/ → shared/ui/
- Updated to use store pattern instead of hook
- Removed pixel values from style tags (uses Tailwind CSS)
- Height now configurable via Tailwind classes (e.g., 'h-96', 'h-[500px]')
- Props changed from shorthand {fonts} to explicit items prop
File Changes:
- Deleted: useVirtualList.ts (replaced by store pattern)
- Deleted: FontVirtualList.svelte (renamed and moved)
- Deleted: useVirtualList.test.ts (updated to test store pattern)
- Updated: README.md with store pattern usage examples
- Updated: index.ts with migration guide
- Created: createVirtualizerStore.ts in shared/store/
- Created: VirtualList.svelte in shared/ui/
- Created: createVirtualizerStore.test.ts
- Created: barrel exports (shared/store/index.ts, shared/ui/index.ts)
Styling Improvements:
- All pixel values removed from <style> tags
- Uses Tailwind CSS for all styling
- Responsive height via Tailwind classes or props
- Only inline styles for dynamic positioning (required for virtualization)
TypeScript & Testing:
- Full TypeScript support with generics
- All 33 tests passing
- Type checking passes
- Linting passes (minor warnings only)
Breaking Changes:
- Component name: FontVirtualList → VirtualList
- Component location: $shared/virtual → $shared/ui
- Hook removed: useVirtualList → createVirtualizerStore
- Props change: {fonts} shorthand → items prop
- Import changes: $shared/virtual → $shared/ui and $shared/store
Documentation:
- Updated README.md with store pattern examples
- Added migration guide in virtual/index.ts
- Documented breaking changes and migration steps
Virtualization - Store Pattern Implementation
This folder contains the virtualization layer for smooth 60FPS scrolling with large font collections.
Updated: Now uses Svelte 5 rune-based store pattern instead of React-inspired hooks.
Files
Store
- createVirtualizerStore.ts: Svelte 5 rune-based store for virtualized lists
Component
- VirtualList.svelte (moved to
shared/ui/): Generic virtualized list component
Why Store Pattern?
The store pattern is more idiomatic for Svelte than React-inspired hooks:
- More Svelte-native: Stores are core to Svelte, hooks are React-specific
- Better reactivity: Stores auto-derive values using
$derived, hooks need manual updates - Consistent with project patterns: Matches
createFilterStoreandcreateControlStore - More extensible: Easy to add store methods and computed values
- Type-safe: Full TypeScript generics support
Usage
Basic Usage with Store
<script lang="ts">
import { createVirtualizerStore } from '$shared/store';
const fonts = $state<UnifiedFont[]>(fontData);
const virtualizer = createVirtualizerStore({
count: fonts.length,
estimateSize: () => 80,
overscan: 5,
getItemKey: index => fonts[index].id,
});
const virtualItems = $derived(() => virtualizer.virtualItems);
const totalSize = $derived(() => virtualizer.totalSize);
</script>
<div bind:this={virtualizer.scrollElement} class="h-96 overflow-auto">
<div style="height: {totalSize}px; position: relative;">
{#each virtualItems as item (item.key)}
<div
style="position: absolute; top: {item.start}px; height: {item.size}px; width: 100%;"
role="option"
aria-selected="false"
>
<FontListItem font={fonts[item.index]} />
</div>
{/each}
</div>
</div>
Using VirtualList Component
<script lang="ts">
import type { UnifiedFont } from '$entities/Font';
import { FontListItem } from '$entities/Font/ui';
import { VirtualList } from '$shared/ui';
const fonts = $state<UnifiedFont[]>(fontData);
</script>
<VirtualList
items={fonts}
itemHeight={80}
height="h-96"
let:item
let:index
>
<FontListItem {item} {index} />
</VirtualList>
API Reference
createVirtualizerStore
function createVirtualizerStore(options: VirtualizerOptions): VirtualizerStore;
Options:
count: Number of items (required)estimateSize: Function returning estimated height for each item (required)overscan: Number of items to render beyond viewport (default: 5)getItemKey: Function to get stable key for each itemscrollMargin: Scroll offset threshold (in pixels)
Returns:
virtualItems: Array of visible virtual items (reactive getter)totalSize: Total height of all items (reactive getter)scrollOffset: Current scroll offset (reactive getter)scrollToIndex: Scroll to specific item indexscrollToOffset: Scroll to specific pixel offsetmeasureElement: Manually measure item elementscrollElement: Reference to scroll element (bindable)
VirtualList Component Props
interface VirtualListProps<T> {
items: T[]; // Items to virtualize
itemHeight?: number | ((index: number) => number; // Item height (default: 80)
overscan?: number; // Overscan items (default: 5)
height?: string; // Container height class (default: "h-96")
scrollMargin?: number; // Scroll margin
class?: string; // CSS class name
getItemKey?: (item: T, index: number) => string | number; // Key function
}
Slots:
let:item: Current itemlet:index: Current item index
Key Features
- Renders only visible items (50-100 max) regardless of total count
- Maintains 60FPS scrolling with 10,000+ items
- Minimal memory usage
- Smooth scrolling without jank
- ARIA roles for accessibility
- Keyboard navigation support
- Customizable overscan for smoother scrolling
- Stable keys for efficient re-rendering
- Responsive height using Tailwind CSS classes
- No pixel-based styling in
<style>tags
Styling
The component uses Tailwind CSS for all styling:
- Height: Use Tailwind classes like
h-96,h-[500px],h-[calc(100vh-200px)] - Responsive: Use responsive classes like
h-96 md:h-[700px] - Custom: Pass custom CSS classes via
classprop
No pixel values in <style> tags - all styling is done through Tailwind utility classes or inline styles for dynamic positioning (which is required for virtualization).
Performance
The virtualization ensures:
- Minimal DOM nodes: Only visible items are rendered
- Smooth scrolling: Overscan reduces blank space during fast scrolling
- Efficient updates: TanStack Virtual optimizes item updates
- Memory efficient: Constant memory usage regardless of dataset size
Testing
Run unit tests:
yarn test:unit src/shared/store/createVirtualizerStore.test.ts
Run E2E tests (with component):
yarn test:e2e
Migration from Hook Pattern
Old (hook):
const { virtualItems, totalSize } = useVirtualList({
items: fonts,
scrollElement,
estimateSize: () => 80,
});
New (store):
const virtualizer = createVirtualizerStore({
count: fonts.length,
estimateSize: () => 80,
});
const virtualItems = $derived(() => virtualizer.virtualItems);
const totalSize = $derived(() => virtualizer.totalSize);
Key differences:
- Use
countinstead ofitemsarray - Store created once, reactive values accessed via getters
- Bind
scrollElementproperty instead of passing in options - Use
$derivedfor reactive values in Svelte 5
Breaking Changes from Phase 2
- Component renamed:
FontVirtualList→VirtualList - Component moved:
shared/virtual/→shared/ui/ - Hook removed:
useVirtualListreplaced withcreateVirtualizerStore - Props changed:
itemsprop (was{fonts}shorthand) - Styling: Removed pixel values from
<style>tags, use Tailwind classes
Examples
Dynamic Item Height
<VirtualList
{fonts}
itemHeight={(index => fonts[index].isFeatured ? 120 : 80)}
height="h-96"
let:item
>
<FontListItem font={item} />
</VirtualList>
Custom Item Keys
<VirtualList
{fonts}
itemHeight={80}
getItemKey={(font => font.id)}
let:item
>
<FontListItem {item} />
</VirtualList>
Responsive Height
<VirtualList
{fonts}
itemHeight={80}
height="h-[500px] md:h-[700px] lg:h-[800px]"
let:item
>
<FontListItem {item} />
</VirtualList>
Scroll Control
<script>
const virtualizer = createVirtualizerStore({
count: fonts.length,
estimateSize: () => 80,
});
function scrollToTop() {
virtualizer.scrollToIndex(0);
}
function scrollToBottom() {
virtualizer.scrollToIndex(fonts.length - 1);
}
</script>
<div>
<button on:click={scrollToTop}>Top</button>
<button on:click={scrollToBottom}>Bottom</button>
</div>