<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Khadijah</title>
    <description>The latest articles on DEV Community by Khadijah (@kgbadao).</description>
    <link>https://dev.to/kgbadao</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4125781%2Ffbf945d6-8c15-4fe4-917b-caf71f8dc1ca.png</url>
      <title>DEV Community: Khadijah</title>
      <link>https://dev.to/kgbadao</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kgbadao"/>
    <language>en</language>
    <item>
      <title>Demystifying WebRTC: A Step By Step Guide to Peer-to-Peer Video in React</title>
      <dc:creator>Khadijah</dc:creator>
      <pubDate>Tue, 15 Sep 2026 07:57:34 +0000</pubDate>
      <link>https://dev.to/kgbadao/demystifying-webrtc-a-step-by-step-guide-to-peer-to-peer-video-in-react-4k3b</link>
      <guid>https://dev.to/kgbadao/demystifying-webrtc-a-step-by-step-guide-to-peer-to-peer-video-in-react-4k3b</guid>
      <description>&lt;p&gt;Introduction&lt;br&gt;
When I decided to build a telemedicine app for my capstone project, I knew it had to have video call as one of its features, the question was how I was to implement it.&lt;br&gt;
I had first thought of using third-party apps like Agora and Twillio, but I was a uni student with a tight budget and big dreams. I thought to myself if my app suddenly takes off, then I'd need money to rely on third-party apps.  Also, I had a really big ego and convinced myself it couldn’t be that hard. &lt;br&gt;
There began my journey of looking up WebRTC tutorials, and I was even more confused than I first began. I spent days trying to figure out how browsers actually “handshake” using servers and how to keep the connection from dropping. &lt;br&gt;
That is why I decided to write this guide. To help beginners understand WebRTC and provide a straight-to-the-point tutorial on how to hook up React frontend to a Node.js signalling server so two users can actually see and hear each other.&lt;br&gt;
Prerequisites and tech stack&lt;br&gt;
Before I drop the full component code, there are a couple of things you need to know about how React handles webRTC.&lt;br&gt;
If you try to manage your peer connection or video streams using standard React (useState), you are going to have a really hard time. WebRTC changes state constantly if your component rerenders every time and the ICE candidate drops or a stream updates. Your video feed will either stutter, freeze or completely crash.&lt;br&gt;
This is why we lean heavily on two specific hooks:&lt;br&gt;
const localVideoRef = useRef(null);&lt;br&gt;
const remoteVideoRef = useRef(null);&lt;br&gt;
const peerRef = useRef(null);&lt;br&gt;
const callRef = useRef(null);&lt;br&gt;
// Assigning the PeerJS instance directly to ref&lt;br&gt;
peerRef.current = peer;&lt;br&gt;
// Assigning active call object to ref without causing UI re-renders&lt;br&gt;
callRef.current = call;&lt;br&gt;
useRef:  this is the secret sauce that lets us hold onto the RTCpeer connection instance and the socket connection across renders without triggering a UI refresh. It keeps our connection stable while React does it’s thing.&lt;br&gt;
// Runs once peer library is ready to handle setup&lt;br&gt;
useEffect(() =&amp;gt; {&lt;br&gt;
  if (!peerLoaded) return;&lt;br&gt;
  let cleanup;&lt;br&gt;
  const init = async () =&amp;gt; {&lt;br&gt;
    try {&lt;br&gt;
      setError('');&lt;br&gt;
      // Request camera and microphone access&lt;br&gt;
      const stream = await navigator.mediaDevices.getUserMedia({&lt;br&gt;
        video: { width: { ideal: 1280 }, height: { ideal: 720 } },&lt;br&gt;
        audio: true,&lt;br&gt;
      });&lt;br&gt;
      setLocalStream(stream);&lt;br&gt;
      if (localVideoRef.current) localVideoRef.current.srcObject = stream;&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  // ... Initialize PeerJS instance and event listeners
} catch (err) {
  setError(`Camera/microphone access error: ${err.message}`);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;};&lt;br&gt;
  init();&lt;br&gt;
  return () =&amp;gt; cleanup?.();&lt;br&gt;
}, [peerLoaded]);&lt;br&gt;
useEffect: we use this to run our initialisation logic  when the user page loads. This is also where the user is asked for camera permission as well as microphone permission. &lt;br&gt;
The mental model (how it works under the hood)&lt;br&gt;
Before we look at the code and inspect it, we need to talk about how webRTC actually connects two people. &lt;br&gt;
If you are someone that does web development, you know the client-server model: your browser talks to a server, and the server talks back. WebRTC throws that out the window. It wants Browser A to talk directly to Browser B without a middleman.&lt;br&gt;
This may sound simple but think of how the internet works.  Browsers sit behind NAT and firewalls, so they don't know each other's real public IP addresses, also firewalls block unsolicited incoming connections by default.&lt;br&gt;
To break through this and start a video call, your app has to handle three core phases. Think of it like organising a blind date:&lt;br&gt;
Media stream (the outfits)&lt;br&gt;
Before going on a date, you need to think of an outfit. In webRTC terms this means capturing your local data. We use the browser's getUserMedia API to turn on the camera and microphone. This creates a MediaStream. We need this stream ready to go so we can hand it over to the connection once it's established.&lt;br&gt;
The signalling server (the matchmaker)&lt;br&gt;
Since our browsers cannot find each other on their own, they need something called the Signaling server. This acts like a temporary matchmaker.&lt;br&gt;
Basically what happens is that Browser A says, "Hey, here are the video formats I support and my connection settings (Offer)."&lt;br&gt;
The signalling server passes that to Browser B, which replies, "Cool, here are my settings (Answer)."&lt;br&gt;
The peer connection (the handshake)&lt;br&gt;
We're using PeerJS, a library that wraps the raw WebRTC APIs,  it handles the offer/answer/ICE exchange for you so you don't have to write that signaling logic by hand.&lt;br&gt;
Now we are thinking, how do the browsers actually find a physical path to each other through all the internet routers and firewalls? They use what we call ICE Candidates and STUN servers. &lt;br&gt;
A STUN server is just a public server that looks at your browser and says, "Hey, here is what your public IP address actually looks like to the outside world."&lt;br&gt;
The browsers collect these public routing pathways (ICE candidates), swap them through the matchmaker, find the shortest path to each other, and lock in the direct peer connection.&lt;br&gt;
Now, data flows straight between the users, bypassing any servers completely.&lt;br&gt;
The step-by-step implementation &lt;br&gt;
Now for the part we have been waiting for, the actual execution. We are going to look at how this component handles media streams, automates the connection handshake, and handles incoming calls.&lt;br&gt;
Step 1: grabbing the user’s microphone and camera&lt;br&gt;
Before we can connect to anyone, we need our own audio and video feeds. We do this inside a useEffect hook using the browser's native navigator.mediaDevices.getUserMedia API.&lt;br&gt;
const stream = await navigator.mediaDevices.getUserMedia({&lt;br&gt;
  video: { width: { ideal: 1280 }, height: { ideal: 720 } },&lt;br&gt;
  audio: true,&lt;br&gt;
});&lt;br&gt;
setLocalStream(stream);&lt;br&gt;
if (localVideoRef.current) localVideoRef.current.srcObject = stream;&lt;br&gt;
What is happening here:&lt;br&gt;
We ask the browser for permission to use the webcam (targeting a 720p resolution) and the microphone. Once the user allows it, the browser gives us a MediaStream object. We save this stream to our React state so we can attach it to our local  element via a useRef hook.&lt;br&gt;
Step 2: The "Smart Room" Strategy (Automating the Handshake)&lt;br&gt;
Usually, WebRTC tutorials make you copy and paste long, random string IDs across screens just to connect two users. For a real app, it is  awful for user experience.&lt;br&gt;
Instead, we can use the appointmentId directly from the URL path (/video-call/:appointmentId) along with the user's role from localStorage to generate matching IDs automatically.&lt;br&gt;
const role = user?.role === 'doctor' ? 'doctor' : 'patient';&lt;br&gt;
const myPeerId = appointmentId ? &lt;code&gt;${role}-${appointmentId}&lt;/code&gt; : null;&lt;br&gt;
const theirPeerId = appointmentId ? &lt;code&gt;${role === 'doctor' ? 'patient' : 'doctor'}-${appointmentId}&lt;/code&gt; : null;&lt;br&gt;
Why does it matter?&lt;br&gt;
If the appointment ID in the URL is abc123, the doctor's browser registers itself with PeerJS as doctor-abc123 and expects to talk to patient-abc123. The patient does the exact opposite. They both know who to look for without exchanging a single message manually.&lt;br&gt;
Step 3: Initialising the Peer and Setting up the Listeners&lt;br&gt;
Once PeerJS loads from the CDN, we initialise our peer instance using our smart room IDs and standard Google STUN servers (which help the browsers find each other's public IP addresses).&lt;br&gt;
const peer = myPeerId&lt;br&gt;
  ? new window.Peer(myPeerId, peerConfig)&lt;br&gt;
  : new window.Peer(peerConfig);&lt;br&gt;
peerRef.current = peer;&lt;br&gt;
Immediately after creating the peer instance, we set up an event listener to handle incoming calls (peer.on('call')):&lt;br&gt;
peer.on('call', (call) =&amp;gt; {&lt;br&gt;
  setConnectionStatus('connecting');&lt;br&gt;
  callRef.current = call;&lt;br&gt;
  call.answer(stream); // Answer the call with our local camera stream&lt;/p&gt;

&lt;p&gt;call.on('stream', (remoteStream) =&amp;gt; {&lt;br&gt;
    // Attach the other person's video to our remote video element&lt;br&gt;
    if (remoteVideoRef.current) remoteVideoRef.current.srcObject = remoteStream;&lt;br&gt;
    setConnectionStatus('connected');&lt;br&gt;
  });&lt;br&gt;
});&lt;br&gt;
When Doctor A is sitting in the room and Patient B tries to call, the doctor's browser detects the incoming call event, automatically answers it by passing their own local camera stream, and hooks the patient's incoming stream directly into the remote video player.&lt;br&gt;
Step 4: The Auto-Call Trigger&lt;br&gt;
To make the connection truly seamless, we don't want the user to click a "Call" button. We want the patient to automatically dial the doctor as soon as they join the room.&lt;br&gt;
 setPeerId(id);&lt;br&gt;
  if (theirPeerId &amp;amp;&amp;amp; role === 'patient') {&lt;br&gt;
    setTimeout(() =&amp;gt; autoCall(stream, theirPeerId), 2000);&lt;br&gt;
  }&lt;br&gt;
});&lt;br&gt;
Why the delay?&lt;br&gt;
We add a small 2-second setTimeout buffer here. If the patient enters the room slightly faster than the doctor, the doctor's peer instance might not be registered yet, resulting in a failed connection. This brief delay gives the doctor's side enough time to initialise.&lt;br&gt;
Conclusion&lt;br&gt;
Here's the part I didn't mention earlier,  the first version of my video call feature was very generic. I'd basically stitched together the bare minimum and called it a day, still riding on the confidence I mentioned in the intro.&lt;br&gt;
Then I presented it at my capstone jury. I put the call on, and the lecturers just looked at me and said it wasn't working. I was confused; I could see the video on my end, so what were they talking about? Turns out there's a way to tell when a video signal actually isn't flowing, even if the UI looks fine, and I had no idea how they could spot that from across the room.&lt;br&gt;
One of them suggested I look into how Google Classroom handled its video calls, since it is open source. I wasn't about to lift their whole architecture,  a telemedicine app has security requirements a classroom app doesn't need to think about, but I used it as a reference point. I went back, rebuilt the connection logic with that as inspiration, and worked with an LLM to help me bring it to life.&lt;br&gt;
Writing this article made me realise that I still have more work to do in regard to my video call feature on my app. I've tested this setup on home wifi and mobile data, across different devices, in different places, and it works. But going through it step by step to explain it to you made me realise I've never once tested it on a locked-down network. Right now, the code only uses STUN servers, which is enough to find a direct path between two people on most networks. But stricter firewalls block that entirely, and the only real fix is a TURN server as a fallback, something I don't have set up yet but is now on my list.&lt;br&gt;
I'm sharing that because I'm not writing this as someone who's already got it all figured out. I'm a beginner who happened to solve this before you did, and I'm still finding gaps in my own understanding as I go. If you're building this for something people will actually rely on, especially anyone connecting from a hospital, office, or anywhere with a strict firewall, take some time to add a TURN server before you call it done. Don't make my mistake of assuming "it worked when I tested it" means "it'll work everywhere."&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>react</category>
      <category>webrtc</category>
      <category>beginners</category>
    </item>
  </channel>
</rss>
