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

394 lines
12 KiB
Markdown

---
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" />}
&nbsp;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" />)}
&nbsp;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>