You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ReactJs useRef current返回null 组件卸载调用stopCamera报错如何解决

问题根因

  • 组件卸载执行清理逻辑时,<video> DOM 元素已经被React从页面移除,videoElement.current 变为null,原有stopCamera直接访问videoElement.current.srcObject会触发空引用报错。
  • navigator.mediaDevices.getUserMedia是异步接口,可能存在组件已经卸载,但摄像头授权/流返回的回调才执行的情况,此时访问已销毁的ref对象也会引发异常,还可能导致摄像头、手电筒资源泄漏无法正常关闭。
  • 媒体流仅绑定在video元素的srcObject属性上,DOM销毁后就无法获取流实例,无法保证所有媒体轨道被正确停止。

修复方案

修改CameraScreen.js代码:

import "./styles.css";
import { useState, useEffect, useRef } from "react";

export default function CameraScreen() {
  const videoElement = useRef(null);
  // 新增:单独存储媒体流实例,不依赖DOM元素
  const mediaStreamRef = useRef(null);
  // 新增:标记组件是否处于挂载状态
  const isMountedRef = useRef(true);
  const [facingMode, setFacingMode] = useState("environment");

  const handleFacingModeToggle = () => {
    stopCamera();
    setFacingMode(prev => prev === "environment" ? "user" : "environment");
  };

  useEffect(() => {
    startCamera();
    return function cleanup() {
      isMountedRef.current = false;
      stopCamera();
    };
  }, [facingMode]); // 注意:facingMode变更需要重新启动相机,所以要加入依赖数组

  const stopCamera = () => {
    // 先停止所有媒体轨道
    if (mediaStreamRef.current) {
      mediaStreamRef.current.getTracks().forEach(t => t.stop());
      mediaStreamRef.current = null;
    }
    // 再清理video元素的srcObject,增加判空避免报错
    if (videoElement.current) {
      videoElement.current.srcObject = null;
    }
  };

  function startCamera() {
    if (!navigator.mediaDevices?.getUserMedia) return;

    navigator.mediaDevices
      .getUserMedia({
        video: { facingMode: facingMode },
        width: { ideal: 1280 },
        height: { ideal: 720 }
      })
      .then(function (stream) {
        // 组件已卸载的情况下直接停止流,不执行后续逻辑
        if (!isMountedRef.current) {
          stream.getTracks().forEach(t => t.stop());
          return;
        }
        // 存储流实例
        mediaStreamRef.current = stream;
        if (videoElement.current) {
          videoElement.current.srcObject = stream;
        }
        const track = stream.getVideoTracks()[0];
        const imageCapture = new ImageCapture(track);
        return imageCapture.getPhotoCapabilities();
      })
      .then(() => {
        if (!isMountedRef.current || !mediaStreamRef.current) return;
        const track = mediaStreamRef.current.getVideoTracks()[0];
        track.applyConstraints({
          advanced: [{ torch: true }]
        });
      })
      .catch(function (error) {
        if (!isMountedRef.current) return;
        alert("请检查设备权限");
        console.log("调用摄像头出错:", error);
      });

    if (videoElement.current) {
      videoElement.current.onloadeddata = function () {
        if (window.NativeDevice) {
          window.NativeDevice.htmlCameraReadyToRecord(true);
        }
      };
    }
  }

  return (
    <>
      <video
        autoPlay
        ref={videoElement}
        style={{
          minHeight: "67.82vh",
          maxHeight: "67.82vh",
          maxWidth: "100%",
          minWidth: "100%"
        }}
        className="border-3rem bg-[#666]"
      ></video>
      <button onClick={stopCamera}>停止摄像头</button>
    </>
  );
}

核心修改说明:

  1. 新增mediaStreamRef单独存储媒体流实例,停止摄像头时优先从该ref获取流,不依赖DOM元素的存在,保证资源可以正常释放。
  2. 新增isMountedRef标记组件挂载状态,所有异步回调执行前先判断组件是否已卸载,避免访问已销毁的对象。
  3. 优化stopCamera逻辑,增加多层判空防护,避免空引用报错。
  4. 修正useEffect依赖数组,加入facingMode保证切换前后摄像头时可以正确重启相机。

内容的提问来源于stack exchange,提问作者TINTIN

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.24 04:36:02