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

React组件中MediaRecorder初始化后引用仍未定义的问题

问题

我正在开发一款React应用,用于录制用户的音视频并上传至后端。在下方的MediaRecorderComponent组件中,initializeDevice函数会请求用户授权音视频录制权限(通过按钮触发,以保证UI清晰明确),授权成功后会将mediaRecorder变量初始化为新的MediaRecorder对象,参数为navigator.mediaDevices.getUserMedia Promise的返回值。

import React, { useEffect, useState } from 'react';
import { RecordAudioButton } from '../Components';

type Props = {
    mediaConstraints: MediaStreamConstraints;
};

function MediaRecorderComponent({ mediaConstraints }: Props) {
    const [permissionGranted, setPermissionGranted] = useState(false);
    const [stream, setStream] = useState<null | MediaStream>(null);
    const [isRecording, setIsRecording] = useState(false);

    let mediaRecorder: MediaRecorder;

    const initializeDevice = async () => {
        try {
            const response = await navigator.mediaDevices.getUserMedia(
                mediaConstraints
            );
            setStream(response);
            setPermissionGranted(true);
            mediaRecorder = new MediaRecorder(response);
        } catch (err) {
            setPermissionGranted(false);
            console.log(err);
        }
    };

    const handleRecording = () => {
        if (isRecording) {
            mediaRecorder.stop();
            setIsRecording(false);
        }

        mediaRecorder.start();
        setIsRecording(true);
    };

    return (
        <div>
            {permissionGranted ? (
                <RecordAudioButton
                    isRecording={isRecording}
                    onClick={handleRecording}
                />
            ) : (
                <>
                    <p>
                        CLIENT INSTRUCTIONS
                    </p>
                    <button
                        type="button"
                        onClick={() => initializeDevice()}
                    >
                        Let's start!
                    </button>
                </>
            )}
        </div>
    );
}

随后,handleRecording函数作为props传入RecordAudioButton组件,点击该组件的div时会调用此函数:

function RecordAudioButton({ isRecording, onClick }: Props) {
    return (
        <div
            className="bg-error text-base-100 rounded-full p-5 shadow-lg"
            onClick={() => onClick()}
        >
        ///... and so on

但点击按钮时,触发Uncaught TypeError: Cannot read properties of undefined (reading 'start')错误,此时mediaRecorder变量显示为undefined,尽管它已在用户授权时的initializeDevice函数中完成初始化。请问为何该引用会未初始化?

原因分析

你用let mediaRecorder: MediaRecorder;声明的是组件内部的普通变量,而React组件每次状态更新(比如调用setPermissionGranted(true))都会触发重渲染,重渲染时组件函数会重新执行一遍,这个普通变量会被重新声明,之前在initializeDevice里赋值的MediaRecorder实例就丢失了,所以点击按钮时mediaRecorder变成了undefined,调用start()自然会报错。

解决方案

把普通变量换成useRef来保存MediaRecorder实例,因为useRef的.current属性在组件整个生命周期内都不会被重置,能持续保存引用:

import React, { useEffect, useState, useRef } from 'react';
import { RecordAudioButton } from '../Components';

type Props = {
    mediaConstraints: MediaStreamConstraints;
};

function MediaRecorderComponent({ mediaConstraints }: Props) {
    const [permissionGranted, setPermissionGranted] = useState(false);
    const [stream, setStream] = useState<null | MediaStream>(null);
    const [isRecording, setIsRecording] = useState(false);

    // 用useRef保存MediaRecorder实例
    const mediaRecorderRef = useRef<MediaRecorder | null>(null);

    const initializeDevice = async () => {
        try {
            const response = await navigator.mediaDevices.getUserMedia(
                mediaConstraints
            );
            setStream(response);
            setPermissionGranted(true);
            // 赋值给ref的current属性
            mediaRecorderRef.current = new MediaRecorder(response);
        } catch (err) {
            setPermissionGranted(false);
            console.log(err);
        }
    };

    const handleRecording = () => {
        // 先判断ref.current是否存在,避免报错
        if (!mediaRecorderRef.current) return;
        
        if (isRecording) {
            mediaRecorderRef.current.stop();
            setIsRecording(false);
        } else {
            mediaRecorderRef.current.start();
            setIsRecording(true);
        }
    };

    return (
        <div>
            {permissionGranted ? (
                <RecordAudioButton
                    isRecording={isRecording}
                    onClick={handleRecording}
                />
            ) : (
                <>
                    <p>
                        CLIENT INSTRUCTIONS
                    </p>
                    <button
                        type="button"
                        onClick={() => initializeDevice()}
                    >
                        Let's start!
                    </button>
                </>
            )}
        </div>
    );
}

另外补充了两个细节:

  1. 在handleRecording里增加了!mediaRecorderRef.current的判断,防止极端情况下(比如权限刚获取但ref还未完成赋值)出现错误
  2. 修正了原代码的逻辑漏洞——之前不管是否在录制都会执行start(),会导致停止录制后立刻重新开始,现在通过else分支区分了录制/停止的逻辑

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.14 04:05:28