0

I'm building a video calling feature into my app and the user's media stream is stored in a React state. However, when I try to access this state in another file (I'm using React Context for this), the stream state hasn't been updated yet and uses the default state value. I learnt, React updates state on the next render rather than instantly but I'm trying to access the stream instantly. However, I'm creating the Peer object inside a function so if I try to delay the stream access for example with a useEffect hook, I then can't use the function outside the hook. Are there any ways I could to this? Some code samples below:

Within context file: const [stream, setStream] = useState(null) Where I'm setting the stream:

useEffect(() => {
        navigator.mediaDevices.getUserMedia({ video: true, audio: true }).then((currentStream) => {
            setStream(currentStream);
        }).catch((error) => {
            console.log('Error accessing media devices: ', error);
        });
    }, []);

One of my functions (stream logs null):

function callUser(id) {
    console.log(stream)
    const peer = new Peer({ initiator: true, trickle: false, stream })

    peer.on('signal', (data) => {
      socket.emit('call-user', { userToCall: id, signalData: data, from: myID, name })
    })

    peer.on('close', () => {
      socket.off('call-answered')
      setCallAccepted(false)
      setCall(null)
      addCall(false)
      setInfoBarShow(false)
    })

    peer.on('stream', (currentStream) => {
      userVideo.current.srcObject = currentStream
    })

    socket.on('call-answered', (signal) => {
      setCallAccepted(true)

      peer.signal(signal)
    })

    socket.on('call-ended', (endReason) => {
      if (statusRef.current) {
        if (endReason === "declined" || endReason === "ended") statusRef.current.innerHTML = `Call ${endReason}`
      }
      setTimeout(() => {
        leaveCall()
      }, 5000)
    })

    connectionRef.current = peer
  }

I've tried lots of things such as wrapping the functions in a useEffect hook with the dependency as stream but then the functions aren't globally available. I've tried using selection but that won't work as the component still isn't being re-rendered/refreshed to include the updated state. Nothing works. For handling the WebRTC side of things I'm using the Simple-Peer package

To clarify: A button calls the function callUser. I'm accessing the stream state inside this function where I pass it into the Peer object for Simple-Peer. That stream value currently is using the default state of null that I set, not a MediaStream as it hasn't updated yet and that's my issue

FaderR
  • 17
  • 1
  • 7
  • The main issue I'm facing is that I understand I can use the useEffect hook to fix this but then the callUser function wouldn't be usable/globally available outside the hook. I'm not sure how to make sure that the stream state is being kept up to date while also keeping my functions usable – FaderR Aug 29 '23 at 12:36
  • 1
    Does this answer your question? [Why does calling react setState method not mutate the state immediately?](https://stackoverflow.com/questions/30782948/why-does-calling-react-setstate-method-not-mutate-the-state-immediately) – thedude Aug 29 '23 at 12:36
  • *"I learnt, React updates state on the next render rather than instantly but I'm trying to access the stream instantly."* - Where are you trying to do that? What code even invokes `callUser()`? If you already learned that state updates are not immediate, why are you relying on them being immediate? – David Aug 29 '23 at 12:39
  • @thedude something like that would fix my issue BUT the only problem is that surely my functions (the callUser one, relies on the stream state) wouldn't be usable outside of this. I tried using a useEffect hook with stream as the dependency and that was the case, not globally usable – FaderR Aug 29 '23 at 12:39
  • @David yeah I should've been more clear with that in my question. A button calls the callUser function. I'm trying to access the stream instantly inside the callUser function where I pass it into the Peer object but it uses the default state value null that I set as it hasn't updated yet – FaderR Aug 29 '23 at 12:42
  • @FaderR: And that button is being clicked *before* the `useEffect` shown in the question has completed and the component has re-rendered with the new state? How long does this `getUserMedia` operation take? If the user is clicking too quickly, maybe you can simply hide/de-activate that button until the operation has completed? It's not really clear to me why this *needs* to be "immediate". Perhaps a more complete example can demonstrate? – David Aug 29 '23 at 12:45
  • @David The user clicking the button also gets the user's media so these are happening at the same time. I could probably test implementing a delay between these two. I'll also test AKASH's answer too – FaderR Aug 29 '23 at 12:50
  • @FaderR: *"The user clicking the button also gets the user's media"* - According to the code shown, the `getUserMedia` operation happens when a component loads, not when a button is clicked. If clicking that button is what causes this component to load (maybe by dynamically showing it in some parent component) and then **immediately** tries to use something from this component before anything has even rendered/loaded then that sounds like a variety of misunderstandings of asynchronous operations. But it increasingly sounds like all we can do is guess without a [mcve] demonstrating. – David Aug 29 '23 at 12:54
  • @David yeah, my implementation of doing this involves a lot of extra code so the minimal reproducible example would've probably been quite long. I understand that because of that the question isn't the easiest to understand so thanks for your responses already. I'll try out AKASH's answer as it seems that would work – FaderR Aug 29 '23 at 12:59

1 Answers1

1

It seems like you are dealing with the asynchronous nature of state updates in React. The issue you're facing is that the stream state is not immediately available when you are trying to use it in the callUser function. This is because the state update doesn't happen instantly, and your function is using the initial state value.

Use Context to Manage State:

import { createContext, useContext, useState } from 'react';

const StreamContext = createContext();

export const StreamProvider = ({ children }) => {
  const [stream, setStream] = useState(null);

  const setMediaStream = (newStream) => {
    setStream(newStream);
  };

  return (
    <StreamContext.Provider value={{ stream, setMediaStream }}>
      {children}
    </StreamContext.Provider>
  );
};

export const useStream = () => {
  return useContext(StreamContext);
};

Wrap App with Context Provider:

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { StreamProvider } from './path of contextfile';

ReactDOM.render(
  <StreamProvider>
    <App />
  </StreamProvider>,
  document.getElementById('root')
);

Access Stream in Function:

import React from 'react';
import { useStream } from './path of contextfile';
import Peer from 'simple-peer';

function CallUserComponent() {
  const { stream } = useStream();

  const callUser = (id) => {
    // use the latest stream value
    console.log(stream);

    // your code
  };

  return (
    <button onClick={() => callUser('someUserId')}>Call User</button>
  );
}

export default CallUserComponent;
AKASH
  • 95
  • 3
  • Thanks for this. I'll test it later as it seems like it would work perfectly. Just to make sure I fully understand, with `setMediaStream`, I would be using this in the section of my code where I'd be getting this MediaStream and I'd be getting the setMediaStream by importing the context file, correct? – FaderR Aug 29 '23 at 12:47
  • 1
    Yes, that's correct. The setMediaStream function in the context acts as a wrapper around the setStream function. You'd use the setMediaStream function where you are currently using setStream. Specifically, in the useEffect where you are getting the user's media stream. Replace setStream(currentStream) with setMediaStream(currentStream). To access the setMediaStream function, you'd use the useStream hook that you import from your context file. – AKASH Aug 29 '23 at 12:52
  • I tested this and I have still have the same issue of stream not using the latest value. To give some more context, the code where I get the MediaStream is dynamically loaded based on whether the button has been clicked so the media stream is only obtained when the user needs it used (i.e. in a call). To try and fix this I used a delay of 1000ms with `setTimeout` so the callUser function is called 1000ms after loading starts. addCall(true) //shows content setTimeout(() => { callUser(userID) }, 1000) – FaderR Aug 29 '23 at 14:52
  • The code works fine and stream uses the latest value if I get the MediaStream as soon as the page loads and not load it in conditionally. However, I only want to ask the user for their MediaStream when it is necessary (i.e. when the call button is pressed) but then the latest stream value isn't used. Here's the basis of the code I used (I modified it a bit for my use case but it's similar: https://www.youtube.com/watch?v=oxFr7we3LC8) – FaderR Aug 29 '23 at 14:58