Pinia Store Architecture in RomM v2: Modular State Management for High-Performance Galleries

RomM v2 uses four specialized Pinia stores—v2GalleryRoms, v2GallerySelection, v2ScrollRestoration, and v2FocusRestoration—to manage sparse window-based ROM caching, multi-selection state, and UI restoration in a modular, high-performance architecture.

The frontend rewrite in RomM v2 (available at rommapp/romm) replaces the monolithic state management of v1 with a targeted Pinia store architecture. These stores handle distinct concerns—gallery data, selection, scroll position, and focus—enabling efficient virtualized grids and seamless navigation. This architecture leverages Vue 3's reactivity while maintaining strict separation from legacy v1 stores.

Core Pinia Stores in RomM v2

Located in frontend/src/v2/stores/galleryRoms.ts, this store maintains a sparse, window-based cache of ROMs rather than loading entire collections. It tracks the current gallery context (platform, collection, virtual collection, smart collection, or search) and manages pagination via charIndex and romIdIndex.

Core state includes:

  • currentPlatform, currentCollection, currentVirtualCollection, currentSmartCollection, currentSearch
  • byPosition: A Map<number, SimpleRom> linking positions to ROM data
  • loadedWindows, pendingWindows, failedWindows: Sets tracking fetch status
  • orderBy, orderDir: Sorting parameters

Key actions:

  • setCurrentPlatform(), setCurrentCollection(), setOrderBy(): Context switchers that trigger resetGallery() and invalidateWindows()
  • fetchInitialMetadata(): Retrieves total counts and filter indices
  • fetchWindowAt(position), fetchRomAt(position): On-demand data loading with WINDOW_SIZE = 72
  • cancelFetchAt(): Uses AbortController instances stored in inFlightControllers to cancel stale requests

v2GallerySelection – Multi-Select State Management

Defined in frontend/src/v2/stores/gallerySelection.ts, this store handles multi-selection independently from the ROM cache. It stores full SimpleRom objects in a Map<id, SimpleRom> to avoid re-lookup after window eviction.

State:

  • selected: Map of ROM IDs to objects
  • lastSelectedPosition: Anchor for shift-click range selections

Actions:

  • toggle(rom, position): Single selection toggle
  • toggleRange(position, getRomAt): Shift-click range selection using the anchor
  • selectAllLoaded(), clear(), removeIds()

Getters:

  • enabled, count, roms, ids

v2ScrollRestoration – Route-Aware Scroll Persistence

This store (frontend/src/v2/stores/scrollRestoration.ts) persists scroll positions for virtual scrollers across route changes.

State:

  • positions: Map<string, number> (routeFullPath → scrollTop)

Actions:

  • save(routeFullPath, scrollTop): Stores position before navigation
  • restore(routeFullPath): Returns saved position or null
  • clear(routeFullPath): Removes specific entry

v2FocusRestoration – Keyboard Navigation State

Found in frontend/src/v2/stores/focusRestoration.ts, this enables gamepad and keyboard navigation to resume after route changes.

State:

  • keys: Map<string, string> (routeFullPath → focusKey)

Actions:

  • save(routeFullPath, focusKey): Stores last focused tile
  • restore(routeFullPath): Retrieves focus key for re-focus

Store Interactions and Data Flow

The stores communicate through well-defined actions rather than direct state mutation:

  1. Gallery Context Changes: When users switch platforms or collections, v2GalleryRoms actions like setCurrentPlatform() reset the cache via resetGallery() and fetch initial metadata through fetchInitialMetadata().

  2. Virtual Scrolling: Components call getRomAt(position) from v2GalleryRoms. If data is missing, they trigger fetchRomAt(position) or fetchWindowAt(position), which updates the byPosition Map reactively.

  3. Selection Handling: Tile components invoke v2GallerySelection.toggle(rom, position) on click. For range selections, toggleRange(position, galleryRoms.getRomAt) uses the ROM store's getter to fetch boundary items.

  4. Navigation Restoration: Before route leave, scrollRestoration.save() and focusRestoration.save() cache UI state. On mount, restore() methods reinitialize the scroller and focus target.

  5. Request Deduplication: v2GalleryRoms maintains a Map<string, AbortController> to track in-flight requests, ensuring rapid filter changes cancel outdated fetches via cancelFetchAt().

Implementation Details and Performance Optimizations

Windowed Caching Strategy: Instead of loading thousands of ROMs, v2GalleryRoms fetches windows of 72 items (matching the backend pagination limit). This reduces initial load time and memory pressure significantly.

Reactive Map Usage: Both byPosition and selected use native JavaScript Map objects. Vue 3's reactive Map support ensures component updates trigger only for specific key changes, crucial for virtualized grids rendering hundreds of cells.

Abort Controller Management: The store centralizes network cancellation through inFlightControllers. When fetchWindowAt() initiates a request, it stores an AbortController; subsequent calls to cancelFetchAt() or new fetches abort stale requests, preventing race conditions.

Immutable Store Boundaries: Selection stores retain full ROM objects to survive window eviction, while gallery stores maintain sparse references. This separation prevents selection loss during aggressive cache trimming.

Practical Code Examples

Accessing the ROM store and initializing gallery data:

import { useStore as useGalleryRoms } from '@/stores/roms'

const galleryRoms = useGalleryRoms()

onMounted(() => {
  // Load first window and metadata (total count, filters)
  galleryRoms.fetchInitialMetadata()
})

Lazy-loading ROM data in virtual scroller components:

function getRom(position: number) {
  const rom = galleryRoms.getRomAt(position)
  if (!rom) {
    // Triggers fetch; store handles deduplication
    galleryRoms.fetchRomAt(position)
  }
  return rom
}

Handling selection toggles in tile components:

import useGallerySelection from '@/stores/gallerySelection'

const selection = useGallerySelection()

function onTileClick(rom: SimpleRom, position: number) {
  selection.toggle(rom, position)
}

Implementing shift-click range selection:

function onTileShiftClick(position: number) {
  // galleryRoms.getRomAt passed as resolver
  selection.toggleRange(position, (p) => galleryRoms.getRomAt(p))
}

Saving scroll position before route navigation:

import useScrollRestoration from '@/stores/scrollRestoration'

function onBeforeRouteLeave(to, from, next) {
  const container = document.querySelector('.gallery-scroller') as HTMLElement
  useScrollRestoration().save(from.fullPath, container.scrollTop)
  next()
}

onMounted(() => {
  const saved = useScrollRestoration().restore(route.fullPath)
  if (saved !== null) {
    const container = document.querySelector('.gallery-scroller') as HTMLElement
    container.scrollTop = saved
  }
})

Persisting and restoring focus for keyboard navigation:

import useFocusRestoration from '@/stores/focusRestoration'

function onTileFocus(e: FocusEvent) {
  const focusKey = (e.target as HTMLElement).dataset.focusKey
  if (focusKey) useFocusRestoration().save(route.fullPath, focusKey)
}

onMounted(() => {
  const key = useFocusRestoration().restore(route.fullPath)
  if (key) {
    const el = document.querySelector(`[data-focus-key="${key}"]`) as HTMLElement
    el?.focus()
  }
})

Summary

  • Four specialized stores (v2GalleryRoms, v2GallerySelection, v2ScrollRestoration, v2FocusRestoration) replace the monolithic v1 architecture in rommapp/romm
  • Sparse window-based caching in v2GalleryRoms loads only 72-item windows on demand, minimizing memory usage
  • Independent selection state stores full ROM objects to survive cache eviction and enable shift-click range selection
  • Navigation restoration persists scroll positions and focus keys per route using simple Map-based stores
  • AbortController integration prevents race conditions during rapid gallery context switches
  • Vue 3 Map reactivity ensures precise component updates without full re-renders in virtualized grids

Frequently Asked Questions

How does v2GalleryRoms handle pagination without loading all ROMs?

The store implements a sparse cache using a Map<number, SimpleRom> called byPosition. It fetches data in 72-item windows via fetchWindowAt() when the virtual scroller requests specific positions. This windowed approach, defined in frontend/src/v2/stores/galleryRoms.ts, keeps memory usage constant regardless of collection size.

Why does v2GallerySelection store full ROM objects instead of just IDs?

Storing complete SimpleRom objects in the selected Map ensures selections persist even when v2GalleryRoms evicts windows from its sparse cache. This decoupling prevents selection loss during aggressive memory management and eliminates the need to re-fetch ROM metadata for the selection bar.

How does RomM v2 cancel outdated API requests when switching galleries quickly?

The v2GalleryRoms store maintains an inFlightControllers Map that tracks AbortController instances for each pending fetch. When contexts change rapidly, cancelFetchAt() or new fetch actions abort previous requests, preventing race conditions and reducing server load.

What is the difference between v2 stores and the legacy v1 stores?

Stores prefixed with v2 (located in frontend/src/v2/stores/) belong to the rewrite and follow a modular, domain-specific architecture. Legacy v1 stores remain frozen in frontend/src/stores/ for backward compatibility. The v2 naming convention allows both implementations to coexist during migration, with v2 stores specifically optimized for virtualized galleries and windowed data fetching.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →