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

12 KiB

phase plan type wave depends_on files_modified autonomous must_haves
32-state-update-optimization 03 execute 1
jam-ui/src/components/client/JKSessionScreen.js
jam-ui/src/components/client/JKResyncButton.js
jam-ui/src/components/client/JKVideoButton.js
true
truths artifacts key_links
resyncLoading state lives in JKResyncButton (not JKSessionScreen)
videoLoading state lives in JKVideoButton (not JKSessionScreen)
Loading state changes don't re-render JKSessionScreen
path provides exports
jam-ui/src/components/client/JKResyncButton.js Self-contained resync button with loading state
JKResyncButton
path provides exports
jam-ui/src/components/client/JKVideoButton.js Self-contained video button with loading state
JKVideoButton
path provides contains
jam-ui/src/components/client/JKSessionScreen.js Uses extracted button components JKResyncButton
from to via pattern
jam-ui/src/components/client/JKSessionScreen.js JKResyncButton import and render <JKResyncButton
from to via pattern
jam-ui/src/components/client/JKSessionScreen.js JKVideoButton import and render <JKVideoButton
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

<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 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

Task 2: Create JKVideoButton component jam-ui/src/components/client/JKVideoButton.js Create a new self-contained video 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';
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 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

Task 3: Refactor JKSessionScreen to use extracted components jam-ui/src/components/client/JKSessionScreen.js Replace inline button implementations with the new components.
  1. Add imports at top:
import JKResyncButton from './JKResyncButton';
import JKVideoButton from './JKVideoButton';
  1. Remove useState declarations (around lines 202-205):
// DELETE these lines:
const [videoLoading, setVideoLoading] = useState(false);
const [resyncLoading, setResyncLoading] = useState(false);
  1. Remove handleResync function (around lines 1056-1075):
// DELETE the entire handleResync function
// const handleResync = useCallback(async (e) => { ... });
  1. Remove handleVideoClick function (around lines 977-1002):
// DELETE the entire handleVideoClick function
// const handleVideoClick = async () => { ... };
  1. 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" />)}
  &nbsp;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} />
  1. Keep the following functions in JKSessionScreen (they're still needed):
  • canVideo (permission check)
  • showVideoUpgradePrompt (toast display)
  • resyncAudio (from useMediaActions)
  1. 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

1. New components exist: ```bash ls -la jam-ui/src/components/client/JKResyncButton.js jam-ui/src/components/client/JKVideoButton.js ```
  1. Loading state removed from JKSessionScreen:

    grep -c "videoLoading\|resyncLoading" jam-ui/src/components/client/JKSessionScreen.js
    

    Should return 0

  2. Components used:

    grep "<JKResyncButton\|<JKVideoButton" jam-ui/src/components/client/JKSessionScreen.js
    

    Should show both component usages

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