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> ); }
另外补充了两个细节:
- 在
handleRecording里增加了!mediaRecorderRef.current的判断,防止极端情况下(比如权限刚获取但ref还未完成赋值)出现错误 - 修正了原代码的逻辑漏洞——之前不管是否在录制都会执行
start(),会导致停止录制后立刻重新开始,现在通过else分支区分了录制/停止的逻辑
内容的提问来源于stack exchange,提问作者LeperAffinity666

