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

7.9 KiB

phase plan type wave depends_on files_modified autonomous must_haves
32-state-update-optimization 02 execute 1
jam-ui/src/hooks/useMixerHelper.js
true
truths artifacts key_links
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
path provides contains
jam-ui/src/hooks/useMixerHelper.js Content comparison before dispatch mixerArraysEqual
from to via pattern
jam-ui/src/hooks/useMixerHelper.js setMetronomeTrackMixers conditional dispatch if.*!mixerArraysEqual.*dispatch
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

<execution_context> @/Users/nuwan/.claude/get-shit-done/workflows/execute-plan.md @/Users/nuwan/.claude/get-shit-done/templates/summary.md </execution_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

Task 1: Add mixer array comparison helper jam-ui/src/hooks/useMixerHelper.js 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.
/**
 * 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 Helper function exists: grep -A 10 "mixerArraysEqual" jam-ui/src/hooks/useMixerHelper.js | head -15 mixerArraysEqual helper function added to useMixerHelper.js
Task 2: Add selectors for current category state jam-ui/src/hooks/useMixerHelper.js 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):
import {
  // ... existing imports ...
  selectMetronomeTrackMixers,
  selectBackingTrackMixers,
  selectJamTrackMixers,
  selectRecordingTrackMixers,
  selectAdhocTrackMixers,
} from '../store/features/mixersSlice';
  1. Inside the useMixerHelper function, add useRef to track previous values. Place this near other refs/selectors:
// 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. 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 Category selectors imported and prevCategoriesRef added

Task 3: Add conditional dispatch to categorization useEffect jam-ui/src/hooks/useMixerHelper.js Modify the categorization useEffect (around lines 236-297) to only dispatch when content changes.

Find the current dispatch block:

// Dispatch to Redux
dispatch(setMetronomeTrackMixers(metronomeTrackMixers));
dispatch(setBackingTrackMixers(backingTrackMixers));
dispatch(setJamTrackMixers(jamTrackMixers));
dispatch(setRecordingTrackMixers(recordingTrackMixers));
dispatch(setAdhocTrackMixers(adhocTrackMixers));

Replace with conditional dispatches:

// 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. 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 Mixer categorization only dispatches when content changes, tracked via prevCategoriesRef

1. Helper function added: ```bash grep -c "mixerArraysEqual" jam-ui/src/hooks/useMixerHelper.js ``` Should return at least 6 (1 definition + 5 usages)
  1. Conditional dispatch pattern:

    grep "if (!mixerArraysEqual" jam-ui/src/hooks/useMixerHelper.js | wc -l
    

    Should return 5

  2. ESLint clean:

    cd jam-ui && npx eslint src/hooks/useMixerHelper.js --max-warnings=0
    

<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>
After completion, create `.planning/phases/32-state-update-optimization/32-02-SUMMARY.md`