docs(32): create phase plan
Phase 32: State Update Optimization - 4 plan(s) in 2 wave(s) - 3 parallel in wave 1, 1 sequential in wave 2 - Ready for execution Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
ae583e7bc7
commit
173cf5e94d
|
|
@ -92,10 +92,13 @@ Plans:
|
|||
3. Mixer categorization doesn't dispatch when content unchanged
|
||||
4. Loading states moved to components that use them
|
||||
5. JKSessionScreen re-render count reduced by 50%+
|
||||
**Plans**: TBD
|
||||
**Plans**: 4 plans
|
||||
|
||||
Plans:
|
||||
- [ ] 32-01: TBD
|
||||
- [ ] 32-01-PLAN.md — Consolidate track sync and fix useSessionModel debounce recreation
|
||||
- [ ] 32-02-PLAN.md — Add content comparison before mixer category dispatches
|
||||
- [ ] 32-03-PLAN.md — Create JKResyncButton and JKVideoButton with colocated loading state
|
||||
- [ ] 32-04-PLAN.md — Audit remaining state and verify re-render reduction
|
||||
|
||||
## Progress
|
||||
|
||||
|
|
@ -107,7 +110,7 @@ Plans:
|
|||
| 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 | 1/1 | ✓ Complete | 2026-03-05 |
|
||||
| 32. State Update Optimization | v1.7 | 0/TBD | Not started | - |
|
||||
| 32. State Update Optimization | v1.7 | 0/4 | Planned | - |
|
||||
|
||||
---
|
||||
*v1.7 roadmap created 2026-03-03*
|
||||
|
|
|
|||
|
|
@ -0,0 +1,294 @@
|
|||
---
|
||||
phase: 32-state-update-optimization
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- jam-ui/src/components/client/JKSessionScreen.js
|
||||
- jam-ui/src/hooks/useSessionModel.js
|
||||
- jam-ui/src/hooks/useDebounceCallback.js
|
||||
autonomous: true
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "Track sync fires once per session join (not 3 times)"
|
||||
- "Debounce timer in trackChanges is stable across state changes"
|
||||
- "Debounced callbacks read fresh state values"
|
||||
artifacts:
|
||||
- path: "jam-ui/src/hooks/useDebounceCallback.js"
|
||||
provides: "Reusable ref-based debounce hook"
|
||||
exports: ["useDebounceCallback"]
|
||||
- path: "jam-ui/src/components/client/JKSessionScreen.js"
|
||||
provides: "Single debounced track sync"
|
||||
contains: "syncTracksDebounced"
|
||||
- path: "jam-ui/src/hooks/useSessionModel.js"
|
||||
provides: "Stable trackChanges debounce"
|
||||
contains: "useDebounceCallback"
|
||||
key_links:
|
||||
- from: "jam-ui/src/components/client/JKSessionScreen.js"
|
||||
to: "syncTracksToServer"
|
||||
via: "single debounced call"
|
||||
pattern: "useMemo.*debounce.*syncTracksToServer"
|
||||
- from: "jam-ui/src/hooks/useSessionModel.js"
|
||||
to: "useDebounceCallback"
|
||||
via: "import and usage"
|
||||
pattern: "useDebounceCallback.*trackChanges"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Fix redundant track sync calls and debounce instance recreation
|
||||
|
||||
Purpose: Eliminate triple API calls on session join (STATE-01) and fix debounce timer reset on state changes (STATE-02). These are the two remaining redundant operation patterns identified in the investigation phase.
|
||||
|
||||
Output: Single debounced track sync call, stable useDebounceCallback hook, refactored trackChanges handler
|
||||
</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 files to modify
|
||||
@jam-ui/src/components/client/JKSessionScreen.js
|
||||
@jam-ui/src/hooks/useSessionModel.js
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Create useDebounceCallback hook</name>
|
||||
<files>jam-ui/src/hooks/useDebounceCallback.js</files>
|
||||
<action>
|
||||
Create a new reusable hook that solves the stale closure problem with debounced callbacks:
|
||||
|
||||
```javascript
|
||||
import { useRef, useEffect, useMemo } from 'react';
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
/**
|
||||
* Hook that creates a stable debounced callback with fresh closure access.
|
||||
* Solves the problem of debounce recreation when dependencies change.
|
||||
*
|
||||
* Pattern: useRef stores latest callback, useMemo creates debounce once.
|
||||
* Source: https://www.developerway.com/posts/debouncing-in-react
|
||||
*
|
||||
* @param {Function} callback - The callback to debounce
|
||||
* @param {number} delay - Debounce delay in ms (default: 500)
|
||||
* @returns {Function} Debounced function that always uses latest callback
|
||||
*/
|
||||
export const useDebounceCallback = (callback, delay = 500) => {
|
||||
const callbackRef = useRef(callback);
|
||||
|
||||
// Always keep ref current with latest callback
|
||||
useEffect(() => {
|
||||
callbackRef.current = callback;
|
||||
}, [callback]);
|
||||
|
||||
// Create debounced function once - never recreated
|
||||
const debouncedFn = useMemo(() =>
|
||||
debounce((...args) => {
|
||||
callbackRef.current?.(...args);
|
||||
}, delay),
|
||||
[delay] // Only recreate if delay changes
|
||||
);
|
||||
|
||||
// Cleanup on unmount
|
||||
useEffect(() => {
|
||||
return () => debouncedFn.cancel();
|
||||
}, [debouncedFn]);
|
||||
|
||||
return debouncedFn;
|
||||
};
|
||||
|
||||
export default useDebounceCallback;
|
||||
```
|
||||
|
||||
Key design decisions:
|
||||
- Uses useRef to store callback, keeping it always current
|
||||
- Uses useMemo with [delay] deps to create debounce function once
|
||||
- Handles cleanup on unmount via useEffect return
|
||||
- Exports both named and default for flexibility
|
||||
</action>
|
||||
<verify>
|
||||
File exists at jam-ui/src/hooks/useDebounceCallback.js
|
||||
Exports useDebounceCallback function
|
||||
No ESLint errors: `cd jam-ui && npx eslint src/hooks/useDebounceCallback.js`
|
||||
</verify>
|
||||
<done>
|
||||
useDebounceCallback hook created with ref-based pattern, ready for import in useSessionModel
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Consolidate track sync to single debounced call</name>
|
||||
<files>jam-ui/src/components/client/JKSessionScreen.js</files>
|
||||
<action>
|
||||
Replace the triple setTimeout pattern (lines ~462-485) with a single debounced call.
|
||||
|
||||
1. Add import for useMemo at top (if not already present)
|
||||
|
||||
2. Find the current track sync useEffect (around lines 453-485):
|
||||
```javascript
|
||||
// Track sync: Sync tracks to server when session joined (3-call pattern matching legacy)
|
||||
useEffect(() => {
|
||||
if (!hasJoined || !sessionId || !server?.clientId || !mixersReady) {
|
||||
return;
|
||||
}
|
||||
const timer1 = setTimeout(() => { ... }, 1000);
|
||||
const timer2 = setTimeout(() => { ... }, 1400);
|
||||
const timer3 = setTimeout(() => { ... }, 6000);
|
||||
return () => { clearTimeout(timer1); ... };
|
||||
}, [hasJoined, sessionId, mixersReady, dispatch])
|
||||
```
|
||||
|
||||
3. Replace with single debounced call pattern:
|
||||
```javascript
|
||||
// Create stable debounced sync function
|
||||
const syncTracksDebounced = useMemo(() =>
|
||||
debounce((sid, cid, d) => {
|
||||
d(syncTracksToServer(sid, cid));
|
||||
}, 1500), // 1.5s delay - adequate for mixer initialization
|
||||
[]
|
||||
);
|
||||
|
||||
// Track sync: Single debounced call when session joined
|
||||
// Replaces legacy 3-timer pattern (1s, 1.4s, 6s) with single 1.5s debounced call
|
||||
useEffect(() => {
|
||||
if (!hasJoined || !sessionId || !server?.clientId || !mixersReady) {
|
||||
return;
|
||||
}
|
||||
|
||||
// console.log('[Track Sync] Mixers ready, scheduling single debounced sync');
|
||||
syncTracksDebounced(sessionId, server.clientId, dispatch);
|
||||
|
||||
return () => syncTracksDebounced.cancel();
|
||||
// Note: server intentionally NOT in deps to avoid re-running on server reference changes
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [hasJoined, sessionId, mixersReady, dispatch, syncTracksDebounced]);
|
||||
```
|
||||
|
||||
4. Add lodash debounce import if not present:
|
||||
```javascript
|
||||
import { debounce } from 'lodash';
|
||||
```
|
||||
|
||||
Why 1.5s delay: Legacy used 1s, 1.4s, 6s. The first two calls at 1s and 1.4s were redundant. A single 1.5s debounced call covers the initial mixer setup window while preventing rapid repeated calls. The 6s call was likely for slow networks - if issues arise, this delay can be increased.
|
||||
</action>
|
||||
<verify>
|
||||
Check the track sync useEffect has single debounce call:
|
||||
`grep -A 20 "Track sync:" jam-ui/src/components/client/JKSessionScreen.js`
|
||||
|
||||
Verify no setTimeout pattern remains in that section:
|
||||
`grep -A 20 "Track sync:" jam-ui/src/components/client/JKSessionScreen.js | grep -c setTimeout` should be 0
|
||||
|
||||
ESLint passes: `cd jam-ui && npx eslint src/components/client/JKSessionScreen.js --max-warnings=0`
|
||||
</verify>
|
||||
<done>
|
||||
Track sync useEffect uses single debounced call instead of 3 setTimeout calls
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Fix trackChanges debounce in useSessionModel</name>
|
||||
<files>jam-ui/src/hooks/useSessionModel.js</files>
|
||||
<action>
|
||||
Replace the useCallback-wrapped debounce with useDebounceCallback hook.
|
||||
|
||||
1. Add import at top:
|
||||
```javascript
|
||||
import { useDebounceCallback } from './useDebounceCallback';
|
||||
```
|
||||
|
||||
2. Find current trackChanges (around lines 604-614):
|
||||
```javascript
|
||||
// Track changes handler - debounced to prevent excessive session refreshes
|
||||
const trackChanges = useCallback(debounce((header, payload) => {
|
||||
if (currentTrackChanges < payload.track_changes_counter) {
|
||||
logger.debug("track_changes_counter = stale. refreshing...");
|
||||
refreshCurrentSession();
|
||||
} else {
|
||||
if (header.type !== 'HEARTBEAT_ACK') {
|
||||
logger.info("track_changes_counter = fresh. skipping refresh...", header, payload);
|
||||
}
|
||||
}
|
||||
}, 500), [currentTrackChanges, refreshCurrentSession]);
|
||||
```
|
||||
|
||||
3. Replace with useDebounceCallback pattern:
|
||||
```javascript
|
||||
// Track changes handler - debounced to prevent excessive session refreshes
|
||||
// Uses useDebounceCallback for stable timer (doesn't reset when deps change)
|
||||
const trackChanges = useDebounceCallback((header, payload) => {
|
||||
if (currentTrackChanges < payload.track_changes_counter) {
|
||||
logger.debug("track_changes_counter = stale. refreshing...");
|
||||
refreshCurrentSession();
|
||||
} else {
|
||||
if (header.type !== 'HEARTBEAT_ACK') {
|
||||
logger.info("track_changes_counter = fresh. skipping refresh...", header, payload);
|
||||
}
|
||||
}
|
||||
}, 500);
|
||||
```
|
||||
|
||||
Key changes:
|
||||
- Remove useCallback wrapper (useDebounceCallback handles memoization internally)
|
||||
- Remove debounce import call (it's inside useDebounceCallback)
|
||||
- Remove [currentTrackChanges, refreshCurrentSession] dependency array
|
||||
- The callback now always reads fresh values via ref closure
|
||||
- Timer is stable - won't reset on currentTrackChanges updates
|
||||
|
||||
4. Remove lodash debounce import if no longer used elsewhere in file:
|
||||
Check for other usages first: `grep -n "debounce" useSessionModel.js`
|
||||
If trackChanges was the only use, remove: `import { debounce } from 'lodash';`
|
||||
If other usages exist, keep the import.
|
||||
</action>
|
||||
<verify>
|
||||
trackChanges now uses useDebounceCallback:
|
||||
`grep -B 2 -A 10 "trackChanges = " jam-ui/src/hooks/useSessionModel.js | head -15`
|
||||
|
||||
No useCallback wrapper on trackChanges:
|
||||
`grep "useCallback(debounce" jam-ui/src/hooks/useSessionModel.js | wc -l` should be 0
|
||||
|
||||
ESLint passes: `cd jam-ui && npx eslint src/hooks/useSessionModel.js --max-warnings=0`
|
||||
</verify>
|
||||
<done>
|
||||
trackChanges uses useDebounceCallback - timer stable across state changes, always reads fresh values
|
||||
</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
1. Track sync fires once per session join:
|
||||
- Grep for setTimeout in track sync section should return 0
|
||||
- useMemo + debounce pattern present
|
||||
|
||||
2. Debounce stability verified:
|
||||
- useDebounceCallback hook exists with ref pattern
|
||||
- trackChanges in useSessionModel uses useDebounceCallback
|
||||
- No useCallback(debounce) pattern in useSessionModel
|
||||
|
||||
3. All files pass ESLint:
|
||||
```bash
|
||||
cd jam-ui && npx eslint src/hooks/useDebounceCallback.js src/hooks/useSessionModel.js src/components/client/JKSessionScreen.js --max-warnings=0
|
||||
```
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- [ ] useDebounceCallback.js created with ref-based debounce pattern
|
||||
- [ ] JKSessionScreen track sync uses single debounced call (not 3 setTimeout)
|
||||
- [ ] useSessionModel trackChanges uses useDebounceCallback (no dependency array recreation)
|
||||
- [ ] All modified files pass ESLint
|
||||
- [ ] No regressions - session join still works (manual verification if time permits)
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/32-state-update-optimization/32-01-SUMMARY.md`
|
||||
</output>
|
||||
|
|
@ -0,0 +1,244 @@
|
|||
---
|
||||
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>
|
||||
|
|
@ -0,0 +1,393 @@
|
|||
---
|
||||
phase: 32-state-update-optimization
|
||||
plan: 03
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- jam-ui/src/components/client/JKSessionScreen.js
|
||||
- jam-ui/src/components/client/JKResyncButton.js
|
||||
- jam-ui/src/components/client/JKVideoButton.js
|
||||
autonomous: true
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "resyncLoading state lives in JKResyncButton (not JKSessionScreen)"
|
||||
- "videoLoading state lives in JKVideoButton (not JKSessionScreen)"
|
||||
- "Loading state changes don't re-render JKSessionScreen"
|
||||
artifacts:
|
||||
- path: "jam-ui/src/components/client/JKResyncButton.js"
|
||||
provides: "Self-contained resync button with loading state"
|
||||
exports: ["JKResyncButton"]
|
||||
- path: "jam-ui/src/components/client/JKVideoButton.js"
|
||||
provides: "Self-contained video button with loading state"
|
||||
exports: ["JKVideoButton"]
|
||||
- path: "jam-ui/src/components/client/JKSessionScreen.js"
|
||||
provides: "Uses extracted button components"
|
||||
contains: "JKResyncButton"
|
||||
key_links:
|
||||
- from: "jam-ui/src/components/client/JKSessionScreen.js"
|
||||
to: "JKResyncButton"
|
||||
via: "import and render"
|
||||
pattern: "<JKResyncButton"
|
||||
- from: "jam-ui/src/components/client/JKSessionScreen.js"
|
||||
to: "JKVideoButton"
|
||||
via: "import and render"
|
||||
pattern: "<JKVideoButton"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Colocate loading states to button components
|
||||
|
||||
Purpose: Move resyncLoading (COLOC-01) and videoLoading (COLOC-02) from JKSessionScreen to their respective button components. This follows state colocation principles - state should live in the component that uses it, preventing parent re-renders.
|
||||
|
||||
Output: JKResyncButton and JKVideoButton components with colocated loading state, JKSessionScreen simplified
|
||||
</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 files
|
||||
@jam-ui/src/components/client/JKSessionScreen.js
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Create JKResyncButton component</name>
|
||||
<files>jam-ui/src/components/client/JKResyncButton.js</files>
|
||||
<action>
|
||||
Create a new self-contained button component with colocated loading state.
|
||||
|
||||
```javascript
|
||||
import React, { useState, useCallback, memo } from 'react';
|
||||
import { Button, Spinner } from 'reactstrap';
|
||||
import { toast } from 'react-toastify';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
/**
|
||||
* Self-contained resync button with colocated loading state.
|
||||
* Loading state changes only re-render this component, not the parent.
|
||||
*
|
||||
* State colocation: https://kentcdodds.com/blog/state-colocation-will-make-your-react-app-faster
|
||||
*/
|
||||
const JKResyncButton = memo(({ resyncAudio, className }) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleClick = useCallback(async (e) => {
|
||||
e.preventDefault();
|
||||
if (loading) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
await resyncAudio();
|
||||
// Silent success (matches legacy behavior)
|
||||
} catch (error) {
|
||||
if (error.message === 'timeout') {
|
||||
toast.error('Audio resync timed out. Please try again.');
|
||||
} else {
|
||||
toast.error('Audio resync failed: ' + (error.message || 'Unknown error'));
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [resyncAudio, loading]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
className={className || 'btn-custom-outline'}
|
||||
outline
|
||||
size="md"
|
||||
onClick={handleClick}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<Spinner size="sm" /> Resyncing...
|
||||
</>
|
||||
) : (
|
||||
'Resync'
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
|
||||
JKResyncButton.displayName = 'JKResyncButton';
|
||||
|
||||
JKResyncButton.propTypes = {
|
||||
resyncAudio: PropTypes.func.isRequired,
|
||||
className: PropTypes.string
|
||||
};
|
||||
|
||||
export default JKResyncButton;
|
||||
```
|
||||
|
||||
Key design decisions:
|
||||
- memo() wrapper prevents re-renders from parent prop stability
|
||||
- Loading state is local - changes don't propagate up
|
||||
- Same error handling as original handleResync
|
||||
- displayName for React DevTools debugging
|
||||
- PropTypes for documentation
|
||||
</action>
|
||||
<verify>
|
||||
File exists and exports component:
|
||||
`ls -la jam-ui/src/components/client/JKResyncButton.js`
|
||||
|
||||
Has useState for loading:
|
||||
`grep "useState(false)" jam-ui/src/components/client/JKResyncButton.js`
|
||||
|
||||
Has memo wrapper:
|
||||
`grep "memo(" jam-ui/src/components/client/JKResyncButton.js`
|
||||
</verify>
|
||||
<done>
|
||||
JKResyncButton component created with colocated loading state
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Create JKVideoButton component</name>
|
||||
<files>jam-ui/src/components/client/JKVideoButton.js</files>
|
||||
<action>
|
||||
Create a new self-contained video button component with colocated loading state.
|
||||
|
||||
```javascript
|
||||
import React, { useState, useCallback, memo } from 'react';
|
||||
import { Button, Spinner } from 'reactstrap';
|
||||
import { toast } from 'react-toastify';
|
||||
import PropTypes from 'prop-types';
|
||||
import videoIcon from '../../assets/images/icons8-video-call-50.png';
|
||||
|
||||
/**
|
||||
* Self-contained video button with colocated loading state.
|
||||
* Loading state changes only re-render this component, not the parent.
|
||||
*
|
||||
* State colocation: https://kentcdodds.com/blog/state-colocation-will-make-your-react-app-faster
|
||||
*/
|
||||
const JKVideoButton = memo(({
|
||||
canVideo,
|
||||
getVideoUrl,
|
||||
onUpgradePrompt,
|
||||
className
|
||||
}) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// Open external link in new window/tab
|
||||
const openExternalLink = useCallback((url) => {
|
||||
window.open(url, '_blank', 'noopener,noreferrer');
|
||||
}, []);
|
||||
|
||||
const handleClick = useCallback(async () => {
|
||||
if (!canVideo()) {
|
||||
onUpgradePrompt();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
// Get video conferencing room URL from server
|
||||
const response = await getVideoUrl();
|
||||
const videoUrl = `${response.url}&audiooff=true`;
|
||||
|
||||
// Open video URL in new browser window/tab
|
||||
openExternalLink(videoUrl);
|
||||
|
||||
} catch (error) {
|
||||
toast.error('Failed to start video session');
|
||||
} finally {
|
||||
// Keep loading state for 10 seconds to prevent multiple clicks
|
||||
setTimeout(() => setLoading(false), 10000);
|
||||
}
|
||||
}, [canVideo, getVideoUrl, onUpgradePrompt, openExternalLink]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
className={className || 'btn-custom-outline'}
|
||||
outline
|
||||
size="md"
|
||||
onClick={handleClick}
|
||||
disabled={loading}
|
||||
>
|
||||
<img
|
||||
src={videoIcon}
|
||||
alt="Video"
|
||||
style={{ width: '20px', height: '20px', marginRight: '0.3rem' }}
|
||||
/>
|
||||
{loading && <Spinner size="sm" />}
|
||||
Video
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
|
||||
JKVideoButton.displayName = 'JKVideoButton';
|
||||
|
||||
JKVideoButton.propTypes = {
|
||||
canVideo: PropTypes.func.isRequired,
|
||||
getVideoUrl: PropTypes.func.isRequired,
|
||||
onUpgradePrompt: PropTypes.func.isRequired,
|
||||
className: PropTypes.string
|
||||
};
|
||||
|
||||
export default JKVideoButton;
|
||||
```
|
||||
|
||||
Key design decisions:
|
||||
- 10-second loading timeout preserved from original
|
||||
- canVideo and onUpgradePrompt as props for flexibility
|
||||
- Video icon imported directly (same path as original)
|
||||
- memo() for render optimization
|
||||
</action>
|
||||
<verify>
|
||||
File exists and exports component:
|
||||
`ls -la jam-ui/src/components/client/JKVideoButton.js`
|
||||
|
||||
Has useState for loading:
|
||||
`grep "useState(false)" jam-ui/src/components/client/JKVideoButton.js`
|
||||
|
||||
Has 10-second timeout:
|
||||
`grep "10000" jam-ui/src/components/client/JKVideoButton.js`
|
||||
</verify>
|
||||
<done>
|
||||
JKVideoButton component created with colocated loading state
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Refactor JKSessionScreen to use extracted components</name>
|
||||
<files>jam-ui/src/components/client/JKSessionScreen.js</files>
|
||||
<action>
|
||||
Replace inline button implementations with the new components.
|
||||
|
||||
1. Add imports at top:
|
||||
```javascript
|
||||
import JKResyncButton from './JKResyncButton';
|
||||
import JKVideoButton from './JKVideoButton';
|
||||
```
|
||||
|
||||
2. Remove useState declarations (around lines 202-205):
|
||||
```javascript
|
||||
// DELETE these lines:
|
||||
const [videoLoading, setVideoLoading] = useState(false);
|
||||
const [resyncLoading, setResyncLoading] = useState(false);
|
||||
```
|
||||
|
||||
3. Remove handleResync function (around lines 1056-1075):
|
||||
```javascript
|
||||
// DELETE the entire handleResync function
|
||||
// const handleResync = useCallback(async (e) => { ... });
|
||||
```
|
||||
|
||||
4. Remove handleVideoClick function (around lines 977-1002):
|
||||
```javascript
|
||||
// DELETE the entire handleVideoClick function
|
||||
// const handleVideoClick = async () => { ... };
|
||||
```
|
||||
|
||||
5. Update the button JSX in the toolbar section (around lines 1341-1388).
|
||||
|
||||
Find the Video button:
|
||||
```jsx
|
||||
<Button className='btn-custom-outline' outline size="md" onClick={handleVideoClick} disabled={videoLoading}>
|
||||
<img src={videoIcon} alt="Video" style={{ width: '20px', height: '20px', marginRight: '0.3rem' }} />
|
||||
{videoLoading && (<Spinner size="sm" />)}
|
||||
Video
|
||||
</Button>
|
||||
```
|
||||
|
||||
Replace with:
|
||||
```jsx
|
||||
<JKVideoButton
|
||||
canVideo={canVideo}
|
||||
getVideoUrl={() => getVideoConferencingRoomUrl(currentSession.id)}
|
||||
onUpgradePrompt={showVideoUpgradePrompt}
|
||||
/>
|
||||
```
|
||||
|
||||
Find the Resync button:
|
||||
```jsx
|
||||
<Button className='btn-custom-outline' outline size="md" onClick={handleResync} disabled={resyncLoading}>
|
||||
<img src={resyncIcon} alt="Resync" style={{ width: '20px', height: '20px', marginRight: '0.3rem' }} />
|
||||
{resyncLoading ? <><Spinner size="sm" /> Resyncing...</> : 'Resync'}
|
||||
</Button>
|
||||
```
|
||||
|
||||
Replace with:
|
||||
```jsx
|
||||
<JKResyncButton resyncAudio={resyncAudio} />
|
||||
```
|
||||
|
||||
6. Keep the following functions in JKSessionScreen (they're still needed):
|
||||
- `canVideo` (permission check)
|
||||
- `showVideoUpgradePrompt` (toast display)
|
||||
- `resyncAudio` (from useMediaActions)
|
||||
|
||||
7. Remove videoIcon import if no longer used elsewhere:
|
||||
```javascript
|
||||
// Check if videoIcon is used elsewhere, if not remove:
|
||||
// import videoIcon from '../../assets/images/icons8-video-call-50.png';
|
||||
```
|
||||
</action>
|
||||
<verify>
|
||||
New components imported:
|
||||
`grep "JKResyncButton\|JKVideoButton" jam-ui/src/components/client/JKSessionScreen.js | head -5`
|
||||
|
||||
Old state removed:
|
||||
`grep "videoLoading\|resyncLoading" jam-ui/src/components/client/JKSessionScreen.js | wc -l` should be 0
|
||||
|
||||
Components used in JSX:
|
||||
`grep "<JKResyncButton\|<JKVideoButton" jam-ui/src/components/client/JKSessionScreen.js`
|
||||
|
||||
ESLint passes:
|
||||
`cd jam-ui && npx eslint src/components/client/JKSessionScreen.js src/components/client/JKResyncButton.js src/components/client/JKVideoButton.js --max-warnings=0`
|
||||
</verify>
|
||||
<done>
|
||||
JKSessionScreen refactored to use JKResyncButton and JKVideoButton, loading state no longer in parent
|
||||
</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
1. New components exist:
|
||||
```bash
|
||||
ls -la jam-ui/src/components/client/JKResyncButton.js jam-ui/src/components/client/JKVideoButton.js
|
||||
```
|
||||
|
||||
2. Loading state removed from JKSessionScreen:
|
||||
```bash
|
||||
grep -c "videoLoading\|resyncLoading" jam-ui/src/components/client/JKSessionScreen.js
|
||||
```
|
||||
Should return 0
|
||||
|
||||
3. Components used:
|
||||
```bash
|
||||
grep "<JKResyncButton\|<JKVideoButton" jam-ui/src/components/client/JKSessionScreen.js
|
||||
```
|
||||
Should show both component usages
|
||||
|
||||
4. ESLint passes:
|
||||
```bash
|
||||
cd jam-ui && npx eslint src/components/client/JKSessionScreen.js src/components/client/JKResyncButton.js src/components/client/JKVideoButton.js --max-warnings=0
|
||||
```
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- [ ] JKResyncButton.js created with local loading state
|
||||
- [ ] JKVideoButton.js created with local loading state
|
||||
- [ ] JKSessionScreen imports and uses both new components
|
||||
- [ ] videoLoading and resyncLoading useState removed from JKSessionScreen
|
||||
- [ ] handleResync and handleVideoClick removed from JKSessionScreen
|
||||
- [ ] All files pass ESLint
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/32-state-update-optimization/32-03-SUMMARY.md`
|
||||
</output>
|
||||
|
|
@ -0,0 +1,223 @@
|
|||
---
|
||||
phase: 32-state-update-optimization
|
||||
plan: 04
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on: ["32-03"]
|
||||
files_modified:
|
||||
- jam-ui/src/components/client/JKSessionScreen.js
|
||||
autonomous: false
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "Local state audit completed with documented findings"
|
||||
- "Any additional colocation candidates identified and addressed"
|
||||
- "JKSessionScreen re-render count measurably reduced"
|
||||
artifacts:
|
||||
- path: ".planning/phases/32-state-update-optimization/32-04-SUMMARY.md"
|
||||
provides: "Audit results and re-render measurements"
|
||||
contains: "State Colocation Audit"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Audit JKSessionScreen local state and verify re-render reduction
|
||||
|
||||
Purpose: Complete COLOC-03 requirement by auditing remaining useState declarations in JKSessionScreen, identifying any additional colocation candidates, and measuring the re-render improvement from Phase 32 changes.
|
||||
|
||||
Output: Audit documentation, any additional colocations if applicable, before/after re-render measurements
|
||||
</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
|
||||
|
||||
# Prior plan summary needed for context
|
||||
@.planning/phases/32-state-update-optimization/32-03-SUMMARY.md
|
||||
|
||||
# Source file to audit
|
||||
@jam-ui/src/components/client/JKSessionScreen.js
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Audit remaining useState declarations</name>
|
||||
<files>jam-ui/src/components/client/JKSessionScreen.js</files>
|
||||
<action>
|
||||
Analyze all remaining useState declarations in JKSessionScreen after Plan 03 changes.
|
||||
|
||||
1. List all useState declarations:
|
||||
```bash
|
||||
grep -n "useState" jam-ui/src/components/client/JKSessionScreen.js
|
||||
```
|
||||
|
||||
2. For each state variable, document:
|
||||
- Name and purpose
|
||||
- Where it's used (components that read it)
|
||||
- Whether it could be colocated to a child component
|
||||
- Decision: KEEP (used by multiple children or parent logic) or CANDIDATE (single child usage)
|
||||
|
||||
Expected states after Plan 03 (with videoLoading/resyncLoading removed):
|
||||
- volumeLevel - Used in volume modal, could be colocated to JKSessionVolumeModal
|
||||
- leaveRating - Used in leave modal flow
|
||||
- leaveComments - Used in leave modal flow
|
||||
- leaveLoading - Used in leave modal button
|
||||
|
||||
3. Analyze each candidate:
|
||||
- volumeLevel: Shared between slider and display, modal handles both - KEEP (modal manages its own state)
|
||||
- leaveRating/leaveComments/leaveLoading: All used within leave modal flow
|
||||
- These could potentially move to JKSessionLeaveModal
|
||||
- But leave modal may need values from parent for submit
|
||||
- Decision: Document as FUTURE CANDIDATE - not critical path for this phase
|
||||
|
||||
4. Record findings in task output for summary.
|
||||
</action>
|
||||
<verify>
|
||||
Review useState declarations present:
|
||||
`grep -c "useState" jam-ui/src/components/client/JKSessionScreen.js`
|
||||
|
||||
Verify videoLoading/resyncLoading removed (should be 0):
|
||||
`grep -c "videoLoading\|resyncLoading" jam-ui/src/components/client/JKSessionScreen.js`
|
||||
</verify>
|
||||
<done>
|
||||
useState audit completed with documented decisions for each state variable
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Document re-render measurement approach</name>
|
||||
<files>jam-ui/src/components/client/JKSessionScreen.js</files>
|
||||
<action>
|
||||
Add a temporary render counter for measurement purposes. This will be removed after verification.
|
||||
|
||||
1. Add render counter at the beginning of the JKSessionScreen function body:
|
||||
```javascript
|
||||
// TEMPORARY: Render counter for Phase 32 verification
|
||||
// Remove after verifying optimization
|
||||
const renderCountRef = React.useRef(0);
|
||||
renderCountRef.current += 1;
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log(`[JKSessionScreen] Render #${renderCountRef.current}`);
|
||||
}
|
||||
```
|
||||
|
||||
2. Document in the summary how to measure:
|
||||
- Open React DevTools Profiler
|
||||
- Start recording
|
||||
- Join a session (triggers track sync and mixer setup)
|
||||
- Wait 10 seconds for stabilization
|
||||
- Stop recording
|
||||
- Check JKSessionScreen render count
|
||||
- Compare with expected reduction (baseline was ~20-30 renders on join, target is 50%+ reduction)
|
||||
|
||||
3. The success criteria from roadmap: "JKSessionScreen re-render count reduced by 50%+"
|
||||
- This measurement is a checkpoint for the user to verify
|
||||
- Document expected numbers in summary
|
||||
</action>
|
||||
<verify>
|
||||
Render counter present (for now):
|
||||
`grep "renderCountRef" jam-ui/src/components/client/JKSessionScreen.js | head -3`
|
||||
|
||||
Counter only logs in development:
|
||||
`grep "NODE_ENV.*development" jam-ui/src/components/client/JKSessionScreen.js`
|
||||
</verify>
|
||||
<done>
|
||||
Render counter added for measurement, documentation of measurement approach prepared
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking">
|
||||
<what-built>
|
||||
Phase 32 state update optimizations:
|
||||
1. Single debounced track sync (replacing 3 setTimeout calls)
|
||||
2. Stable useDebounceCallback hook for trackChanges
|
||||
3. Conditional dispatch in mixer categorization
|
||||
4. JKResyncButton with colocated loading state
|
||||
5. JKVideoButton with colocated loading state
|
||||
6. State colocation audit completed
|
||||
</what-built>
|
||||
<how-to-verify>
|
||||
1. **Functional verification:**
|
||||
- Start jam-ui: `cd jam-ui && npm run start`
|
||||
- Log in and join a session
|
||||
- Verify session loads correctly
|
||||
- Click Resync button - should show loading spinner
|
||||
- Click Video button - should open video conferencing
|
||||
|
||||
2. **Re-render measurement:**
|
||||
- Open React DevTools in browser (F12 -> Profiler tab)
|
||||
- Click "Start profiling"
|
||||
- Join a new session (or refresh current session)
|
||||
- Wait 10 seconds
|
||||
- Click "Stop profiling"
|
||||
- Look at JKSessionScreen render count
|
||||
- Expected: Significantly fewer renders than before (target: 50%+ reduction)
|
||||
- Check console for `[JKSessionScreen] Render #X` logs
|
||||
|
||||
3. **Track sync verification:**
|
||||
- Open Network tab in DevTools
|
||||
- Join a session
|
||||
- Filter by "track" in network requests
|
||||
- Should see only ONE track sync call (not 3)
|
||||
|
||||
4. **Console verification:**
|
||||
- After joining session, check console for:
|
||||
- `[Track Sync] Mixers ready, scheduling single debounced sync` (if uncommented)
|
||||
- `[useMixerHelper] Metronome mixers changed, dispatching` (on first categorization)
|
||||
- Subsequent mixer updates should NOT log dispatch messages
|
||||
|
||||
5. **Remove render counter:**
|
||||
- After verification, the render counter can be removed from JKSessionScreen
|
||||
- Or keep for ongoing monitoring (your preference)
|
||||
</how-to-verify>
|
||||
<resume-signal>
|
||||
Type "approved" if:
|
||||
- Session joins and works correctly
|
||||
- Buttons function as expected
|
||||
- Re-render count is measurably reduced
|
||||
- Track sync fires once (not 3 times)
|
||||
|
||||
Or describe any issues found.
|
||||
</resume-signal>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
1. Audit completed (Task 1):
|
||||
- All useState documented with decisions
|
||||
- No missed colocation opportunities for this phase
|
||||
|
||||
2. Measurement approach documented (Task 2):
|
||||
- Render counter added
|
||||
- Steps for measuring improvement clear
|
||||
|
||||
3. Human verification (Task 3):
|
||||
- Functional tests pass
|
||||
- Performance improvement confirmed
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- [ ] All useState declarations audited and documented
|
||||
- [ ] Render counter added for measurement
|
||||
- [ ] User verifies session join works
|
||||
- [ ] User verifies buttons work
|
||||
- [ ] User confirms re-render reduction (50%+ target)
|
||||
- [ ] Track sync fires once per session join
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/32-state-update-optimization/32-04-SUMMARY.md`
|
||||
|
||||
Include in summary:
|
||||
- State audit results table
|
||||
- Re-render measurements (before/after if available)
|
||||
- Any additional candidates for future optimization
|
||||
</output>
|
||||
Loading…
Reference in New Issue