jam-cloud/.planning/phases/32-state-update-optimization/32-02-PLAN.md

245 lines
7.9 KiB
Markdown

---
phase: 32-state-update-optimization
plan: 02
type: execute
wave: 1
depends_on: []
files_modified:
- jam-ui/src/hooks/useMixerHelper.js
autonomous: true
must_haves:
truths:
- "Mixer categorization doesn't dispatch when content unchanged"
- "Redux state only updates when mixer arrays meaningfully change"
- "Unchanged mixer list doesn't trigger downstream re-renders"
artifacts:
- path: "jam-ui/src/hooks/useMixerHelper.js"
provides: "Content comparison before dispatch"
contains: "mixerArraysEqual"
key_links:
- from: "jam-ui/src/hooks/useMixerHelper.js"
to: "setMetronomeTrackMixers"
via: "conditional dispatch"
pattern: "if.*!mixerArraysEqual.*dispatch"
---
<objective>
Prevent redundant Redux dispatches in mixer categorization
Purpose: Stop dispatching mixer category arrays when their content hasn't changed (STATE-03). Currently, every mixer state change triggers 5 category dispatches even if the categories haven't changed, causing unnecessary re-renders.
Output: useMixerHelper with content comparison before each category dispatch
</objective>
<execution_context>
@/Users/nuwan/.claude/get-shit-done/workflows/execute-plan.md
@/Users/nuwan/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/32-state-update-optimization/32-RESEARCH.md
# Source file to modify
@jam-ui/src/hooks/useMixerHelper.js
@jam-ui/src/store/features/mixersSlice.js
</context>
<tasks>
<task type="auto">
<name>Task 1: Add mixer array comparison helper</name>
<files>jam-ui/src/hooks/useMixerHelper.js</files>
<action>
Add a helper function to compare mixer arrays by their IDs. Place this near the top of the file, after imports but before the hook function.
```javascript
/**
* Compare two mixer arrays by their IDs to determine if content changed.
* Used to prevent unnecessary Redux dispatches when categorizing mixers.
*
* @param {Array} prev - Previous mixer array
* @param {Array} next - New mixer array
* @returns {boolean} True if arrays have same content (by ID)
*/
const mixerArraysEqual = (prev, next) => {
// Same reference = definitely equal
if (prev === next) return true;
// Different lengths = definitely not equal
if (!prev || !next || prev.length !== next.length) return false;
// Compare by mixer pair IDs (master.id + personal.id creates unique pair key)
const getIds = (arr) => arr
.map(pair => `${pair.master?.id || ''}-${pair.personal?.id || ''}`)
.sort()
.join(',');
return getIds(prev) === getIds(next);
};
```
Why ID-based comparison:
- Mixer objects have stable `id` fields
- Deep comparison with JSON.stringify is slow and fragile
- We only care if the set of mixers changed, not individual properties
- Master/personal pair creates unique identifier
</action>
<verify>
Helper function exists:
`grep -A 10 "mixerArraysEqual" jam-ui/src/hooks/useMixerHelper.js | head -15`
</verify>
<done>
mixerArraysEqual helper function added to useMixerHelper.js
</done>
</task>
<task type="auto">
<name>Task 2: Add selectors for current category state</name>
<files>jam-ui/src/hooks/useMixerHelper.js</files>
<action>
Import the category selectors and add refs to track previous values. Find the existing selector imports and add the category selectors.
1. Update imports from mixersSlice (around line 10-30):
```javascript
import {
// ... existing imports ...
selectMetronomeTrackMixers,
selectBackingTrackMixers,
selectJamTrackMixers,
selectRecordingTrackMixers,
selectAdhocTrackMixers,
} from '../store/features/mixersSlice';
```
2. Inside the useMixerHelper function, add useRef to track previous values. Place this near other refs/selectors:
```javascript
// Track previous category values for comparison
const prevCategoriesRef = useRef({
metronome: [],
backing: [],
jam: [],
recording: [],
adhoc: []
});
```
Note: We use a ref instead of selectors to avoid circular updates. The ref stores the last dispatched values so we can compare before dispatching again.
</action>
<verify>
Category selectors imported:
`grep "selectMetronomeTrackMixers\|selectBackingTrackMixers" jam-ui/src/hooks/useMixerHelper.js | head -5`
prevCategoriesRef defined:
`grep "prevCategoriesRef" jam-ui/src/hooks/useMixerHelper.js | head -3`
</verify>
<done>
Category selectors imported and prevCategoriesRef added
</done>
</task>
<task type="auto">
<name>Task 3: Add conditional dispatch to categorization useEffect</name>
<files>jam-ui/src/hooks/useMixerHelper.js</files>
<action>
Modify the categorization useEffect (around lines 236-297) to only dispatch when content changes.
Find the current dispatch block:
```javascript
// Dispatch to Redux
dispatch(setMetronomeTrackMixers(metronomeTrackMixers));
dispatch(setBackingTrackMixers(backingTrackMixers));
dispatch(setJamTrackMixers(jamTrackMixers));
dispatch(setRecordingTrackMixers(recordingTrackMixers));
dispatch(setAdhocTrackMixers(adhocTrackMixers));
```
Replace with conditional dispatches:
```javascript
// Dispatch to Redux ONLY if content changed
// This prevents unnecessary re-renders when mixer objects change but categories stay same
if (!mixerArraysEqual(prevCategoriesRef.current.metronome, metronomeTrackMixers)) {
console.log('[useMixerHelper] Metronome mixers changed, dispatching');
dispatch(setMetronomeTrackMixers(metronomeTrackMixers));
prevCategoriesRef.current.metronome = metronomeTrackMixers;
}
if (!mixerArraysEqual(prevCategoriesRef.current.backing, backingTrackMixers)) {
console.log('[useMixerHelper] Backing track mixers changed, dispatching');
dispatch(setBackingTrackMixers(backingTrackMixers));
prevCategoriesRef.current.backing = backingTrackMixers;
}
if (!mixerArraysEqual(prevCategoriesRef.current.jam, jamTrackMixers)) {
console.log('[useMixerHelper] Jam track mixers changed, dispatching');
dispatch(setJamTrackMixers(jamTrackMixers));
prevCategoriesRef.current.jam = jamTrackMixers;
}
if (!mixerArraysEqual(prevCategoriesRef.current.recording, recordingTrackMixers)) {
console.log('[useMixerHelper] Recording mixers changed, dispatching');
dispatch(setRecordingTrackMixers(recordingTrackMixers));
prevCategoriesRef.current.recording = recordingTrackMixers;
}
if (!mixerArraysEqual(prevCategoriesRef.current.adhoc, adhocTrackMixers)) {
console.log('[useMixerHelper] Adhoc mixers changed, dispatching');
dispatch(setAdhocTrackMixers(adhocTrackMixers));
prevCategoriesRef.current.adhoc = adhocTrackMixers;
}
```
Note: Console logs are useful for debugging but can be commented out in production. They help verify that dispatches are being skipped when expected.
</action>
<verify>
Conditional dispatch pattern present:
`grep -A 3 "mixerArraysEqual.*metronome" jam-ui/src/hooks/useMixerHelper.js | head -5`
All 5 categories have conditional checks:
`grep -c "mixerArraysEqual.*prevCategoriesRef" jam-ui/src/hooks/useMixerHelper.js` should be 5
ESLint passes:
`cd jam-ui && npx eslint src/hooks/useMixerHelper.js --max-warnings=0`
</verify>
<done>
Mixer categorization only dispatches when content changes, tracked via prevCategoriesRef
</done>
</task>
</tasks>
<verification>
1. Helper function added:
```bash
grep -c "mixerArraysEqual" jam-ui/src/hooks/useMixerHelper.js
```
Should return at least 6 (1 definition + 5 usages)
2. Conditional dispatch pattern:
```bash
grep "if (!mixerArraysEqual" jam-ui/src/hooks/useMixerHelper.js | wc -l
```
Should return 5
3. ESLint clean:
```bash
cd jam-ui && npx eslint src/hooks/useMixerHelper.js --max-warnings=0
```
</verification>
<success_criteria>
- [ ] mixerArraysEqual helper function created
- [ ] prevCategoriesRef tracks previously dispatched values
- [ ] All 5 category dispatches use conditional check
- [ ] prevCategoriesRef updated only when dispatch occurs
- [ ] ESLint passes with no warnings
</success_criteria>
<output>
After completion, create `.planning/phases/32-state-update-optimization/32-02-SUMMARY.md`
</output>