diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 6ee2fc416..1a4fe2fcf 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -77,10 +77,10 @@ Plans: 2. Derived data uses createSelector for memoization 3. Object selectors use shallowEqual comparison 4. Single mixer field change triggers 1-3 selector runs (not 18+) -**Plans**: TBD +**Plans**: 1 plan Plans: -- [ ] 31-01: TBD +- [ ] 31-01-PLAN.md — Create composed selectors with createSelector and refactor useMixerHelper to use shallowEqual ### Phase 32: State Update Optimization **Goal**: Redundant and cascading state updates eliminated @@ -106,7 +106,7 @@ Plans: | 28. VU Meter Optimization | v1.7 | 2/2 | ✓ Complete | 2026-03-05 | | 29. Context Optimization | v1.7 | 1/1 | ✓ Complete | 2026-03-05 | | 30. Component Memoization | v1.7 | 1/1 | ✓ Complete | 2026-03-05 | -| 31. Selector Optimization | v1.7 | 0/TBD | Not started | - | +| 31. Selector Optimization | v1.7 | 0/1 | Planned | - | | 32. State Update Optimization | v1.7 | 0/TBD | Not started | - | --- diff --git a/.planning/phases/31-selector-optimization/31-01-PLAN.md b/.planning/phases/31-selector-optimization/31-01-PLAN.md new file mode 100644 index 000000000..c891d3330 --- /dev/null +++ b/.planning/phases/31-selector-optimization/31-01-PLAN.md @@ -0,0 +1,323 @@ +--- +phase: 31-selector-optimization +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - jam-ui/src/store/features/mixersSlice.js + - jam-ui/src/hooks/useMixerHelper.js +autonomous: true + +must_haves: + truths: + - "Single mixer field change triggers 1-3 selector runs instead of 18+" + - "useMixerHelper uses 3-5 composed selectors with shallowEqual" + - "Derived data is memoized via createSelector" + artifacts: + - path: "jam-ui/src/store/features/mixersSlice.js" + provides: "Composed memoized selectors for mixer state" + exports: ["selectCoreMixerState", "selectTrackMixerState", "selectMixerLookupTables"] + contains: "createSelector" + - path: "jam-ui/src/hooks/useMixerHelper.js" + provides: "Optimized hook using composed selectors" + contains: "shallowEqual" + key_links: + - from: "jam-ui/src/hooks/useMixerHelper.js" + to: "jam-ui/src/store/features/mixersSlice.js" + via: "import composed selectors" + pattern: "import.*selectCoreMixerState.*from.*mixersSlice" + - from: "jam-ui/src/hooks/useMixerHelper.js" + to: "react-redux" + via: "shallowEqual comparison" + pattern: "useSelector\\(.*shallowEqual\\)" +--- + + +Consolidate 18+ individual Redux selectors in useMixerHelper into 3-5 composed memoized selectors using createSelector from Redux Toolkit. + +Purpose: Reduce selector overhead from 18+ independent subscriptions and equality checks per Redux action to 3-5 memoized selectors that only recompute when their specific inputs change. This eliminates unnecessary re-renders when unrelated mixer fields change. + +Output: Composed selectors in mixersSlice.js and refactored useMixerHelper.js using shallowEqual comparison for object returns. + + + +@/Users/nuwan/.claude/get-shit-done/workflows/execute-plan.md +@/Users/nuwan/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/phases/31-selector-optimization/31-RESEARCH.md + +# Key source files +@jam-ui/src/store/features/mixersSlice.js +@jam-ui/src/hooks/useMixerHelper.js + + + + + + Task 1: Create composed selectors in mixersSlice.js + jam-ui/src/store/features/mixersSlice.js + +Add createSelector from Redux Toolkit and create three composed memoized selectors that group related mixer state: + +1. Import createSelector at top: +```javascript +import { createSlice, createSelector } from '@reduxjs/toolkit'; +``` + +2. Add composed selectors after the existing simple selectors (near end of file, before the parameterized selectors): + +```javascript +// Composed memoized selectors for useMixerHelper optimization +// These group related data that's typically used together + +export const selectCoreMixerState = createSelector( + [selectChatMixer, selectBroadcastMixer, selectRecordingMixer], + (chatMixer, broadcastMixer, recordingMixer) => ({ + chatMixer, + broadcastMixer, + recordingMixer + }) +); + +export const selectTrackMixerState = createSelector( + [ + selectRecordingTrackMixers, + selectBackingTrackMixers, + selectJamTrackMixers, + selectMetronomeTrackMixers, + selectAdhocTrackMixers + ], + (recordingTrackMixers, backingTrackMixers, jamTrackMixers, metronomeTrackMixers, adhocTrackMixers) => ({ + recordingTrackMixers, + backingTrackMixers, + jamTrackMixers, + metronomeTrackMixers, + adhocTrackMixers + }) +); + +export const selectMixerLookupTables = createSelector( + [selectAllMixers, selectMixersByResourceId, selectMixersByTrackId], + (allMixers, mixersByResourceId, mixersByTrackId) => ({ + allMixers, + mixersByResourceId, + mixersByTrackId + }) +); + +export const selectMasterPersonalMixers = createSelector( + [selectMasterMixers, selectPersonalMixers], + (masterMixers, personalMixers) => ({ + masterMixers, + personalMixers + }) +); + +export const selectMixerMetadata = createSelector( + [selectMetronome, selectMetronomeSettings, selectMediaSummary, selectNoAudioUsers, selectClientsWithAudioOverride, selectMixersReady], + (metronome, metronomeSettings, mediaSummary, noAudioUsers, clientsWithAudioOverride, isReady) => ({ + metronome, + metronomeSettings, + mediaSummary, + noAudioUsers, + clientsWithAudioOverride, + isReady + }) +); + +export const selectSimulatedCategoryMixers = createSelector( + [selectSimulatedMusicCategoryMixers, selectSimulatedChatCategoryMixers], + (simulatedMusicCategoryMixers, simulatedChatCategoryMixers) => ({ + simulatedMusicCategoryMixers, + simulatedChatCategoryMixers + }) +); +``` + +These selectors use createSelector which: +- Memoizes the result based on input selectors +- Only recomputes when any input selector returns a different reference +- Returns the same object reference when inputs haven't changed + + +Run syntax check: +```bash +cd jam-ui && node -c src/store/features/mixersSlice.js +``` + +Verify createSelector import and exports: +```bash +grep -E "import.*createSelector.*@reduxjs/toolkit" jam-ui/src/store/features/mixersSlice.js +grep -E "export const select(CoreMixerState|TrackMixerState|MixerLookupTables)" jam-ui/src/store/features/mixersSlice.js +``` + + +mixersSlice.js exports 6 composed selectors using createSelector: +- selectCoreMixerState (chatMixer, broadcastMixer, recordingMixer) +- selectTrackMixerState (5 track mixer arrays) +- selectMixerLookupTables (allMixers, mixersByResourceId, mixersByTrackId) +- selectMasterPersonalMixers (masterMixers, personalMixers) +- selectMixerMetadata (metronome, settings, mediaSummary, noAudioUsers, etc.) +- selectSimulatedCategoryMixers (simulatedMusic, simulatedChat) + + + + + Task 2: Refactor useMixerHelper to use composed selectors with shallowEqual + jam-ui/src/hooks/useMixerHelper.js + +Replace 18+ individual useSelector calls with 6 composed selector calls using shallowEqual: + +1. Update imports at top of file: + +Replace the long list of individual selector imports with: +```javascript +import { useSelector, useDispatch, shallowEqual } from 'react-redux'; +import { selectActiveSession, selectInSession } from '../store/features/activeSessionSlice'; +import { + // Composed selectors (use these instead of individual ones) + selectCoreMixerState, + selectTrackMixerState, + selectMixerLookupTables, + selectMasterPersonalMixers, + selectMixerMetadata, + selectSimulatedCategoryMixers, + // Actions (still needed) + setMasterMixers, + setPersonalMixers, + organizeMixers, + updateMixer, + setChatMixer, + setBackingTracks as setBackingTracksAction, + setJamTracks as setJamTracksAction, + setRecordedTracks as setRecordedTracksAction, + setMetronome as setMetronomeAction, + setMediaSummary, + setSimulatedMusicCategoryMixers as setSimulatedMusicAction, + setSimulatedChatCategoryMixers as setSimulatedChatAction, + setRecordingTrackMixers, + setBackingTrackMixers, + setJamTrackMixers, + setMetronomeTrackMixers, + setAdhocTrackMixers +} from '../store/features/mixersSlice'; +``` + +2. Replace the 18+ individual useSelector calls (lines 72-92) with composed selector calls: + +OLD (remove this block): +```javascript +const chatMixer = useSelector(selectChatMixer); +const broadcastMixer = useSelector(selectBroadcastMixer); +const recordingMixer = useSelector(selectRecordingMixer); +// ... 15+ more individual selectors +``` + +NEW (replace with): +```javascript +// Composed selectors with shallowEqual - reduces 18+ subscriptions to 6 +const coreMixers = useSelector(selectCoreMixerState, shallowEqual); +const trackMixers = useSelector(selectTrackMixerState, shallowEqual); +const lookupTables = useSelector(selectMixerLookupTables, shallowEqual); +const masterPersonal = useSelector(selectMasterPersonalMixers, shallowEqual); +const metadata = useSelector(selectMixerMetadata, shallowEqual); +const simulatedMixers = useSelector(selectSimulatedCategoryMixers, shallowEqual); + +// Destructure for backward compatibility with rest of hook +const { chatMixer, broadcastMixer, recordingMixer } = coreMixers; +const { recordingTrackMixers, backingTrackMixers, jamTrackMixers, + metronomeTrackMixers, adhocTrackMixers } = trackMixers; +const { allMixers, mixersByResourceId, mixersByTrackId } = lookupTables; +const { masterMixers, personalMixers } = masterPersonal; +const { metronome, metronomeSettings, mediaSummary, noAudioUsers, + clientsWithAudioOverride, isReady: isReadyRedux } = metadata; +const { simulatedMusicCategoryMixers, simulatedChatCategoryMixers } = simulatedMixers; +``` + +This preserves all existing variable names used throughout the hook while reducing selector subscriptions from 18+ to 6. + +IMPORTANT: Keep all other code in useMixerHelper.js unchanged - only modify imports and the selector calls section. + + +Run syntax check: +```bash +cd jam-ui && node -c src/hooks/useMixerHelper.js +``` + +Verify shallowEqual usage: +```bash +grep -c "shallowEqual" jam-ui/src/hooks/useMixerHelper.js +# Should return 6 or more (imports + 6 useSelector calls) +``` + +Verify composed selector imports: +```bash +grep "selectCoreMixerState" jam-ui/src/hooks/useMixerHelper.js +grep "selectTrackMixerState" jam-ui/src/hooks/useMixerHelper.js +grep "selectMixerLookupTables" jam-ui/src/hooks/useMixerHelper.js +``` + +Count useSelector calls (should be significantly reduced): +```bash +grep -c "useSelector" jam-ui/src/hooks/useMixerHelper.js +# Should be around 9-12 (6 composed + a few remaining individual like selectMixMode) +``` + + +useMixerHelper.js uses 6 composed selectors with shallowEqual instead of 18+ individual selectors: +- coreMixers via selectCoreMixerState +- trackMixers via selectTrackMixerState +- lookupTables via selectMixerLookupTables +- masterPersonal via selectMasterPersonalMixers +- metadata via selectMixerMetadata +- simulatedMixers via selectSimulatedCategoryMixers + +All existing variable names preserved via destructuring for backward compatibility. + + + + + + +After both tasks: + +1. **Syntax validation:** +```bash +cd jam-ui && node -c src/store/features/mixersSlice.js && node -c src/hooks/useMixerHelper.js +``` + +2. **Build check:** +```bash +cd jam-ui && npm run build 2>&1 | head -50 +``` + +3. **Selector count verification:** +- mixersSlice.js should export 6 new composed selectors +- useMixerHelper.js should have 6 useSelector calls with shallowEqual +- Total useSelector calls in useMixerHelper reduced from 18+ to ~10-12 + +4. **Pattern verification:** +```bash +grep -E "createSelector|shallowEqual" jam-ui/src/store/features/mixersSlice.js jam-ui/src/hooks/useMixerHelper.js +``` + + + +- [ ] mixersSlice.js imports createSelector from @reduxjs/toolkit +- [ ] mixersSlice.js exports 6 composed selectors (selectCoreMixerState, selectTrackMixerState, selectMixerLookupTables, selectMasterPersonalMixers, selectMixerMetadata, selectSimulatedCategoryMixers) +- [ ] useMixerHelper.js imports shallowEqual from react-redux +- [ ] useMixerHelper.js uses 6 composed selectors with shallowEqual comparison +- [ ] All existing variable names preserved via destructuring +- [ ] Syntax validation passes for both files +- [ ] Build completes without errors + + + +After completion, create `.planning/phases/31-selector-optimization/31-01-SUMMARY.md` +