WebRTC

WebRTC React: How to Build Real-Time Video and Audio Apps

14 min read
Developer on a WebRTC video call built with React
Reading Time: 10 minutes

Video chat, screen sharing, and live audio all run on the same browser technology: WebRTC. Pair it with React and you get a component-driven way to build real-time features that work in the browser with no plugins and no third-party player.

The catch is that WebRTC and React don’t naturally fit together. WebRTC is an imperative, event-driven API built around long-lived connection objects. React wants declarative components and predictable renders. Getting them to cooperate (media streams in refs, peer connections in effects, signaling over a socket) is where most developers get stuck.

This article walks through WebRTC in React from the ground up: the core concepts, a working peer-to-peer video call with code, hooks patterns to keep it clean, the libraries worth knowing, and what to do when peer-to-peer stops scaling.

What Is WebRTC in React?

WebRTC (Web Real-Time Communication) is a browser API that lets two devices exchange audio, video, and arbitrary data directly, peer to peer, without routing media through a server. It’s built into every modern browser, so a React app can capture a camera, open a connection to another user, and stream video with nothing installed on either end.

React is the UI layer. It doesn’t know anything about media or connections. You use React to render the video elements, manage call state, and drive the WebRTC APIs from inside components. The pattern is consistent: WebRTC objects like RTCPeerConnection and MediaStream live outside React’s render cycle (usually in useRef), and React reacts to events they fire.

To learn the protocol itself in depth, see what is WebRTC. This guide focuses on wiring it into a React app.

Here’s the split in practice:

Concern Owned by WebRTC Owned by React
Camera and mic access getUserMedia() Trigger it, store the stream in a ref
Peer connection RTCPeerConnection Create in an effect, clean up on unmount
Media rendering Produces a MediaStream Attach stream to a <video> element
Call state (ringing, connected) Fires connection events Hold state, re-render UI
Signaling Not included; you build it Send/receive over a socket

Why Use WebRTC With React

React’s component model maps well to real-time UIs. A call screen is a set of video tiles, controls, and status indicators, exactly what components are good at. A few reasons the pairing works:

  • Reusable call components. A <VideoTile> or <CallControls> component can be dropped into any screen and driven by props.
  • Hooks encapsulate the mess. A custom useWebRTC hook hides connection setup, cleanup, and event wiring behind a clean interface.
  • State-driven UI. Mute status, connection quality, and participant lists live in React state, so the UI stays in sync with the call automatically.
  • One codebase, web and mobile. The same mental model carries to React Native, so teams reuse patterns across platforms.

The trade-off: WebRTC’s lifecycle doesn’t match React’s. Connections outlive renders, events fire outside React’s control, and forgetting to clean up leaks camera access. The patterns below exist to handle exactly that.

Core WebRTC Concepts Every React Developer Needs

Before writing components, you need four building blocks. Each one shows up in the code later.

getUserMedia captures local audio and video. It returns a MediaStream you attach to a <video> element and add to the peer connection.

RTCPeerConnection is the connection between two peers. It handles encoding, network traversal, and media transport. You create one per remote participant.

Signaling is how two peers find each other and agree on connection details. WebRTC does not provide this. You build it yourself, usually with WebSockets or Socket.IO. Peers exchange session descriptions (offer/answer) and network routes (ICE candidates) through the signaling channel. A dedicated WebRTC signaling server covers the protocol in detail.

ICE, STUN, and TURN solve the networking problem. Most devices sit behind routers using NAT, so their real address isn’t reachable. A STUN server tells a peer its public address; a TURN server relays media when a direct connection can’t be made. The whole discovery process is called NAT traversal.

Concept What it does React responsibility
getUserMedia() Captures camera/mic Call it, store stream in a ref
RTCPeerConnection Connects two peers Instantiate, wire events, clean up
Signaling Exchanges offers, answers, ICE Send/receive over your socket
STUN/TURN Finds a reachable path Pass server URLs in config
ICE candidates Possible network routes Relay each one via signaling
Data channel Sends arbitrary data peer-to-peer Open it, handle messages

One more concept worth knowing: the data channel. Alongside audio and video, a peer connection can open an RTCDataChannel for arbitrary data (chat messages, file transfers, game state) sent directly between browsers. If you only need data and not media, compare the approaches in WebRTC vs WebSocket.

How WebRTC Works in a React App

The connection handshake follows a fixed sequence. Understanding the order makes the code readable.

  1. Capture media. Each peer calls getUserMedia() and shows its local video.
  2. Create the connection. Each peer instantiates an RTCPeerConnection with STUN/TURN config and adds its local tracks.
  3. Caller creates an offer. It calls createOffer(), sets it as the local description, and sends it through signaling.
  4. Callee answers. It sets the offer as its remote description, calls createAnswer(), and sends the answer back.
  5. Trade ICE candidates. As each side discovers network routes, it fires onicecandidate and relays them over signaling. The other side adds them.
  6. Media flows. Once a route is agreed, ontrack fires with the remote stream, and React attaches it to a <video> element.

Everything before step 6 goes through your signaling server. The media itself flows directly between peers.

How to Build a WebRTC Video Call in React

This is a minimal two-peer video call: a Node signaling server and a React client. It’s deliberately stripped down so the WebRTC logic stays visible.

Step 1: Set Up the Signaling Server

The server relays offers, answers, and ICE candidates. It never touches the media. Socket.IO keeps the relay short.

// server.js
const express = require("express");
const { Server } = require("socket.io");
const http = require("http");

const app = express();
const server = http.createServer(app);
const io = new Server(server, { cors: { origin: "*" } });

io.on("connection", (socket) => {
  socket.on("join", (room) => {
    socket.join(room);
    socket.to(room).emit("peer-joined");
  });

  // Relay signaling messages to everyone else in the room
  socket.on("offer", ({ room, offer }) => socket.to(room).emit("offer", offer));
  socket.on("answer", ({ room, answer }) => socket.to(room).emit("answer", answer));
  socket.on("ice", ({ room, candidate }) => socket.to(room).emit("ice", candidate));
});

server.listen(4000, () => console.log("Signaling server on :4000"));

Step 2: Capture Local Media

In the React client, grab the camera and mic and show the local preview. Store the stream in a ref so it survives re-renders.

import { useEffect, useRef } from "react";

function useLocalStream(videoRef) {
  const streamRef = useRef(null);

  useEffect(() => {
    let active = true;
    navigator.mediaDevices
      .getUserMedia({ video: true, audio: true })
      .then((stream) => {
        if (!active) return;
        streamRef.current = stream;
        if (videoRef.current) videoRef.current.srcObject = stream;
      });

    return () => {
      active = false;
      streamRef.current?.getTracks().forEach((t) => t.stop());
    };
  }, [videoRef]);

  return streamRef;
}

The cleanup function matters. Calling track.stop() on unmount releases the camera. Skip it and the camera light stays on after the user leaves the call.

Step 3: Create the Peer Connection

Instantiate RTCPeerConnection with a STUN server, add the local tracks, and wire the two events React cares about: onicecandidate (relay it) and ontrack (show the remote stream).

function createPeer(localStream, remoteVideoRef, socket, room) {
  const pc = new RTCPeerConnection({
    iceServers: [{ urls: "stun:stun.l.google.com:19302" }],
  });

  localStream.getTracks().forEach((track) => pc.addTrack(track, localStream));

  pc.onicecandidate = (e) => {
    if (e.candidate) socket.emit("ice", { room, candidate: e.candidate });
  };

  pc.ontrack = (e) => {
    remoteVideoRef.current.srcObject = e.streams[0];
  };

  return pc;
}

For production, add a TURN server to the iceServers array. STUN alone fails when both peers sit behind restrictive NATs. A TURN server relays the media in those cases.

Step 4: Exchange the Offer and Answer

The caller creates an offer; the callee answers. Both go through the socket. This effect ties the pieces together.

useEffect(() => {
  const pc = createPeer(streamRef.current, remoteVideoRef, socket, room);

  // Caller side
  socket.on("peer-joined", async () => {
    const offer = await pc.createOffer();
    await pc.setLocalDescription(offer);
    socket.emit("offer", { room, offer });
  });

  // Callee side
  socket.on("offer", async (offer) => {
    await pc.setRemoteDescription(offer);
    const answer = await pc.createAnswer();
    await pc.setLocalDescription(answer);
    socket.emit("answer", { room, answer });
  });

  socket.on("answer", (answer) => pc.setRemoteDescription(answer));
  socket.on("ice", (candidate) => pc.addIceCandidate(candidate));

  return () => pc.close();
}, [room]);

That’s a full WebRTC video call in React between two peers. The local preview shows immediately; the remote video appears once ontrack fires. Closing the connection on cleanup prevents leaked peer connections when the component unmounts.

Managing WebRTC With React Hooks

The code above works but spreads WebRTC logic across a component. The cleaner pattern is a custom hook that owns the connection and exposes only what the UI needs.

function useWebRTC(room) {
  const localRef = useRef(null);
  const remoteRef = useRef(null);
  const pcRef = useRef(null);

  useEffect(() => {
    const socket = io("http://localhost:4000");
    let stream;

    async function start() {
      stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
      localRef.current.srcObject = stream;
      pcRef.current = createPeer(stream, remoteRef, socket, room);
      socket.emit("join", room);
      // ...offer/answer/ice wiring from Step 4
    }
    start();

    return () => {
      stream?.getTracks().forEach((t) => t.stop());
      pcRef.current?.close();
      socket.disconnect();
    };
  }, [room]);

  return { localRef, remoteRef };
}

The component that uses it stays declarative:

function VideoCall({ room }) {
  const { localRef, remoteRef } = useWebRTC(room);
  return (
    <div className="call">
      <video ref={localRef} autoPlay muted playsInline />
      <video ref={remoteRef} autoPlay playsInline />
    </div>
  );
}

A few rules keep hooks-based WebRTC stable:

  • Keep connections in refs, not state. Setting a peer connection in state triggers re-renders and can recreate the connection. Refs hold mutable objects without re-rendering.
  • Do all cleanup in the effect’s return. Stop tracks, close the connection, and disconnect the socket. Missing any one leaks resources.
  • Guard async setup. If the component unmounts mid-setup, check a flag before assigning refs so you don’t touch a detached element.
  • One effect per connection lifecycle. Tie the effect’s dependency to the room or peer ID so it re-runs only when the call target actually changes.

WebRTC in React Native

The web APIs above don’t exist in React Native: there’s no browser DOM and no navigator.mediaDevices. Instead, the react-native-webrtc library provides native bindings that mirror the same API surface: RTCPeerConnection, mediaDevices.getUserMedia(), and an RTCView component to render streams.

The signaling logic, offer/answer flow, and ICE handling are identical to the web. Only media capture and rendering differ. For the full mobile setup (including permissions, Expo config, and iOS/Android quirks), see the dedicated guide on React Native WebRTC. If you’re rendering recorded or HLS video rather than live peer streams, the React Native video example covers that path.

Libraries and Tools for WebRTC in React

Writing raw RTCPeerConnection code is fine for learning and for simple one-to-one calls. For anything larger, a library removes boilerplate.

Library Best for What it handles
simple-peer 1-to-1 and small mesh calls Wraps RTCPeerConnection in a clean API
PeerJS Quick prototypes Includes a hosted signaling/broker server
mediasoup Self-hosted group calls SFU for many-participant rooms
LiveKit Scaled real-time apps Open-source SFU plus React SDK
Socket.IO Custom signaling Reliable WebSocket transport for offers/ICE

Two things none of these replace: a signaling server and, at scale, media routing. simple-peer and PeerJS shorten the client code but still need signaling. For playing back the resulting streams outside a live call, a general-purpose React video player handles HLS and on-demand files.

If you’d rather not run any of this infrastructure yourself, a video conferencing API or video SDK gives you the connection, signaling, and scaling as a managed service.

The Limits of Peer-to-Peer WebRTC

Everything above assumes a small number of peers. That assumption breaks fast, and it’s the point where most WebRTC React projects hit a wall.

In a pure peer-to-peer mesh, every participant connects directly to every other participant. Three people means three connections each; ten people means nine uploads of your video per person. Browsers and uplinks can’t sustain that past four or five participants. The connection count grows quadratically while bandwidth stays flat.

Three other limits show up in production:

  • Reach. Peer-to-peer works for a handful of participants, not an audience of thousands watching one broadcaster.
  • Recording and replay. Media that flows directly between browsers never touches your servers, so recording a call or turning it into video on demand takes extra infrastructure.
  • Delivery to non-WebRTC devices. Smart TVs, Roku, and native players speak HLS, not WebRTC. Compare the two in WebRTC vs HLS.

This is the border between a demo and a product. Small interactive calls stay on WebRTC. One-to-many broadcasts and anything that needs recording or wide device support move to a different architecture.

Scaling Beyond Peer-to-Peer

When mesh stops scaling, media routing takes over. Instead of every peer talking to every other peer, participants send one stream to a server that forwards it. Two common shapes:

  • SFU (Selective Forwarding Unit). Each peer uploads once; the server forwards streams to everyone else. This is how group video calls scale to dozens of participants. mediasoup and LiveKit are SFUs. See WebRTC server for how these are built.
  • Broadcast pipeline. For one-to-many (a webinar, a live event, a class of hundreds), you ingest one WebRTC or RTMP stream and deliver it as HLS over a CDN. This trades sub-second latency for near-unlimited reach.

For that broadcast path, LiveAPI handles the heavy part. You capture the stream in your React app, send it to LiveAPI over RTMP or SRT, and it transcodes to adaptive bitrate HLS delivered through Akamai, Cloudflare, and Fastly — up to 4K, with automatic live-to-VOD recording. Your WebRTC layer stays for the interactive part; the video API handles distribution to any device.

The rule of thumb: keep WebRTC for real-time interaction between a few people, and hand off to a streaming pipeline the moment you need scale, recording, or reach.

WebRTC React FAQ

Can you use WebRTC directly in React?
Yes. WebRTC is a browser API, so any React app can call getUserMedia() and RTCPeerConnection directly. The main work is keeping WebRTC’s connection objects in refs (not state) and cleaning them up in effects so React’s render cycle doesn’t recreate or leak them.

Do you need a server for WebRTC in React?
Media flows peer-to-peer, but you always need a signaling server to exchange offers, answers, and ICE candidates before the connection forms. You’ll also typically need a STUN server for NAT traversal and a TURN server as a fallback relay when a direct connection fails.

How do you build a video call in React with WebRTC?
Capture media with getUserMedia(), create an RTCPeerConnection for each peer, exchange an offer and answer through a signaling server, trade ICE candidates, and attach the remote stream to a <video> element when the ontrack event fires. A Node plus Socket.IO server handles the signaling relay.

What’s the difference between WebRTC in React and React Native?
The signaling and connection logic are the same. React uses the built-in browser APIs, while React Native uses the react-native-webrtc library for native RTCPeerConnection bindings and an RTCView component to render streams instead of a <video> tag.

How many users can a WebRTC React app support?
A peer-to-peer mesh handles roughly four to five participants before browser and bandwidth limits hit. Beyond that, you route media through an SFU for group calls or a broadcast pipeline that delivers HLS for one-to-many audiences.

Which library is best for WebRTC in React?
For simple one-to-one calls, simple-peer or PeerJS cut the boilerplate. For group calls you self-host, mediasoup or LiveKit provide an SFU. For managed scale without running servers, a hosted video conferencing API handles connection and routing.

Does WebRTC work in all browsers?
WebRTC runs in all current versions of Chrome, Firefox, Safari, and Edge. Older browsers and some in-app webviews have gaps, and codec support varies, so test your target browsers and keep TURN available for restrictive networks.

Bringing It Together

WebRTC gives React apps real-time video, audio, and data straight in the browser. The core pattern is consistent: capture media, create a peer connection, signal the offer and answer, trade ICE candidates, and render the remote stream — with every WebRTC object living in a ref and cleaned up in an effect.

Peer-to-peer is the right tool for small, interactive calls. Once you need to reach a large audience, record sessions, or deliver to TVs and native players, the media moves to a streaming pipeline built on HLS and a CDN.

If your React app needs that reach, try LiveAPI free — capture in the browser, stream to any device, and go live in days instead of months.

Join 200,000+ satisfied streamers

Still on the fence? Take a sneak peek and see what you can do with Castr.

No Castr Branding

No Castr Branding

We do not include our branding on your videos.

No Commitment

No Commitment

No contracts. Cancel or change your plans anytime.

24/7 Support

24/7 Support

Highly skilled in-house engineers ready to help.

  • Check Free 7-day trial
  • CheckCancel anytime
  • CheckNo credit card required

Related Articles