Video ref only renders after refresh + function components cannot be given refs

Viewed 260

I have a NEXT JS app that uses the following VideoPlayer component:

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;

The myVideo ref is being retrieved by the useSocket hook which comes from the following Store declaration:

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 };

And finally Im rendering the VideoPlayer component in this CallPage:

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;

When I click on my home to take me to CallPage, by doing this:

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

The router takes me and everything is rendered but the VideoRef. If I refresh the page it then loads. Do you have any idea why this happens? The devtools throws an error that says that

function components cannot be given refs

even though Im using such ref in a DOM element as you can see. So I have already ruled out using forwardref. I would really appreciate any help. Im clueless here.

1 Answers

One idea...

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

Your hook has an empty dependency array. It's certainly possible that on first render the router.isReady returns false. You should pass router.isReady to the dependency array.

Additionally your CallPage function is conditionally running an effect. That effect:

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

should also pass isAdmin and whatever the VALUE of the setMeetInfoPopup setter is into the dependency array. Assuming the useState value for that setter is called meetInfoPopup, the hook should actually read like this:

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