---
phase: 28
plan: 01
subsystem: performance
tags: [vu-meters, external-store, raf-batching, useSyncExternalStore, react-performance]
requires: []
provides:
  - external-vu-store
  - vu-store-hooks
  - raf-batching-infrastructure
affects: [28-02, 28-03]
tech-stack:
  added:
    - use-sync-external-store: "1.6.0"
  patterns:
    - external-store-with-raf-batching
    - useSyncExternalStore-hooks
key-files:
  created:
    - jam-ui/src/stores/vuStore.js
    - jam-ui/src/hooks/useVuStore.js
  modified:
    - jam-ui/package.json
    - jam-ui/package-lock.json
decisions:
  - id: external-store-pattern
    what: Use external store with RAF batching for VU data
    why: Native client sends 50-70 updates/sec which overwhelms React reconciliation
    impact: Limits VU updates to 60fps, eliminates React state overhead
  - id: useSyncExternalStore-shim
    what: Use use-sync-external-store npm package
    why: React 16 compatibility (project uses React 14.21.3)
    impact: Enables safe external store subscription without concurrent rendering bugs
  - id: dual-hook-api
    what: Provide both useAllVuLevels and useVuLevel hooks
    why: useAllVuLevels convenient but inefficient, useVuLevel optimized for single mixer
    impact: Components can choose granularity based on needs
metrics:
  duration: 3 minutes
  completed: 2026-03-03
---

# Phase 28 Plan 01: External VU Store Infrastructure Summary

**One-liner:** External VU store with RAF batching limits 50-70 updates/sec to 60fps using useSyncExternalStore shim

## What Was Built

Created foundational infrastructure for high-performance VU meter updates by moving VU data storage outside React state management.

**Core components:**
1. **vuStore.js** - External store module with requestAnimationFrame batching
2. **useVuStore.js** - React hooks wrapping useSyncExternalStore for safe subscription
3. **use-sync-external-store dependency** - Official React shim for React 16 compatibility

## Implementation Details

### External VU Store (vuStore.js)

**State management:**
- `vuLevels` - Map of mixerId to `{ level, clipping, timestamp }`
- `pendingUpdates` - Buffer for incoming updates before RAF flush
- `listeners` - Set of subscriber callbacks
- `rafId` - Animation frame ID for cleanup

**Key methods:**
- `updateLevel(mixerId, level, clipping)` - Buffers update without immediate emission
- `removeLevel(mixerId)` - Removes mixer, emits change immediately
- `getSnapshot()` - Returns vuLevels reference (same object if unchanged)
- `getLevelSnapshot(mixerId)` - Returns single mixer's data for efficient subscription
- `subscribe(listener)` - Adds listener, starts RAF loop on first subscriber
- `reset()` - Clears all state for session cleanup

**RAF batching behavior:**
- `flushUpdates()` runs on animation frame loop at 60fps
- Only creates new vuLevels object when pendingUpdates has entries
- Automatically starts when first subscriber added
- Automatically stops when last subscriber removed

### React Hooks (useVuStore.js)

**useAllVuLevels:**
- Subscribes to entire vuLevels object
- Re-renders on any VU change
- Use sparingly - only when all levels needed

**useVuLevel(mixerId):**
- Subscribes to single mixer's data
- More efficient - only re-renders when specific mixer changes
- getSnapshot callback memoized with useCallback to prevent resubscription

## Technical Architecture

**Data flow:**
```
Native C++ client (50-70/sec)
  ↓
vuStore.updateLevel() (buffers in pendingUpdates)
  ↓
RAF loop (60fps) → flushUpdates()
  ↓
Merge into vuLevels → emitChange()
  ↓
useSyncExternalStore subscribers (React components)
```

**Why this works:**
- Native updates come faster than display can render (50-70/sec vs 60fps max)
- Buffering in pendingUpdates prevents React from processing every native update
- RAF synchronizes with browser display refresh (60fps)
- useSyncExternalStore allows React to subscribe without useState overhead

## Deviations from Plan

None - plan executed exactly as written.

## Testing Notes

**Manual verification:**
- ✅ use-sync-external-store@1.6.0 installed in package.json
- ✅ vuStore.js exports vuStore with all required methods
- ✅ useVuStore.js exports useAllVuLevels and useVuLevel
- ✅ Hooks import from 'use-sync-external-store/shim' (correct path)

**Integration testing (deferred to 28-02):**
- VU meter components will test actual subscription behavior
- Performance profiling will verify 60fps limit
- React DevTools will confirm no state-based re-renders

## Decisions Made

**1. External store over Redux for VU data**
- **Context:** Native sends 50-70 updates/sec
- **Decision:** Store VU data outside React entirely
- **Rationale:** Redux actions/reducers add middleware overhead, DevTools would overwhelm
- **Impact:** VU updates bypass React reconciliation completely

**2. useSyncExternalStore shim from npm**
- **Context:** Project uses React 14.21.3, useSyncExternalStore added in React 18
- **Decision:** Install official React team shim package
- **Rationale:** Handles concurrent rendering edge cases, officially maintained
- **Impact:** Safe external store subscription without hand-rolling solution

**3. RAF batching centralized in store**
- **Context:** Could batch per-component or in store
- **Decision:** Single RAF loop in store
- **Rationale:** Avoids multiple rAF loops, ensures consistent timing across all meters
- **Impact:** More efficient, easier to reason about performance

## Next Phase Readiness

**Ready for Phase 28-02 (Integrate VU Store):**
- ✅ External store infrastructure complete
- ✅ React hooks ready for component integration
- ✅ No blockers identified

**Required for next plan:**
- Modify useMixerStore.js to call vuStore.updateLevel instead of dispatch
- Update VU meter components to use useVuLevel hook
- Verify no VU-related Redux state remains

**Potential issues:**
- None identified - infrastructure is standalone and non-breaking

## Key Files Created

**jam-ui/src/stores/vuStore.js (145 lines)**
- External store with RAF batching
- API: updateLevel, removeLevel, getSnapshot, getLevelSnapshot, subscribe, reset
- Pattern: Buffered updates + animation frame synchronization

**jam-ui/src/hooks/useVuStore.js (60 lines)**
- useAllVuLevels - Subscribe to all VU levels
- useVuLevel(mixerId) - Subscribe to single mixer
- Uses useSyncExternalStore shim for React 16

**jam-ui/package.json**
- Added dependency: use-sync-external-store@1.6.0

## Knowledge for Future Phases

**Performance characteristics:**
- RAF loop only runs when subscribers exist (auto start/stop)
- getSnapshot must return same reference if unchanged (Object.is comparison)
- pendingUpdates pattern prevents object creation on every native update

**Usage patterns:**
- Prefer useVuLevel over useAllVuLevels for single meter components
- Always clean up with vuStore.removeLevel on component unmount
- Call vuStore.reset() on session end to prevent memory leaks

**Integration points:**
- useMixerStore.js handleBridgeCallback - should call vuStore.updateLevel
- VU meter components - should use useVuLevel hook
- VuContext.js - can be simplified to provide vuStore reference

## Commits

- `5da85c1` - feat(28-01): create external VU store with RAF batching
- `8344721` - feat(28-01): create useVuStore React hooks
