Skip to content

Commit 5852fb2

Browse files
authored
Merge pull request #276 from naomiaro/fix/react-production-readiness
Improve React production readiness
2 parents beaad1b + fa0d8e6 commit 5852fb2

File tree

16 files changed

+141
-136
lines changed

16 files changed

+141
-136
lines changed

CLAUDE.MD

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,11 +90,12 @@ pnpm publish --filter @waveform-playlist/NEW-PACKAGE --no-git-checks --access pu
9090

9191
**API Source of Truth:**
9292
- Context types (hooks, state, controls): `packages/browser/src/WaveformPlaylistContext.tsx`
93+
- Context hooks: `usePlaybackAnimation`, `usePlaylistState`, `usePlaylistControls`, `usePlaylistData` (no combined hook — `useWaveformPlaylist` was removed in v6.0.2)
9394
- MediaElement context types: `packages/browser/src/MediaElementPlaylistContext.tsx`
9495
- AudioTrackConfig interface: `packages/browser/src/hooks/useAudioTracks.ts`
9596
- Effects hooks return types: `packages/browser/src/hooks/useDynamicEffects.ts`, `useTrackDynamicEffects.ts`
9697

97-
**Common Doc Drift:** Non-existent hooks, wrong property names (e.g., `gain` vs `volume`, `seek` vs `seekTo`), properties attributed to wrong context hooks. Always cross-check docs against source interfaces.
98+
**Common Doc Drift:** Non-existent hooks (e.g., `useWaveformPlaylist` was removed), wrong property names (e.g., `gain` vs `volume`, `seek` vs `seekTo`), properties attributed to wrong context hooks. Always cross-check docs against source interfaces.
9899

99100
**Verify docs render:** `pnpm --filter website build` (CSS calc warnings are pre-existing, harmless)
100101

@@ -546,6 +547,10 @@ const analyser = context.createAnalyser();
546547
11. **Playlist Loading Detection** - Use `data-playlist-state` attribute and `waveform-playlist:ready` custom event for reliable loading detection in CSS, E2E tests, and external integrations
547548
12. **Stable React Keys for Tracks/Clips** - Always use `track.id` / `clip.clipId` as React keys, never array indices. Index-based keys cause DOM reuse on removal, breaking `transferControlToOffscreen()` (can only be called once per canvas) and causing stale OffscreenCanvas references.
548549
13. **Per-Track Maps Must Use Track ID** - Any `Map` storing per-track overrides (render modes, configs) must be keyed by `track.id` (string), not array index. Index keys break when tracks are added/removed.
550+
14. **Context Value Memoization** - All context value objects in providers must be wrapped with `useMemo`. Extract inline callbacks into `useCallback` first to avoid dependency churn.
551+
15. **Error Boundary Available** - `PlaylistErrorBoundary` from `@waveform-playlist/ui-components` catches render errors. Uses plain CSS (no styled-components) so it works without ThemeProvider.
552+
16. **Audio Disconnect Diagnostics** - Use `console.warn('[waveform-playlist] ...')` in catch blocks for audio node disconnect errors, never silently swallow.
553+
17. **Fetch Cleanup with AbortController** - `useAudioTracks` uses AbortController to cancel in-flight fetches on cleanup. Follow this pattern for any fetch in useEffect.
549554

550555
### Playlist Loading Detection
551556

PROJECT_STRUCTURE.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -878,7 +878,7 @@ The playlist now provides a **flexible/headless API** using React Context and pr
878878
- `WaveformPlaylistProvider` wraps your app and provides state via context
879879
- Primitive components (PlayButton, ZoomInButton, etc.) work anywhere inside the provider
880880
- `Waveform` component accepts a render prop for custom track controls
881-
- `useWaveformPlaylist` hook provides direct access to state/methods
881+
- Split context hooks (`usePlaylistData`, `usePlaylistControls`, etc.) provide direct access to state/methods
882882

883883
### Benefits
884884

@@ -900,12 +900,14 @@ import {
900900
StopButton,
901901
Waveform,
902902
MasterVolumeControl,
903-
useWaveformPlaylist,
903+
usePlaylistData,
904+
usePlaylistControls,
904905
} from '@waveform-playlist/browser';
905906

906907
// Custom track controls
907908
const CustomTrackControls = ({ trackIndex }) => {
908-
const { trackStates, setTrackMute } = useWaveformPlaylist();
909+
const { trackStates } = usePlaylistData();
910+
const { setTrackMute } = usePlaylistControls();
909911
return (
910912
<button onClick={() => setTrackMute(trackIndex, !trackStates[trackIndex].muted)}>
911913
{trackStates[trackIndex].muted ? 'Unmute' : 'Mute'}

packages/browser/src/WaveformPlaylistContext.tsx

Lines changed: 19 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -41,77 +41,6 @@ export interface TrackState {
4141
pan: number;
4242
}
4343

44-
export interface WaveformPlaylistContextValue {
45-
// State
46-
isPlaying: boolean;
47-
currentTime: number;
48-
duration: number;
49-
audioBuffers: AudioBuffer[];
50-
peaksDataArray: TrackClipPeaks[]; // Array of tracks, each containing array of clip peaks
51-
trackStates: TrackState[];
52-
annotations: AnnotationData[];
53-
activeAnnotationId: string | null;
54-
selectionStart: number;
55-
selectionEnd: number;
56-
isAutomaticScroll: boolean;
57-
continuousPlay: boolean;
58-
linkEndpoints: boolean;
59-
annotationsEditable: boolean;
60-
61-
// Playback controls
62-
play: (startTime?: number, playDuration?: number) => Promise<void>;
63-
pause: () => void;
64-
stop: () => void;
65-
setCurrentTime: (time: number) => void;
66-
67-
// Track controls
68-
setTrackMute: (trackIndex: number, muted: boolean) => void;
69-
setTrackSolo: (trackIndex: number, soloed: boolean) => void;
70-
setTrackVolume: (trackIndex: number, volume: number) => void;
71-
setTrackPan: (trackIndex: number, pan: number) => void;
72-
73-
// Selection
74-
setSelection: (start: number, end: number) => void;
75-
76-
// Time format
77-
timeFormat: TimeFormat;
78-
setTimeFormat: (format: TimeFormat) => void;
79-
formatTime: (seconds: number) => string;
80-
81-
// Zoom
82-
samplesPerPixel: number;
83-
zoomIn: () => void;
84-
zoomOut: () => void;
85-
canZoomIn: boolean;
86-
canZoomOut: boolean;
87-
88-
// Master volume
89-
masterVolume: number;
90-
setMasterVolume: (volume: number) => void;
91-
92-
// Automatic scroll
93-
setAutomaticScroll: (enabled: boolean) => void;
94-
setScrollContainer: (element: HTMLDivElement | null) => void;
95-
96-
// Annotation controls
97-
setContinuousPlay: (enabled: boolean) => void;
98-
setLinkEndpoints: (enabled: boolean) => void;
99-
setAnnotationsEditable: (enabled: boolean) => void;
100-
setAnnotations: React.Dispatch<React.SetStateAction<AnnotationData[]>>;
101-
setActiveAnnotationId: (id: string | null) => void;
102-
103-
// Refs
104-
playoutRef: React.RefObject<TonePlayout | null>;
105-
currentTimeRef: React.RefObject<number>;
106-
107-
// Playlist info
108-
sampleRate: number;
109-
waveHeight: number;
110-
timeScaleHeight: number;
111-
minimumPlaylistHeight: number;
112-
controls: { show: boolean; width: number };
113-
}
114-
11544
// Split contexts for performance optimization
11645
// High-frequency updates (currentTime) are isolated from low-frequency state changes
11746

@@ -222,9 +151,6 @@ const PlaylistStateContext = createContext<PlaylistStateContextValue | null>(nul
222151
const PlaylistControlsContext = createContext<PlaylistControlsContextValue | null>(null);
223152
const PlaylistDataContext = createContext<PlaylistDataContextValue | null>(null);
224153

225-
// Keep the original context for backwards compatibility
226-
const WaveformPlaylistContext = createContext<WaveformPlaylistContextValue | null>(null);
227-
228154
export interface WaveformPlaylistProviderProps {
229155
tracks: ClipTrack[]; // Updated to use clip-based model
230156
timescale?: boolean;
@@ -1071,15 +997,15 @@ export const WaveformPlaylistProvider: React.FC<WaveformPlaylistProviderProps> =
1071997
// Split context values for performance optimization
1072998
// High-frequency updates (currentTime) isolated from other state
1073999

1074-
const animationValue: PlaybackAnimationContextValue = {
1000+
const animationValue: PlaybackAnimationContextValue = useMemo(() => ({
10751001
isPlaying,
10761002
currentTime,
10771003
currentTimeRef,
10781004
playbackStartTimeRef,
10791005
audioStartPositionRef,
1080-
};
1006+
}), [isPlaying, currentTime, currentTimeRef, playbackStartTimeRef, audioStartPositionRef]);
10811007

1082-
const stateValue: PlaylistStateContextValue = {
1008+
const stateValue: PlaylistStateContextValue = useMemo(() => ({
10831009
continuousPlay,
10841010
linkEndpoints,
10851011
annotationsEditable,
@@ -1092,18 +1018,24 @@ export const WaveformPlaylistProvider: React.FC<WaveformPlaylistProviderProps> =
10921018
selectedTrackId,
10931019
loopStart,
10941020
loopEnd,
1095-
};
1021+
}), [continuousPlay, linkEndpoints, annotationsEditable, isAutomaticScroll, isLoopEnabled, annotations, activeAnnotationId, selectionStart, selectionEnd, selectedTrackId, loopStart, loopEnd]);
10961022

1097-
const controlsValue: PlaylistControlsContextValue = {
1023+
const setCurrentTimeControl = useCallback((time: number) => {
1024+
currentTimeRef.current = time;
1025+
setCurrentTime(time);
1026+
}, [currentTimeRef]);
1027+
1028+
const setAutomaticScrollControl = useCallback((enabled: boolean) => {
1029+
setIsAutomaticScroll(enabled);
1030+
}, []);
1031+
1032+
const controlsValue: PlaylistControlsContextValue = useMemo(() => ({
10981033
// Playback controls
10991034
play,
11001035
pause,
11011036
stop,
11021037
seekTo,
1103-
setCurrentTime: (time: number) => {
1104-
currentTimeRef.current = time;
1105-
setCurrentTime(time);
1106-
},
1038+
setCurrentTime: setCurrentTimeControl,
11071039

11081040
// Track controls
11091041
setTrackMute,
@@ -1127,9 +1059,7 @@ export const WaveformPlaylistProvider: React.FC<WaveformPlaylistProviderProps> =
11271059
setMasterVolume,
11281060

11291061
// Automatic scroll
1130-
setAutomaticScroll: (enabled: boolean) => {
1131-
setIsAutomaticScroll(enabled);
1132-
},
1062+
setAutomaticScroll: setAutomaticScrollControl,
11331063
setScrollContainer,
11341064
scrollContainerRef,
11351065

@@ -1146,9 +1076,9 @@ export const WaveformPlaylistProvider: React.FC<WaveformPlaylistProviderProps> =
11461076
setLoopRegionFromSelection,
11471077
clearLoopRegion,
11481078

1149-
};
1079+
}), [play, pause, stop, seekTo, setCurrentTimeControl, setTrackMute, setTrackSolo, setTrackVolume, setTrackPan, setSelection, setSelectedTrackId, setTimeFormat, formatTime, zoom.zoomIn, zoom.zoomOut, setMasterVolume, setAutomaticScrollControl, setScrollContainer, scrollContainerRef, setContinuousPlay, setLinkEndpoints, setAnnotationsEditable, setAnnotations, setActiveAnnotationId, setLoopEnabled, setLoopRegion, setLoopRegionFromSelection, clearLoopRegion]);
11501080

1151-
const dataValue: PlaylistDataContextValue = {
1081+
const dataValue: PlaylistDataContextValue = useMemo(() => ({
11521082
duration,
11531083
audioBuffers,
11541084
peaksDataArray,
@@ -1170,15 +1100,7 @@ export const WaveformPlaylistProvider: React.FC<WaveformPlaylistProviderProps> =
11701100
progressBarWidth,
11711101
isReady,
11721102
mono,
1173-
};
1174-
1175-
// Combined value for backwards compatibility
1176-
const value: WaveformPlaylistContextValue = {
1177-
...animationValue,
1178-
...stateValue,
1179-
...controlsValue,
1180-
...dataValue,
1181-
};
1103+
}), [duration, audioBuffers, peaksDataArray, trackStates, tracks, sampleRate, waveHeight, timeScaleHeight, minimumPlaylistHeight, controls, playoutRef, samplesPerPixel, timeFormat, masterVolume, zoom.canZoomIn, zoom.canZoomOut, barWidth, barGap, progressBarWidth, isReady, mono]);
11821104

11831105
// Merge user theme with default theme
11841106
const mergedTheme = { ...defaultTheme, ...userTheme };
@@ -1189,9 +1111,7 @@ export const WaveformPlaylistProvider: React.FC<WaveformPlaylistProviderProps> =
11891111
<PlaylistStateContext.Provider value={stateValue}>
11901112
<PlaylistControlsContext.Provider value={controlsValue}>
11911113
<PlaylistDataContext.Provider value={dataValue}>
1192-
<WaveformPlaylistContext.Provider value={value}>
11931114
{children}
1194-
</WaveformPlaylistContext.Provider>
11951115
</PlaylistDataContext.Provider>
11961116
</PlaylistControlsContext.Provider>
11971117
</PlaylistStateContext.Provider>
@@ -1235,12 +1155,3 @@ export const usePlaylistData = () => {
12351155
return context;
12361156
};
12371157

1238-
// Main hook that combines all contexts - use this for backwards compatibility
1239-
// or when you need access to multiple contexts
1240-
export const useWaveformPlaylist = () => {
1241-
const context = useContext(WaveformPlaylistContext);
1242-
if (!context) {
1243-
throw new Error('useWaveformPlaylist must be used within WaveformPlaylistProvider');
1244-
}
1245-
return context;
1246-
};

packages/browser/src/components/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,4 @@ export * from './PlaylistVisualization';
1111
export * from './PlaylistAnnotationList';
1212

1313
// Re-export WaveformPlaylistProvider and types from context
14-
export { WaveformPlaylistProvider, useWaveformPlaylist, type WaveformTrack } from '../WaveformPlaylistContext';
14+
export { WaveformPlaylistProvider, type WaveformTrack } from '../WaveformPlaylistContext';

packages/browser/src/hooks/useAudioTracks.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ export function useAudioTracks(
118118
}
119119

120120
let cancelled = false;
121+
const abortController = new AbortController();
121122
// Track loaded tracks by their config index for progressive mode
122123
const loadedTracksMap = new Map<number, ClipTrack>();
123124

@@ -225,7 +226,7 @@ export function useAudioTracks(
225226
throw new Error(`Track ${index + 1}: Must provide src, audioBuffer, or waveformData`);
226227
}
227228

228-
const response = await fetch(config.src);
229+
const response = await fetch(config.src, { signal: abortController.signal });
229230
if (!response.ok) {
230231
throw new Error(`Failed to fetch ${config.src}: ${response.statusText}`);
231232
}
@@ -275,9 +276,10 @@ export function useAudioTracks(
275276

276277
loadTracks();
277278

278-
// Cleanup function to prevent state updates if component unmounts
279+
// Cleanup: prevent state updates and abort in-flight fetches on unmount
279280
return () => {
280281
cancelled = true;
282+
abortController.abort();
281283
};
282284
}, [configs, progressive]);
283285

packages/browser/src/hooks/useDynamicEffects.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ export function useDynamicEffects(fftSize: number = 256): UseDynamicEffectsRetur
8181
try {
8282
masterGainNode.disconnect();
8383
} catch (e) {
84-
// Ignore disconnect errors
84+
console.warn('[waveform-playlist] Error disconnecting master effects chain:', e);
8585
}
8686

8787
// Get effect instances in order
@@ -101,7 +101,7 @@ export function useDynamicEffects(fftSize: number = 256): UseDynamicEffectsRetur
101101
try {
102102
inst.disconnect();
103103
} catch (e) {
104-
// Ignore
104+
console.warn(`[waveform-playlist] Error disconnecting effect "${inst.id}":`, e);
105105
}
106106
currentNode.connect(inst.effect);
107107
currentNode = inst.effect;

packages/browser/src/hooks/useTrackDynamicEffects.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ export function useTrackDynamicEffects(): UseTrackDynamicEffectsReturn {
8787
try {
8888
graphEnd.disconnect();
8989
} catch (e) {
90-
// Ignore disconnect errors
90+
console.warn(`[waveform-playlist] Error disconnecting track "${trackId}" effect chain:`, e);
9191
}
9292

9393
// Get effect instances in order
@@ -106,7 +106,7 @@ export function useTrackDynamicEffects(): UseTrackDynamicEffectsReturn {
106106
try {
107107
inst.disconnect();
108108
} catch (e) {
109-
// Ignore
109+
console.warn(`[waveform-playlist] Error disconnecting effect "${inst.id}" on track "${trackId}":`, e);
110110
}
111111
currentNode.connect(inst.effect);
112112
currentNode = inst.effect;

packages/browser/src/index.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,12 @@ export type { EffectsFunction, TrackEffectsFunction } from '@waveform-playlist/p
88
// Export new flexible/headless API
99
export {
1010
WaveformPlaylistProvider,
11-
useWaveformPlaylist,
1211
usePlaybackAnimation,
1312
usePlaylistState,
1413
usePlaylistControls,
1514
usePlaylistData,
1615
} from './WaveformPlaylistContext';
17-
export type { WaveformPlaylistContextValue, WaveformTrack, TrackState } from './WaveformPlaylistContext';
16+
export type { WaveformTrack, TrackState } from './WaveformPlaylistContext';
1817

1918
// Export MediaElement-based provider (single-track with playback rate control)
2019
export {

0 commit comments

Comments
 (0)