Fix: Prevent camera remounting issues from parent re-renders
- Add React.memo to CameraSelfie to prevent unnecessary re-renders - Memoize callbacks in CameraSelfie - Track previous open/facingMode state in RobustCamera to detect actual changes - Add streamAttachedRef to prevent duplicate stream attachments - Fix useEffect dependencies to prevent cleanup on parent re-renders - Ensure camera only initializes on actual dialog open (not parent re-render)
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import React from 'react';
|
import React, { memo, useCallback } from 'react';
|
||||||
import { RobustCamera } from './RobustCamera';
|
import { RobustCamera } from './RobustCamera';
|
||||||
|
|
||||||
interface CameraSelfieProps {
|
interface CameraSelfieProps {
|
||||||
@@ -7,6 +7,21 @@ interface CameraSelfieProps {
|
|||||||
open: boolean;
|
open: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CameraSelfie: React.FC<CameraSelfieProps> = ({ onCapture, onClose, open }) => {
|
// Memoize to prevent re-renders when parent updates
|
||||||
return <RobustCamera onCapture={onCapture} onClose={onClose} open={open} />;
|
export const CameraSelfie: React.FC<CameraSelfieProps> = memo(({ onCapture, onClose, open }) => {
|
||||||
};
|
// Memoize callbacks to prevent unnecessary effect triggers in child
|
||||||
|
const handleCapture = useCallback((dataUrl: string) => {
|
||||||
|
onCapture(dataUrl);
|
||||||
|
}, [onCapture]);
|
||||||
|
|
||||||
|
const handleClose = useCallback(() => {
|
||||||
|
onClose();
|
||||||
|
}, [onClose]);
|
||||||
|
|
||||||
|
return <RobustCamera onCapture={handleCapture} onClose={handleClose} open={open} />;
|
||||||
|
}, (prevProps, nextProps) => {
|
||||||
|
// Custom comparison - only re-render if open state changes
|
||||||
|
return prevProps.open === nextProps.open &&
|
||||||
|
prevProps.onCapture === nextProps.onCapture &&
|
||||||
|
prevProps.onClose === nextProps.onClose;
|
||||||
|
});
|
||||||
|
|||||||
@@ -42,10 +42,16 @@ export const RobustCamera: React.FC<RobustCameraProps> = ({ onCapture, onClose,
|
|||||||
const isInitializingRef = useRef(false);
|
const isInitializingRef = useRef(false);
|
||||||
const isMountedRef = useRef(true);
|
const isMountedRef = useRef(true);
|
||||||
|
|
||||||
|
// Track attachment state
|
||||||
|
const streamAttachedRef = useRef(false);
|
||||||
|
|
||||||
// Cleanup function - stops all tracks and clears video
|
// Cleanup function - stops all tracks and clears video
|
||||||
const cleanupCamera = useCallback(() => {
|
const cleanupCamera = useCallback(() => {
|
||||||
console.log('[RobustCamera] Cleaning up camera');
|
console.log('[RobustCamera] Cleaning up camera');
|
||||||
|
|
||||||
|
// Reset attachment tracking
|
||||||
|
streamAttachedRef.current = false;
|
||||||
|
|
||||||
// Stop video playback
|
// Stop video playback
|
||||||
if (videoElementRef.current) {
|
if (videoElementRef.current) {
|
||||||
videoElementRef.current.pause();
|
videoElementRef.current.pause();
|
||||||
@@ -144,23 +150,26 @@ export const RobustCamera: React.FC<RobustCameraProps> = ({ onCapture, onClose,
|
|||||||
}, [facingMode, stream]);
|
}, [facingMode, stream]);
|
||||||
|
|
||||||
// SINGLE useEffect to handle stream attachment to video
|
// SINGLE useEffect to handle stream attachment to video
|
||||||
// This runs whenever stream changes or video element becomes available
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const video = videoElementRef.current;
|
const video = videoElementRef.current;
|
||||||
|
|
||||||
|
// Early exit conditions
|
||||||
if (!video || !stream) {
|
if (!video || !stream) {
|
||||||
console.log('[RobustCamera] Cannot attach - video:', !!video, 'stream:', !!stream);
|
console.log('[RobustCamera] Cannot attach - video:', !!video, 'stream:', !!stream);
|
||||||
|
streamAttachedRef.current = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (video.srcObject === stream) {
|
// Skip if already attached to this stream
|
||||||
|
if (video.srcObject === stream && streamAttachedRef.current) {
|
||||||
console.log('[RobustCamera] Stream already attached to video');
|
console.log('[RobustCamera] Stream already attached to video');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[RobustCamera] Attaching stream to video element');
|
console.log('[RobustCamera] Attaching stream to video element');
|
||||||
|
streamAttachedRef.current = true;
|
||||||
|
|
||||||
// Set up event handlers before attaching
|
// Set up event handlers
|
||||||
const handleLoadedMetadata = () => {
|
const handleLoadedMetadata = () => {
|
||||||
console.log('[RobustCamera] Video metadata loaded, playing...');
|
console.log('[RobustCamera] Video metadata loaded, playing...');
|
||||||
if (!isMountedRef.current) return;
|
if (!isMountedRef.current) return;
|
||||||
@@ -194,7 +203,7 @@ export const RobustCamera: React.FC<RobustCameraProps> = ({ onCapture, onClose,
|
|||||||
// Attach the stream
|
// Attach the stream
|
||||||
video.srcObject = stream;
|
video.srcObject = stream;
|
||||||
|
|
||||||
// Cleanup function
|
// Cleanup function - only remove listeners, don't detach stream
|
||||||
return () => {
|
return () => {
|
||||||
console.log('[RobustCamera] Cleaning up video event listeners');
|
console.log('[RobustCamera] Cleaning up video event listeners');
|
||||||
video.removeEventListener('loadedmetadata', handleLoadedMetadata);
|
video.removeEventListener('loadedmetadata', handleLoadedMetadata);
|
||||||
@@ -202,10 +211,14 @@ export const RobustCamera: React.FC<RobustCameraProps> = ({ onCapture, onClose,
|
|||||||
};
|
};
|
||||||
}, [stream]);
|
}, [stream]);
|
||||||
|
|
||||||
|
// Track previous open state to detect actual open/close changes
|
||||||
|
const prevOpenRef = useRef(open);
|
||||||
|
|
||||||
// Initialize camera when dialog opens
|
// Initialize camera when dialog opens
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) {
|
// Only initialize if dialog is opening (transition from false to true)
|
||||||
console.log('[RobustCamera] Dialog opened');
|
if (open && !prevOpenRef.current) {
|
||||||
|
console.log('[RobustCamera] Dialog opening');
|
||||||
isMountedRef.current = true;
|
isMountedRef.current = true;
|
||||||
|
|
||||||
// Small delay to ensure DOM is ready
|
// Small delay to ensure DOM is ready
|
||||||
@@ -216,27 +229,42 @@ export const RobustCamera: React.FC<RobustCameraProps> = ({ onCapture, onClose,
|
|||||||
return () => {
|
return () => {
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
};
|
};
|
||||||
} else {
|
}
|
||||||
// Dialog closed - cleanup
|
|
||||||
console.log('[RobustCamera] Dialog closed, cleaning up');
|
// Only cleanup if dialog is closing (transition from true to false)
|
||||||
|
if (!open && prevOpenRef.current) {
|
||||||
|
console.log('[RobustCamera] Dialog closing, cleaning up');
|
||||||
cleanupCamera();
|
cleanupCamera();
|
||||||
}
|
}
|
||||||
}, [open, initializeCamera, cleanupCamera]);
|
|
||||||
|
// Update ref
|
||||||
|
prevOpenRef.current = open;
|
||||||
|
}, [open]); // Remove other dependencies - only react to open prop changes
|
||||||
|
|
||||||
|
// Track previous facing mode to detect actual changes
|
||||||
|
const prevFacingModeRef = useRef(facingMode);
|
||||||
|
|
||||||
// Handle facing mode changes
|
// Handle facing mode changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open || !stream) return;
|
// Only reinitialize if facing mode actually changed AND we have an active stream
|
||||||
|
if (facingMode !== prevFacingModeRef.current && open && stream) {
|
||||||
|
console.log('[RobustCamera] Facing mode changed from', prevFacingModeRef.current, 'to', facingMode);
|
||||||
|
|
||||||
|
cleanupCamera();
|
||||||
|
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
isInitializingRef.current = false;
|
||||||
|
initializeCamera();
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
prevFacingModeRef.current = facingMode;
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
|
||||||
console.log('[RobustCamera] Facing mode changed, reinitializing');
|
// Update ref if it hasn't been set yet
|
||||||
cleanupCamera();
|
prevFacingModeRef.current = facingMode;
|
||||||
|
}, [facingMode, open, stream]); // Dependencies are fine - we check for actual changes inside
|
||||||
const timer = setTimeout(() => {
|
|
||||||
isInitializingRef.current = false;
|
|
||||||
initializeCamera();
|
|
||||||
}, 300);
|
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}, [facingMode, open]); // Only re-run when facingMode actually changes
|
|
||||||
|
|
||||||
// Cleanup on unmount
|
// Cleanup on unmount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user