12 KiB
| phase | plan | type | wave | depends_on | files_modified | autonomous | must_haves | |||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 32-state-update-optimization | 03 | execute | 1 |
|
true |
|
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
<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.mdSource files
@jam-ui/src/components/client/JKSessionScreen.js
Task 1: Create JKResyncButton component jam-ui/src/components/client/JKResyncButton.js Create a new self-contained button component with colocated loading state.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
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
JKResyncButton component created with colocated loading state
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
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
JKVideoButton component created with colocated loading state
- Add imports at top:
import JKResyncButton from './JKResyncButton';
import JKVideoButton from './JKVideoButton';
- Remove useState declarations (around lines 202-205):
// DELETE these lines:
const [videoLoading, setVideoLoading] = useState(false);
const [resyncLoading, setResyncLoading] = useState(false);
- Remove handleResync function (around lines 1056-1075):
// DELETE the entire handleResync function
// const handleResync = useCallback(async (e) => { ... });
- Remove handleVideoClick function (around lines 977-1002):
// DELETE the entire handleVideoClick function
// const handleVideoClick = async () => { ... };
- Update the button JSX in the toolbar section (around lines 1341-1388).
Find the Video button:
<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:
<JKVideoButton
canVideo={canVideo}
getVideoUrl={() => getVideoConferencingRoomUrl(currentSession.id)}
onUpgradePrompt={showVideoUpgradePrompt}
/>
Find the Resync button:
<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:
<JKResyncButton resyncAudio={resyncAudio} />
- Keep the following functions in JKSessionScreen (they're still needed):
canVideo(permission check)showVideoUpgradePrompt(toast display)resyncAudio(from useMediaActions)
- Remove videoIcon import if no longer used elsewhere:
// Check if videoIcon is used elsewhere, if not remove:
// import videoIcon from '../../assets/images/icons8-video-call-50.png';
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
JKSessionScreen refactored to use JKResyncButton and JKVideoButton, loading state no longer in parent
-
Loading state removed from JKSessionScreen:
grep -c "videoLoading\|resyncLoading" jam-ui/src/components/client/JKSessionScreen.jsShould return 0
-
Components used:
grep "<JKResyncButton\|<JKVideoButton" jam-ui/src/components/client/JKSessionScreen.jsShould show both component usages
-
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
<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>