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
+
+
+