Tighten React hook dependencies

This commit is contained in:
young committed 2026-08-28 09:54:42 +10:00
1 parent 0375b9c183
commit bc2a5cc7af
5 files changed
+142 -123

No files matched your search

+1 -1
View File
@@ -47,7 +47,7 @@ export default function App() {
loadAllCustomFonts().catch((error) => {
console.error("Failed to load custom fonts:", error);
});
}, []);
}, [isMacOS]);
useEffect(() => {
document.title =
+15 -6
View File
@@ -42,7 +42,7 @@ export function MarqueeText({ text }: { text: string }) {
useLayoutEffect(() => {
const node = staticRef.current;
if (!node) return;
if (!node || node.textContent !== text) return;
const checkOverflow = () => {
setOverflowing(node.scrollWidth > node.clientWidth + 1);
};
@@ -81,7 +81,10 @@ export const SourceSelectorContent = ({
selectedSource = "Screen",
loading = false,
onSourceSelect = () => undefined,
}: Pick<SourceSelectorProps, "screenSources" | "windowSources" | "selectedSource" | "loading" | "onSourceSelect">) => {
}: Pick<
SourceSelectorProps,
"screenSources" | "windowSources" | "selectedSource" | "loading" | "onSourceSelect"
>) => {
const t = useScopedT("launch");
const renderSourceItem = (source: DesktopSource, index: number) => {
const isSelected = selectedSource === source.name;
@@ -116,12 +119,14 @@ export const SourceSelectorContent = ({
)}
</div>
<div className="flex-1 min-w-0 flex flex-col items-start text-left">
<div className="flex-1 min-w-0 flex flex-col items-start text-left">
<div className="text-sm font-medium source-selector-text w-full">
<MarqueeText text={source.windowTitle || source.name} />
</div>
<div className="text-xs source-selector-subtle truncate w-full text-left">
{source.sourceType === "screen" ? t("recording.screen") : t("recording.window")}
{source.sourceType === "screen"
? t("recording.screen")
: t("recording.window")}
</div>
</div>
</button>
@@ -156,7 +161,9 @@ export const SourceSelectorContent = ({
</span>
</div>
<div className="space-y-0.5">
{screenSources.map((source, index) => renderSourceItem(source, index))}
{screenSources.map((source, index) =>
renderSourceItem(source, index),
)}
</div>
</div>
) : null}
@@ -166,7 +173,9 @@ export const SourceSelectorContent = ({
{t("recording.windows")}
</div>
<div className="space-y-0.5">
{windowSources.map((source, index) => renderSourceItem(source, index))}
{windowSources.map((source, index) =>
renderSourceItem(source, index),
)}
</div>
</div>
) : null}
+13 -9
View File
@@ -1260,7 +1260,7 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
const nextVolume = Math.max(0, Math.min(1, volume));
video.volume = nextVolume;
video.muted = nextVolume <= 0.001;
}, [volume, videoPath]);
}, [volume]);
useEffect(() => {
layoutVideoContentRef.current = layoutVideoContent;
@@ -1362,12 +1362,12 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
return () => {
cancelled = true;
};
}, [aspectRatio, borderRadius, cropRegion, frame, frameUpdateCounter, padding]);
}, [frame, frameUpdateCounter]);
// Always re-run geometric layout when layout props change, even if frame sprite isn't reloaded.
useEffect(() => {
layoutVideoContentRef.current?.();
}, [aspectRatio, borderRadius, cropRegion, padding]);
layoutVideoContent();
}, [layoutVideoContent]);
const selectedZoom = useMemo(() => {
if (!selectedZoomId) return null;
@@ -1545,6 +1545,7 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
useEffect(() => {
suspendRenderingRef.current = suspendRendering;
if (!pixiReady) return;
const app = appRef.current;
if (!app?.ticker) {
return;
@@ -1656,6 +1657,7 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
}, [speedRegions]);
useEffect(() => {
if (!pixiReady) return;
const videoEffectsContainer = videoEffectsContainerRef.current;
const zoomBlurFilter = zoomBlurFilterRef.current;
const motionBlurFilter = motionBlurFilterRef.current;
@@ -1783,7 +1785,7 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
motionBlurStateRef.current = createMotionBlurState();
videoEffectsContainer.filters =
zoomMotionBlur > 0 ? [motionBlurFilter, zoomBlurFilter] : null;
}, [videoPath, zoomMotionBlur]);
}, [zoomMotionBlur]);
useEffect(() => {
zoomMotionBlurTuningRef.current = zoomMotionBlurTuning;
@@ -1905,7 +1907,7 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
}
});
});
}, [pixiReady, videoReady, layoutVideoContent, cropRegion]);
}, [pixiReady, videoReady, layoutVideoContent]);
useEffect(() => {
if (!pixiReady || !videoReady) return;
@@ -2013,11 +2015,13 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
syncWebcamMedia();
}, [syncWebcamMedia]);
// biome-ignore lint/correctness/useExhaustiveDependencies: The media path intentionally triggers source-specific state reset.
useEffect(() => {
setWebcamVideoDimensions(null);
lastWebcamSyncTimeRef.current = null;
}, [webcamVideoPath]);
// biome-ignore lint/correctness/useExhaustiveDependencies: The wallpaper identity intentionally resets media synchronization.
useEffect(() => {
lastBackgroundSyncTimeRef.current = null;
}, [wallpaper]);
@@ -2171,8 +2175,9 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
cursorContainerRef.current = null;
videoSpriteRef.current = null;
};
}, [initializePixiRenderer, onError]);
}, [initializePixiRenderer, onError, syncPreviewMotionBlurQuality]);
// biome-ignore lint/correctness/useExhaustiveDependencies: A new media path must reset the persistent video element.
useEffect(() => {
const video = videoRef.current;
if (!video) return;
@@ -2273,7 +2278,7 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
videoSpriteRef.current = null;
};
}, [pixiReady, videoReady, onTimeUpdate, updateOverlayForRegion]);
}, [layoutVideoContent, onPlayStateChange, onTimeUpdate, pixiReady, videoReady]);
useEffect(() => {
if (!pixiReady || !videoReady) return;
@@ -2650,7 +2655,6 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
}, [
pixiReady,
videoReady,
clampFocusToStage,
applyWebcamBubbleLayout,
borderRadius,
padding,
@@ -5,110 +5,115 @@ import type {
} from "@/components/video-editor/audio/audioTypes";
interface UseSourceAudioTrackSettingsParams {
selectedClipId: string | null;
activeClipId: string | null;
sourceAudioTrackSettingsByClip: Record<string, SourceAudioTrackSettings>;
setSourceAudioTrackSettingsByClip: React.Dispatch<
React.SetStateAction<Record<string, SourceAudioTrackSettings>>
>;
defaultSourceAudioTrackSettings: SourceAudioTrackSettings;
setDefaultSourceAudioTrackSettings: React.Dispatch<React.SetStateAction<SourceAudioTrackSettings>>;
selectedClipId: string | null;
activeClipId: string | null;
sourceAudioTrackSettingsByClip: Record<string, SourceAudioTrackSettings>;
setSourceAudioTrackSettingsByClip: React.Dispatch<
React.SetStateAction<Record<string, SourceAudioTrackSettings>>
>;
defaultSourceAudioTrackSettings: SourceAudioTrackSettings;
setDefaultSourceAudioTrackSettings: React.Dispatch<
React.SetStateAction<SourceAudioTrackSettings>
>;
}
export interface UseSourceAudioTrackSettingsResult {
sourceAudioTrackMeta: SourceAudioTrackMeta;
activeSourceAudioTrackSettings: SourceAudioTrackSettings;
selectedClipSourceAudioTrackSettings: SourceAudioTrackSettings;
getSourceAudioTrackSettingsForClip: (clipId: string | null) => SourceAudioTrackSettings;
onSourceAudioTracksMetaChange: (tracks: SourceAudioTrackMeta) => void;
onSelectedClipSourceAudioTrackVolumeChange: (id: string, volume: number) => void;
onSelectedClipSourceAudioTrackNormalizeChange: (id: string, normalize: boolean) => void;
sourceAudioTrackMeta: SourceAudioTrackMeta;
activeSourceAudioTrackSettings: SourceAudioTrackSettings;
selectedClipSourceAudioTrackSettings: SourceAudioTrackSettings;
getSourceAudioTrackSettingsForClip: (clipId: string | null) => SourceAudioTrackSettings;
onSourceAudioTracksMetaChange: (tracks: SourceAudioTrackMeta) => void;
onSelectedClipSourceAudioTrackVolumeChange: (id: string, volume: number) => void;
onSelectedClipSourceAudioTrackNormalizeChange: (id: string, normalize: boolean) => void;
}
function isSameTrackMeta(left: SourceAudioTrackMeta, right: SourceAudioTrackMeta): boolean {
if (left.length !== right.length) return false;
for (let index = 0; index < left.length; index += 1) {
const leftTrack = left[index];
const rightTrack = right[index];
if (!leftTrack || !rightTrack) return false;
if (leftTrack.id !== rightTrack.id || leftTrack.label !== rightTrack.label) {
return false;
}
}
return true;
if (left.length !== right.length) return false;
for (let index = 0; index < left.length; index += 1) {
const leftTrack = left[index];
const rightTrack = right[index];
if (!leftTrack || !rightTrack) return false;
if (leftTrack.id !== rightTrack.id || leftTrack.label !== rightTrack.label) {
return false;
}
}
return true;
}
export function useSourceAudioTrackSettings({
selectedClipId,
activeClipId,
sourceAudioTrackSettingsByClip,
setSourceAudioTrackSettingsByClip,
defaultSourceAudioTrackSettings,
setDefaultSourceAudioTrackSettings,
selectedClipId,
activeClipId,
sourceAudioTrackSettingsByClip,
setSourceAudioTrackSettingsByClip,
defaultSourceAudioTrackSettings,
setDefaultSourceAudioTrackSettings,
}: UseSourceAudioTrackSettingsParams): UseSourceAudioTrackSettingsResult {
const [sourceAudioTrackMeta, setSourceAudioTrackMeta] = useState<SourceAudioTrackMeta>([]);
const [sourceAudioTrackMeta, setSourceAudioTrackMeta] = useState<SourceAudioTrackMeta>([]);
const activeSourceAudioTrackSettings = useMemo(() => {
if (!activeClipId) {
return defaultSourceAudioTrackSettings;
}
return {
...defaultSourceAudioTrackSettings,
...(sourceAudioTrackSettingsByClip[activeClipId] ?? {}),
};
}, [activeClipId, defaultSourceAudioTrackSettings, sourceAudioTrackSettingsByClip]);
const activeSourceAudioTrackSettings = useMemo(() => {
if (!activeClipId) {
return defaultSourceAudioTrackSettings;
}
return {
...defaultSourceAudioTrackSettings,
...(sourceAudioTrackSettingsByClip[activeClipId] ?? {}),
};
}, [activeClipId, defaultSourceAudioTrackSettings, sourceAudioTrackSettingsByClip]);
const selectedClipSourceAudioTrackSettings = useMemo(() => {
if (!selectedClipId) {
return defaultSourceAudioTrackSettings;
}
return {
...defaultSourceAudioTrackSettings,
...(sourceAudioTrackSettingsByClip[selectedClipId] ?? {}),
};
}, [defaultSourceAudioTrackSettings, selectedClipId, sourceAudioTrackSettingsByClip]);
const selectedClipSourceAudioTrackSettings = useMemo(() => {
if (!selectedClipId) {
return defaultSourceAudioTrackSettings;
}
return {
...defaultSourceAudioTrackSettings,
...(sourceAudioTrackSettingsByClip[selectedClipId] ?? {}),
};
}, [defaultSourceAudioTrackSettings, selectedClipId, sourceAudioTrackSettingsByClip]);
const onSourceAudioTracksMetaChange = useCallback((tracks: SourceAudioTrackMeta) => {
setSourceAudioTrackMeta((prev) => (isSameTrackMeta(prev, tracks) ? prev : tracks));
setDefaultSourceAudioTrackSettings((prev) => {
const next: SourceAudioTrackSettings = {};
for (const track of tracks) {
next[track.id] = prev[track.id] ?? { volume: 1, normalize: false };
}
const prevKeys = Object.keys(prev);
const nextKeys = Object.keys(next);
if (prevKeys.length !== nextKeys.length) {
return next;
}
for (const key of nextKeys) {
const prevSetting = prev[key];
const nextSetting = next[key];
if (!prevSetting || !nextSetting) {
return next;
}
if (
prevSetting.volume !== nextSetting.volume ||
prevSetting.normalize !== nextSetting.normalize
) {
return next;
}
}
return prev;
});
}, []);
const onSourceAudioTracksMetaChange = useCallback(
(tracks: SourceAudioTrackMeta) => {
setSourceAudioTrackMeta((prev) => (isSameTrackMeta(prev, tracks) ? prev : tracks));
setDefaultSourceAudioTrackSettings((prev) => {
const next: SourceAudioTrackSettings = {};
for (const track of tracks) {
next[track.id] = prev[track.id] ?? { volume: 1, normalize: false };
}
const prevKeys = Object.keys(prev);
const nextKeys = Object.keys(next);
if (prevKeys.length !== nextKeys.length) {
return next;
}
for (const key of nextKeys) {
const prevSetting = prev[key];
const nextSetting = next[key];
if (!prevSetting || !nextSetting) {
return next;
}
if (
prevSetting.volume !== nextSetting.volume ||
prevSetting.normalize !== nextSetting.normalize
) {
return next;
}
}
return prev;
});
},
[setDefaultSourceAudioTrackSettings],
);
const getSourceAudioTrackSettingsForClip = useCallback(
(clipId: string | null): SourceAudioTrackSettings => {
if (!clipId) {
return defaultSourceAudioTrackSettings;
}
return {
...defaultSourceAudioTrackSettings,
...(sourceAudioTrackSettingsByClip[clipId] ?? {}),
};
},
[defaultSourceAudioTrackSettings, sourceAudioTrackSettingsByClip],
);
const getSourceAudioTrackSettingsForClip = useCallback(
(clipId: string | null): SourceAudioTrackSettings => {
if (!clipId) {
return defaultSourceAudioTrackSettings;
}
return {
...defaultSourceAudioTrackSettings,
...(sourceAudioTrackSettingsByClip[clipId] ?? {}),
};
},
[defaultSourceAudioTrackSettings, sourceAudioTrackSettingsByClip],
);
const onSelectedClipSourceAudioTrackVolumeChange = useCallback(
(id: string, volume: number) => {
@@ -137,7 +142,7 @@ export function useSourceAudioTrackSettings({
};
});
},
[defaultSourceAudioTrackSettings, selectedClipId],
[defaultSourceAudioTrackSettings, selectedClipId, setSourceAudioTrackSettingsByClip],
);
const onSelectedClipSourceAudioTrackNormalizeChange = useCallback(
@@ -161,16 +166,16 @@ export function useSourceAudioTrackSettings({
};
});
},
[defaultSourceAudioTrackSettings, selectedClipId],
[defaultSourceAudioTrackSettings, selectedClipId, setSourceAudioTrackSettingsByClip],
);
return {
sourceAudioTrackMeta,
activeSourceAudioTrackSettings,
selectedClipSourceAudioTrackSettings,
getSourceAudioTrackSettingsForClip,
onSourceAudioTracksMetaChange,
onSelectedClipSourceAudioTrackVolumeChange,
onSelectedClipSourceAudioTrackNormalizeChange,
};
return {
sourceAudioTrackMeta,
activeSourceAudioTrackSettings,
selectedClipSourceAudioTrackSettings,
getSourceAudioTrackSettingsForClip,
onSourceAudioTracksMetaChange,
onSelectedClipSourceAudioTrackVolumeChange,
onSelectedClipSourceAudioTrackNormalizeChange,
};
}
@@ -44,6 +44,7 @@ function AudioWaveformComponent({
}
}, []);
// biome-ignore lint/correctness/useExhaustiveDependencies: resizeKey intentionally redraws the canvas after ResizeObserver notifications.
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
@@ -79,15 +80,15 @@ function AudioWaveformComponent({
const visibleStartMs = segmentStartMs ?? range.start;
const visibleEndMs = segmentEndMs ?? range.end;
const visibleDurationMs = visibleEndMs - visibleStartMs;
if (visibleDurationMs <= 0) return;
const midY = height / 2;
ctx.beginPath();
for (let px = 0; px < width; px++) {
const t = visibleStartMs + (px / width) * visibleDurationMs;
// If the timeline time is beyond the actual audio duration, we draw nothing (flat line)
if (t < 0 || t > durationMs) continue;
@@ -95,12 +96,12 @@ function AudioWaveformComponent({
const leftIndex = Math.floor(exactIndex);
const rightIndex = Math.min(peakData.length - 1, leftIndex + 1);
const mix = exactIndex - leftIndex;
let amplitude = peakData[leftIndex] * (1 - mix) + peakData[rightIndex] * mix;
if (normalize) amplitude = Math.sqrt(Math.max(0, amplitude));
amplitude = Math.max(0, Math.min(1, amplitude * gain));
const barHeight = amplitude * midY * 0.85;
ctx.moveTo(px, midY - barHeight);