视频 ref 只在刷新后渲染 + 功能组件不能给 refs

Posted

技术标签:

【中文标题】视频 ref 只在刷新后渲染 + 功能组件不能给 refs【英文标题】:Video ref only renders after refresh + function components cannot be given refs 【发布时间】:2021-10-14 23:58:02 【问题描述】:

我有一个使用以下VideoPlayer 的 NEXT JS 应用程序 组件:

import React,  useCallback, useEffect, useState, useContext  from "react";    
import  useSocket, useSocketUpdate  from "../Store";
import  useRouter  from "next/router";

import styles from "../styles/CallPage.module.css";

const VideoPlayer = (props: any) => 
  const 
    isAdmin,
    myVideo,
    userVideo
  : any = useSocket();
  const router = useRouter();
  useEffect(() => 
    if (router.isReady && myVideo.current && userVideo.current) 
      myVideo?.current.load();
      userVideo?.current.load();
    
  , []);

  return (
    <div className=styles.grid_container>
      stream && (
        <div>
          <video
            className=styles.video
            playsInline
            muted
            ref=myVideo
            autoPlay
          />
          <div className=styles.overlay>
            <h2 className=styles.overlay_h2>OVERLAY</h2>
          </div>
        </div>
      )
      callAccepted && !callEnded ? (
        <div>
          <video
            className=styles.video
            playsInline
            ref=userVideo
            autoPlay
          />
          <div className=styles.overlay>
            <h2 className=styles.overlay_h2>OVERLAY2</h2>
          </div>
        </div>
      ) : (
        isGuest &&
        !callAccepted && (
          <div className=styles.grid_container>
            <div className=styles.no_match_content>
              <h2 className=styles.h2_no_match>
                Please wait until the host of this room let you in.
              </h2>
              <div className=styles.btn_no_match></div>
            </div>
          </div>
        )
      )
    </div>
  );
;

export default VideoPlayer;

来自以下Store 声明的 useSocket 挂钩正在检索 myVideo 引用:

import React, 
  createContext,
  useState,
  useRef,
  useEffect,
  useContext
 from "react";
import  io  from "socket.io-client";
import Peer from "simple-peer";

import  useRouter  from "next/router";
  
const SocketContext = createContext();
const SocketUpdateContext = createContext();

const socket = io(`$process.env.NEXT_PUBLIC_SERVER_URL`);
   
function useSocket() 
  const useSocket = useContext(SocketContext);
  if (useSocket === undefined) 
    throw new Error("useSocket must be used within a SocketProvider");
  
  return useSocket;


function useSocketUpdate() 
  const useSocketUpdate = useContext(SocketUpdateContext);
  if (useSocketUpdate === undefined) 
    throw new Error("useSocketUpdate must be used within a SocketProvider");
  
  return useSocketUpdate;


const ContextProvider = ( children : any) => 
  const router = useRouter();
  const [callAccepted, setCallAccepted] = useState(false);
  const [callEnded, setCallEnded] = useState(false);
  const [stream, setStream] = useState<MediaStream>();
  const [name, setName] = useState("");
  const [call, setCall] = useState<any>();
  const [me, setMe] = useState("");
  const [isAdmin, setAdmin] = useState(false);
  const [isGuest, setGuest] = useState(false);
  const [idToCall, setIdToCall] = useState("");

  const myVideo = useRef<htmlVideoElement>(null!);
  const userVideo = useRef<HTMLVideoElement>(null!);
  const connectionRef = useRef<Peer.Instance>(null!);

  useEffect(() => 
    navigator.mediaDevices
      .getUserMedia( video: true, audio: true )
      .then((currentStream) => 
        setStream(currentStream);
        if (myVideo.current) 
          myVideo.current.srcObject = currentStream;
        
      );

    socket.on("me", (id) => 
      setMe(id);
    );

    socket.on("callUser", ( from, name: callerName, signal ) => 
      setCall( isReceivingCall: true, from, name: callerName, signal );
    );
  , []);

  const answerCall = () => 
    setCallAccepted(true);

    const peer = new Peer( initiator: false, trickle: false, stream );

    peer.on("signal", (data) => 
      socket.emit("answerCall",  signal: data, to: call.from );
    );

    peer.on("stream", (currentStream) => 
      if (userVideo.current) userVideo.current.srcObject = currentStream;
    );

    peer.signal(call.signal);

    connectionRef.current = peer;
  ;

  const callUser = (id: String) => 
    // setAdmin(false);
    const peer = new Peer( initiator: true, trickle: false, stream );

    peer.on("signal", async (data) => 
      socket.emit("callUser", 
        userToCall: id,
        signalData: data,
        from: me,
        name
      );
    );

    peer.on("stream", (currentStream) => 
      if (userVideo.current) userVideo.current.srcObject = currentStream;
    );

    socket.on("callAccepted", (signal) => 
      setCallAccepted(true);

      peer.signal(signal);
    );
    if (connectionRef.current) connectionRef.current = peer;
  ;

  const leaveCall = () => 
    setCallEnded(true);
    setAdmin(false);
    if (connectionRef.current) connectionRef.current.destroy();
    // if (typeof window !== "undefined") window.location.reload();
    router.push("/home");
  ;

  return (
    <SocketContext.Provider
      value=
        call,
        callAccepted,
        myVideo,
        userVideo,
        stream,
        name,
        callEnded,
        me,
        isAdmin,
        router,
        isGuest,
        user,
        isAuthenticated,
        isUnauthenticated,
        isAuthenticating,
        authError,
        idToCall
      
    >
      <SocketUpdateContext.Provider
        value=
          callUser,
          leaveCall,
          answerCall,
          setName,
          setAdmin,
          setGuest,
          logout,
          authenticate,
          signup,
          login,
          setIdToCall
        
      >
        children
      </SocketUpdateContext.Provider>
    </SocketContext.Provider>
  );
;

export  ContextProvider, SocketContext, useSocket, useSocketUpdate ;

最后我在 CallPage 中渲染 VideoPlayer 组件:

import  useEffect, useReducer, useState, useContext  from "react";
import styles from "../styles/CallPage.module.css";
import Peer from "simple-peer";
import VideoPlayer from "../components/VideoPlayer";

import Page from "../components/UI/Page";
import Link from "next/link";
import Head from "next/head";    
import  useSocket, useSocketUpdate  from "../Store";
const initialState = [];

const CallPage = () => 
  const  isAdmin, myVideo, isAuthenticated, isUnauthenticated, user : any =
    useSocket();
  if (isUnauthenticated) 
    return (
      <Page>
        <div className=styles.no_match_content>
          <h2 className=styles.h2_no_match>
            Please login first to access a call.
          </h2>
          <div className=styles.btn_no_match>
            <Link passHref href="/login">
              Login
            </Link>
          </div>
        </div>
      </Page>
    );
  
  let alertTimeout = null;

  const [messageList, messageListReducer] = useReducer(
    MessageListReducer,
    initialState
  );    
  useEffect(() => 
    if (isAdmin) 
      setMeetInfoPopup(true);
    
  , []);   
  return (
    <div className=styles.page_container>
      <VideoPlayer className=styles.video_container />
      )
    </div>
  );
;
export default CallPage;

当我点击我的家将我带到 CallPage 时,这样做:

<Link
                passHref
                href="/callpage"
              >
                <Button
                  loadingText="Loading"
                  colorScheme="teal"
                  variant="outline"
                  spinnerPlacement="start"
                  className=styles.btn
                  onClick=handleAdmin
                >
                  New Meeting
                </Button>
              </Link>

路由器带我去渲染除了 VideoRef 之外的所有东西。如果我刷新页面,它就会加载。你知道为什么会这样吗? devtools 抛出一个错误,表明

函数组件不能给定引用

尽管我在 DOM 元素中使用了这样的 ref,如您所见。所以我已经排除了使用forwardref。我真的很感激任何帮助。我在这里一无所知。

【问题讨论】:

函数组件不接受 React refs,您需要转发任何传递的 React refs。哪些组件将 ref 传递给它们,它是哪个 ref? 【参考方案1】:

一个想法...

useEffect(() =>  
 if (router.isReady && myVideo.current && userVideo.current) 
    myVideo?.current.load();
    userVideo?.current.load();
  
, []);

你的钩子有一个空的依赖数组。在第一次渲染时,router.isReady 肯定有可能返回 false。你应该将router.isReady 传递给依赖数组。

此外,您的 CallPage 函数有条件地运行效果。那个效果:

useEffect(() => 
  if (isAdmin) 
    setMeetInfoPopup(true);
  
, []); 

还应该将isAdminsetMeetInfoPopup 设置器的值传递到依赖数组中。假设该 setter 的 useState 值称为 meetInfoPopup,则挂钩实际上应该是这样的:

useEffect(() => 
  if (isAdmin && !meetInfoPopup) 
    setMeetInfoPopup(true);
  
, [isAdmin, meetInfoPopup]); 

【讨论】:

以上是关于视频 ref 只在刷新后渲染 + 功能组件不能给 refs的主要内容,如果未能解决你的问题,请参考以下文章

ref属性与props配置项

vue同步组件和异步组件的区别

渲染组件后如何关注特定的输入文本?

ReactJs:为啥 ref.current 在渲染组件时返回 null?

vue关于this.$refs.tabs.refreshs()刷新组件,缓存

如何在不渲染的情况下从 forwardref 组件添加 ref?