From 834472127d1f5d8541f232f3dc5107614b0b3626 Mon Sep 17 00:00:00 2001 From: Nuwan Date: Tue, 3 Mar 2026 20:20:26 +0530 Subject: [PATCH] feat(28-01): create useVuStore React hooks - Add useAllVuLevels hook for subscribing to all VU levels - Add useVuLevel hook for efficient single-mixer subscription - Use useSyncExternalStore shim for React 16 compatibility - Memoize getSnapshot callback to avoid unnecessary resubscription Co-Authored-By: Claude Opus 4.5 --- jam-ui/src/hooks/useVuStore.js | 60 ++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 jam-ui/src/hooks/useVuStore.js diff --git a/jam-ui/src/hooks/useVuStore.js b/jam-ui/src/hooks/useVuStore.js new file mode 100644 index 000000000..2f8423bbb --- /dev/null +++ b/jam-ui/src/hooks/useVuStore.js @@ -0,0 +1,60 @@ +/** + * React hooks for subscribing to the external VU store + * + * Uses useSyncExternalStore shim for React 16 compatibility. + * Provides efficient subscription patterns for VU meter components. + */ + +import { useSyncExternalStore } from 'use-sync-external-store/shim'; +import { useCallback } from 'react'; +import { vuStore } from '../stores/vuStore'; + +/** + * Subscribe to all VU levels + * + * WARNING: Use sparingly - causes re-render on ANY VU change. + * Prefer useVuLevel for single mixer subscription. + * + * @returns {Object} - Map of mixerId to { level, clipping, timestamp } + * + * @example + * function AllVuMeters() { + * const allLevels = useAllVuLevels(); + * return Object.keys(allLevels).map(mixerId => ( + * + * )); + * } + */ +export function useAllVuLevels() { + return useSyncExternalStore( + vuStore.subscribe, + vuStore.getSnapshot + ); +} + +/** + * Subscribe to single mixer's VU level + * + * More efficient than useAllVuLevels - only re-renders when this specific + * mixer's level changes. + * + * @param {string} mixerId - Qualified mixer ID (e.g., "M123" or "P456") + * @returns {Object|null} - { level, clipping, timestamp } or null if mixer not found + * + * @example + * function VuMeter({ mixerId }) { + * const vuData = useVuLevel(mixerId); + * if (!vuData) return null; + * return
Level: {vuData.level}
; + * } + */ +export function useVuLevel(mixerId) { + // Create stable getSnapshot callback + // CRITICAL: Must be memoized with useCallback to avoid unnecessary resubscription + const getSnapshot = useCallback(() => vuStore.getLevelSnapshot(mixerId), [mixerId]); + + return useSyncExternalStore( + vuStore.subscribe, + getSnapshot + ); +}