jam-cloud/jam-ui/docs/REDUX_MIGRATION_GUIDE.md

8.8 KiB

Redux Migration Guide for JKSessionScreen

Files Created

1. src/store/features/activeSessionSlice.js

Redux slice for managing the active session state including:

  • Session data and lifecycle (joining, leaving)
  • Participants management
  • User tracks, jam tracks, and backing tracks
  • Recording state
  • Connection status

Key Actions:

  • fetchActiveSession(sessionId) - Async thunk to fetch session data
  • joinActiveSession({ sessionId, options }) - Async thunk to join session
  • leaveActiveSession(sessionId) - Async thunk to leave session
  • addParticipant(participant) - Add a participant to the session
  • removeParticipant(participantId) - Remove a participant
  • addUserTrack(track) - Add a user track
  • setSelectedJamTrack(jamTrack) - Set the selected jam track
  • startRecording(recordingId) - Start recording
  • stopRecording() - Stop recording
  • setConnectionStatus(status) - Update connection status
  • clearSession() - Clear all session state

Key Selectors:

  • selectActiveSession(state) - Get session data
  • selectJoinStatus(state) - Get join status
  • selectHasJoined(state) - Check if user has joined
  • selectParticipants(state) - Get participants list
  • selectUserTracks(state) - Get user tracks
  • selectIsRecording(state) - Check if recording is active
  • selectConnectionStatus(state) - Get connection status

2. src/store/features/sessionUISlice.js

Redux slice for managing session UI state including:

  • Modal visibility (settings, invite, volume, recording, leave, etc.)
  • Panel visibility (chat, mixer, participants, tracks)
  • View preferences (layout, video visibility, sidebar state)

Key Actions:

  • openModal(modalName) - Open a specific modal
  • closeModal(modalName) - Close a specific modal
  • toggleModal(modalName) - Toggle a modal
  • closeAllModals() - Close all modals
  • togglePanel(panelName) - Toggle a panel
  • setParticipantLayout(layout) - Set participant layout ('grid' | 'list' | 'compact')
  • toggleMixer() - Toggle mixer visibility
  • resetUI() - Reset all UI state

Key Selectors:

  • selectModal(modalName)(state) - Check if a modal is open
  • selectIsAnyModalOpen(state) - Check if any modal is open
  • selectPanel(panelName)(state) - Check if a panel is open
  • selectShowMixer(state) - Check if mixer is visible
  • selectParticipantLayout(state) - Get participant layout

3. src/hooks/useSessionWebSocket.js

Custom hook that integrates WebSocket messages with Redux actions. This hook:

  • Listens to WebSocket events from jamServer
  • Dispatches appropriate Redux actions when events occur
  • Handles cleanup on unmount

Usage:

import { useSessionWebSocket } from '../hooks/useSessionWebSocket';

function JKSessionScreen() {
  const { sessionId } = useParams();

  // This hook will automatically dispatch Redux actions
  // when WebSocket events are received
  useSessionWebSocket(sessionId);

  // ... rest of component
}

4. Updated src/store/store.js

Added the new reducers to the Redux store:

  • activeSession - Active session state
  • sessionUI - Session UI state

Migration Strategy

Phase 1: Quick Wins - Migrate Modal State (Start Here)

Replace useState for modals with Redux:

Before:

const [showSettingsModal, setShowSettingsModal] = useState(false);
const [showInviteModal, setShowInviteModal] = useState(false);

// In JSX
<button onClick={() => setShowSettingsModal(true)}>Settings</button>
{showSettingsModal && <SettingsModal onClose={() => setShowSettingsModal(false)} />}

After:

import { useDispatch, useSelector } from 'react-redux';
import { openModal, closeModal, selectModal } from '../../store/features/sessionUISlice';

const dispatch = useDispatch();
const showSettingsModal = useSelector(selectModal('settings'));

// In JSX
<button onClick={() => dispatch(openModal('settings'))}>Settings</button>
{showSettingsModal && <SettingsModal onClose={() => dispatch(closeModal('settings'))} />}

Benefits:

  • Removes 5-8 useState hooks immediately
  • Easy to test
  • Good practice for team

Phase 2: Migrate Session Lifecycle

Replace session join/leave logic with Redux thunks:

Before:

const [hasJoined, setHasJoined] = useState(false);
const [sessionGuardsPassed, setSessionGuardsPassed] = useState(false);

const handleJoinSession = async () => {
  // Guard checks...
  setSessionGuardsPassed(true);
  const response = await joinSession(sessionId, options);
  setHasJoined(true);
};

After:

import { useDispatch, useSelector } from 'react-redux';
import {
  joinActiveSession,
  selectJoinStatus,
  selectHasJoined
} from '../../store/features/activeSessionSlice';

const dispatch = useDispatch();
const joinStatus = useSelector(selectJoinStatus);
const hasJoined = useSelector(selectHasJoined);

const handleJoinSession = async () => {
  // Guard checks...
  await dispatch(joinActiveSession({ sessionId, options }));
};

Phase 3: Migrate Entity Collections

Replace useState arrays with Redux:

Before:

const [userTracks, setUserTracks] = useState([]);
const [participants, setParticipants] = useState([]);

// When WebSocket message arrives
jamServer.on('trackAdded', (data) => {
  setUserTracks([...userTracks, data.track]);
});

After:

import { useDispatch, useSelector } from 'react-redux';
import { selectUserTracks, selectParticipants } from '../../store/features/activeSessionSlice';
import { useSessionWebSocket } from '../../hooks/useSessionWebSocket';

const userTracks = useSelector(selectUserTracks);
const participants = useSelector(selectParticipants);

// WebSocket integration is automatic via custom hook
useSessionWebSocket(sessionId);

// No manual event handlers needed!

Phase 4: Replace CurrentSessionContext

Once most state is in Redux, you can simplify or remove CurrentSessionContext:

Before:

const { currentSession, setCurrentSession, refreshSession } = useCurrentSessionContext();

After:

import { useSelector, useDispatch } from 'react-redux';
import { selectActiveSession, fetchActiveSession } from '../../store/features/activeSessionSlice';

const currentSession = useSelector(selectActiveSession);
const dispatch = useDispatch();

// To refresh
dispatch(fetchActiveSession(sessionId));

Testing Your Migration

Check Redux DevTools

Install Redux DevTools browser extension to:

  1. View the entire state tree
  2. See every action dispatched
  3. Time-travel debug state changes
  4. Export/import state for testing

Verify State Updates

After each phase, verify:

// Check if state is in Redux
console.log('Redux State:', store.getState().activeSession);
console.log('Redux UI:', store.getState().sessionUI);

Test WebSocket Integration

In JKSessionScreen, verify the custom hook is working:

useSessionWebSocket(sessionId);

// Watch Redux DevTools - you should see actions dispatched
// when WebSocket events occur

Common Patterns

Pattern 1: Conditional Rendering Based on Redux State

const isRecording = useSelector(selectIsRecording);
const connectionStatus = useSelector(selectConnectionStatus);

return (
  <>
    {connectionStatus === 'disconnected' && <ConnectionAlert />}
    {isRecording && <RecordingIndicator />}
  </>
);

Pattern 2: Dispatching Multiple Actions

const handleLeaveSession = async () => {
  // Close all modals first
  dispatch(closeAllModals());

  // Leave the session
  await dispatch(leaveActiveSession(sessionId));

  // Navigate away
  navigate('/dashboard');
};

Pattern 3: Using Multiple Selectors

const session = useSelector(selectActiveSession);
const participants = useSelector(selectParticipants);
const tracks = useSelector(selectUserTracks);
const showMixer = useSelector(selectModal('mixer'));

// All selectors are memoized, so re-renders only happen when data changes

Next Steps

  1. Start with Phase 1: Migrate modal state first - it's the easiest and gives immediate value
  2. Test thoroughly: After each phase, test the UI and check Redux DevTools
  3. Document as you go: Update component documentation with Redux patterns
  4. Remove old contexts gradually: Once state is migrated, clean up unused contexts

Notes

  • The WebSocket event names in useSessionWebSocket.js are examples. Update them to match your actual Protocol Buffer message types.
  • The jamServer API methods (on, off) may differ based on your implementation. Adjust accordingly.
  • Consider creating additional custom hooks for specific features (e.g., useSessionRecording, useSessionTracks) that encapsulate Redux logic.

Questions?

If you need help with:

  • Specific WebSocket message types and how to handle them
  • Complex state transformations
  • Performance optimization with selectors
  • Testing Redux logic

Just ask!