Local network file transfer apps are becoming popular again because businesses and developers want instant file sharing without depending on the internet.
When two devices are on the same WiFi or LAN, they can exchange files directly, no uploads, no slow downloads, & no security concerns from third-party cloud platforms.
Traditional cloud services force you to upload the file first and then download it again, which wastes time.
But with ReactJS + WebRTC, you can enable real-time peer-to-peer (P2P) file transfer where devices communicate directly using the browser.
This means faster speeds, reduced delays, and higher privacy. In this blog, we’ll build a local network file transfer app in ReactJS using WebRTC’s RTCDataChannel.
You’ll also get a live GitHub repository, so you can easily start transferring files instantly within your network.
What Makes WebRTC the Best Choice for Peer-to-Peer File Sharing?
1. Zero server load: Pure P2P
- WebRTC is designed for pure peer-to-peer communication, meaning files never touch any server.
- This reduces server costs and significantly increases privacy.
- Once peers are connected, the transfer happens directly inside the LAN, making it extremely fast.
2. How does RTCDataChannel make file transfer smooth?
- WebRTC’s RTCDataChannel allows sending binary data, like images, documents, videos, and ZIP files, smoothly between two devices.
- It supports file chunking, reliable delivery, and high-speed streaming, making it perfect for local network file sharing in ReactJS.
3. Why WebSockets are not enough alone?
- WebSockets are fast, but they still require a server in the middle to relay data. This means slower speeds and higher bandwidth usage.
- For large file transfers, this becomes expensive and inefficient.
- WebSockets = server-dependent
- WebRTC = peer-to-peer, no server load
When to choose WebRTC vs WebSockets?
| Feature | WebRTC | WebSockets |
| Server Load | Zero (P2P) | High (server relays data) |
| Ideal For | File transfer, P2P apps | Chats, notifications |
| Speed | Very fast on LAN | Slower due to server hop |
| Complexity | Higher | Lower |
| Best Choice For File Transfer | Yes | No |
If your goal is file sharing inside a local network, WebRTC wins every time.
How Local Network File Sharing Works?
Device A → LAN → Signaling → Device B
- Device A wants to share a file.
- It connects to Device B through a signaling server.
- Once WebRTC connects the peers, local LAN routing takes over.
- The file transfers directly via RTCDataChannel.
How does STUN work even inside local networks?
- A STUN server helps each device discover its local IP address and open a stable communication path.
- Even when both devices are on the same WiFi, STUN ensures a smooth peer connection.
Why does this method transfer files faster than cloud services?
- Cloud: Upload → Server → Download → Device
- Local WebRTC: Device → LAN → Device (direct)
No middleman. No internet. No bottleneck.
That’s why WebRTC file transfer in React can hit speeds close to your router’s maximum capacity.
How React + WebRTC Connect to Transfer Files Over LAN?
Folder Structure
/src
/components
FileSender.js
FileReceiver.js
/utils
rtcConfig.js
chunkUtils.js
App.js
signaling.js
WebRTC connection workflow
- Sender starts connection.
- Signaling server exchanges SDPs.
- RTCDataChannel opens.
- File chunks stream from sender → receiver.
- The receiver assembles chunks into a complete file.
Secure P2P communication
WebRTC encrypts data automatically using DTLS + SRTP.
This makes your local network file sharing system safe by default, without adding any extra code.
How to Set Up The Project?
Installing dependencies
We’ll use the Create React App for simplicity and a tiny WebSocket server (ws) for signaling.
# 1. Create React app
npx create-react-app react-webrtc-file-transfer
cd react-webrtc-file-transfer
# 2. Create server folder for signaling
mkdir server
cd server
npm init -y
npm i ws express
# 3. Back to client folder for optional dev tools
cd ..
# (No extra client deps required — we use native WebRTC APIs)
Why these packages?
- create-react-app: Quick ReactJS scaffolding for UI.
- ws: Tiny WebSocket server for signaling (sockets).
- express: Optional static hosting for the signaling server.
Adding WebRTC API support
Add a .env if you want to set the signaling server URL:
REACT_APP_SIGNALING_URL=ws://<YOUR_SERVER_IP>:PORT
Replace <YOUR_SERVER_IP> with your machine IP on the same WiFi (e.g., 192.168.1.50 ), so other devices on the LAN can reach the signaling server.
Project structure /src:
/src
App.js
FileSender.js
FileReceiver.js
signaling.js
utils.js
index.css
How to Create the WebRTC Signaling Logic?
Create server/index.js:
// server/index.js
const express = require('express');
const http = require('http');
const WebSocket = require('ws');
const path = require('path');
const app = express();
const server = http.createServer(app);
const wss = new WebSocket.Server({ server });
const clients = new Map(); // clientId -> ws
wss.on('connection', (ws) => {
// assign random id
const id = Math.random().toString(36).substring(2, 9);
clients.set(id, ws);
console.log(`Client connected: ${id}`);
ws.send(JSON.stringify({ type: 'id', id }));
ws.on('message', (message) => {
try {
const data = JSON.parse(message);
// if target specified, relay
if (data.target && clients.has(data.target)) {
const targetWs = clients.get(data.target);
targetWs.send(JSON.stringify({ ...data, from: id }));
} else if (data.type === 'list') {
// send list of connected clients
const list = Array.from(clients.keys()).filter(c => c !== id);
ws.send(JSON.stringify({ type: 'list', clients: list }));
} else {
// broadcast to all others (optional)
for (let [cid, clientWs] of clients) {
if (cid !== id && clientWs.readyState === WebSocket.OPEN) {
clientWs.send(JSON.stringify({ ...data, from: id }));
}
}
}
} catch (err) {
console.error('Failed to parse message', err);
}
});
ws.on('close', () => {
clients.delete(id);
console.log(`Client disconnected: ${id}`);
});
});
// Optional: serve static UI or health-check
app.get('/', (req, res) => res.send('Signaling server running'));
const PORT = process.env.PORT || 8888;
server.listen(PORT, () => {
console.log(`Signaling server listening on port ${PORT}`);
});
Start the server:
cd server
node index.js
# or for development: npx nodemon index.js
How it works: The server assigns an ID to each client and relays messages when a target is specified. Clients can request a list of peers with type ‘list’.
How to Build the RTCDataChannel File Transfer System in ReactJS?
src/signaling.js: Lightweight signaling client (WebSocket)
// src/signaling.js
const SIGNALING_URL = process.env.REACT_APP_SIGNALING_URL || 'ws://localhost:8888';
export default class Signaling {
constructor(onMessage) {
this.ws = new WebSocket(SIGNALING_URL);
this.id = null;
this.onMessage = onMessage;
this.ws.onopen = () => console.log('Signaling connected');
this.ws.onmessage = (ev) => {
const data = JSON.parse(ev.data);
if (data.type === 'id') {
this.id = data.id;
}
if (this.onMessage) this.onMessage(data);
};
this.ws.onclose = () => console.log('Signaling disconnected');
this.ws.onerror = (err) => console.error('Signaling error', err);
}
send(obj) {
this.ws.send(JSON.stringify(obj));
}
}
src/utils.js: Chunk helpers
// src/utils.js
export const CHUNK_SIZE = 16 * 1024; // 16KB
export function sliceFile(file) {
const chunks = [];
let offset = 0;
while (offset < file.size) {
const slice = file.slice(offset, offset + CHUNK_SIZE);
chunks.push(slice);
offset += CHUNK_SIZE;
}
return chunks;
}
export function blobFromChunks(chunks, type) {
return new Blob(chunks, { type });
}
src/FileSender.js: Sender component (ReactJS + WebRTC)
// src/FileSender.js
import React, { useState, useRef, useEffect } from 'react';
import Signaling from './signaling';
import { sliceFile, CHUNK_SIZE } from './utils';
export default function FileSender() {
const [signaling, setSignaling] = useState(null);
const [peers, setPeers] = useState([]);
const [myId, setMyId] = useState(null);
const [targetId, setTargetId] = useState('');
const [file, setFile] = useState(null);
const [progress, setProgress] = useState(0);
const [speed, setSpeed] = useState(0);
const pcRef = useRef(null);
const dcRef = useRef(null);
const sendingRef = useRef({ bytesSent: 0, lastTime: 0, lastBytes: 0 });
useEffect(() => {
const s = new Signaling(handleSignaling);
setSignaling(s);
return () => s.ws.close();
}, []);
function handleSignaling(data) {
if (data.type === 'id') {
setMyId(data.id);
} else if (data.type === 'list') {
setPeers(data.clients);
} else if (data.type === 'offer' && data.from === targetId) {
// not used in sender example (sender usually creates offer)
} else if (data.type === 'answer' && data.from === targetId) {
pcRef.current.setRemoteDescription(new RTCSessionDescription(data.answer));
} else if (data.type === 'ice' && data.from === targetId) {
pcRef.current.addIceCandidate(new RTCIceCandidate(data.candidate));
}
}
async function createPeerAndChannel() {
const pc = new RTCPeerConnection();
pcRef.current = pc;
// ICE candidates -> signaling server
pc.onicecandidate = (e) => {
if (e.candidate) {
signaling.send({ type: 'ice', target: targetId, candidate: e.candidate });
}
};
// Create DataChannel for file sending
const dc = pc.createDataChannel('file-transfer');
dcRef.current = dc;
dc.onopen = () => {
console.log('DataChannel open - ready to send');
// start sending if file is selected
if (file) sendFile(file);
};
dc.onclose = () => console.log('DataChannel closed');
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
signaling.send({ type: 'offer', target: targetId, offer });
}
async function sendFile(fileToSend) {
if (!dcRef.current || dcRef.current.readyState !== 'open') {
console.warn('DataChannel not open yet');
return;
}
const chunks = sliceFile(fileToSend);
const totalChunks = chunks.length;
sendingRef.current = { bytesSent: 0, lastTime: Date.now(), lastBytes: 0 };
// Send metadata first
dcRef.current.send(JSON.stringify({ meta: { name: fileToSend.name, size: fileToSend.size, type: fileToSend.type } }));
for (let i = 0; i < totalChunks; i++) {
const chunk = await chunks[i].arrayBuffer();
// Wait for buffer to free if necessary
await waitForBufferedAmountLow(dcRef.current);
dcRef.current.send(chunk);
sendingRef.current.bytesSent += chunk.byteLength;
const percent = Math.round((sendingRef.current.bytesSent / fileToSend.size) * 100);
setProgress(percent);
updateSpeed();
}
// signal end
dcRef.current.send(JSON.stringify({ done: true }));
console.log('Send complete');
}
function waitForBufferedAmountLow(dc) {
return new Promise((resolve) => {
const threshold = CHUNK_SIZE * 8; // buffer threshold
if (dc.bufferedAmount < threshold) return resolve();
const handler = () => {
if (dc.bufferedAmount < threshold) {
dc.removeEventListener('bufferedamountlow', handler);
resolve();
}
};
dc.addEventListener('bufferedamountlow', handler);
// also resolve after a timeout fallback
setTimeout(resolve, 2000);
});
}
function updateSpeed() {
const now = Date.now();
const elapsed = (now - sendingRef.current.lastTime) / 1000 || 0.001;
const bytes = sendingRef.current.bytesSent - sendingRef.current.lastBytes;
const bps = bytes / elapsed;
sendingRef.current.lastTime = now;
sendingRef.current.lastBytes = sendingRef.current.bytesSent;
setSpeed(Math.round(bps)); // bytes per second
}
// Handle incoming signaling messages like answer
useEffect(() => {
// handle 'offer' responses from server if any
const s = signaling;
if (!s) return;
s.send({ type: 'list' }); // fetch peers
}, [signaling]);
// UI handlers
return (
<div style={{ padding: 16 }}>
<h3>Sender (React + WebRTC)</h3>
<p>Your ID: <b>{myId || 'connecting...'}</b></p>
<div>
<label>Available peers: </label>
<select value={targetId} onChange={(e) => setTargetId(e.target.value)}>
<option value=''>Select peer</option>
{peers.map(p => <option value={p} key={p}>{p}</option>)}
</select>
<button onClick={() => signaling && signaling.send({ type: 'list' })}>Refresh Peers</button>
</div>
<div style={{ marginTop: 12 }}>
<input type="file" onChange={(e) => setFile(e.target.files[0])} />
</div>
<div style={{ marginTop: 12 }}>
<button onClick={createPeerAndChannel} disabled={!targetId || !file}>Connect & Send</button>
</div>
<div style={{ marginTop: 12 }}>
<p>Progress: {progress}%</p>
<div style={{ width: '100%', height: 12, background: '#eee' }}>
<div style={{ width: `${progress}%`, height: 12, background: '#4caf50' }} />
</div>
<p>Speed: {(speed/1024).toFixed(2)} KB/s</p>
</div>
</div>
);
}
src/FileReceiver.js: Receiver component (ReactJS + WebRTC)
// src/FileReceiver.js
import React, { useState, useRef, useEffect } from 'react';
import Signaling from './signaling';
import { blobFromChunks } from './utils';
export default function FileReceiver() {
const [signaling, setSignaling] = useState(null);
const [myId, setMyId] = useState(null);
const [peers, setPeers] = useState([]);
const [targetId, setTargetId] = useState('');
const [progress, setProgress] = useState(0);
const [status, setStatus] = useState('idle');
const pcRef = useRef(null);
const dcRef = useRef(null);
const receivedChunksRef = useRef([]);
const metaRef = useRef(null);
const receivedBytesRef = useRef(0);
const speedRef = useRef({ lastTime: Date.now(), lastBytes: 0 });
const [speed, setSpeed] = useState(0);
useEffect(() => {
const s = new Signaling(handleSignaling);
setSignaling(s);
return () => s.ws.close();
}, []);
function handleSignaling(data) {
if (data.type === 'id') {
setMyId(data.id);
} else if (data.type === 'list') {
setPeers(data.clients);
} else if (data.type === 'offer' && data.from === targetId) {
acceptOffer(data.offer, data.from);
} else if (data.type === 'ice' && data.from === targetId) {
pcRef.current.addIceCandidate(new RTCIceCandidate(data.candidate));
} else if (data.type === 'answer' && data.from === targetId) {
// if receiver generated answer earlier
pcRef.current.setRemoteDescription(new RTCSessionDescription(data.answer));
}
}
async function acceptOffer(offer, from) {
setStatus('connecting');
const pc = new RTCPeerConnection();
pcRef.current = pc;
pc.ondatachannel = (event) => {
const dc = event.channel;
dcRef.current = dc;
setupDataChannel(dc);
};
pc.onicecandidate = (e) => {
if (e.candidate) {
signaling.send({ type: 'ice', target: from, candidate: e.candidate });
}
};
await pc.setRemoteDescription(new RTCSessionDescription(offer));
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
signaling.send({ type: 'answer', target: from, answer });
}
function setupDataChannel(dc) {
dc.binaryType = 'arraybuffer';
dc.onopen = () => {
setStatus('connected');
console.log('DataChannel open (receiver)');
};
dc.onmessage = async (event) => {
// handle JSON meta and done signals vs binary chunk
try {
// try parse json (meta or done)
if (typeof event.data === 'string') {
const data = JSON.parse(event.data);
if (data.meta) {
metaRef.current = data.meta;
receivedChunksRef.current = [];
receivedBytesRef.current = 0;
setProgress(0);
setStatus('receiving');
} else if (data.done) {
// assemble and download
const blob = blobFromChunks(receivedChunksRef.current, metaRef.current.type || 'application/octet-stream');
downloadBlob(blob, metaRef.current.name);
setStatus('complete');
}
} else {
// binary chunk
receivedChunksRef.current.push(event.data);
receivedBytesRef.current += event.data.byteLength;
if (metaRef.current && metaRef.current.size) {
const percent = Math.round((receivedBytesRef.current / metaRef.current.size) * 100);
setProgress(percent);
updateSpeed();
}
}
} catch (err) {
console.error('Error handling data', err);
}
};
dc.onclose = () => setStatus('closed');
}
function downloadBlob(blob, name) {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.style.display = 'none';
a.href = url;
a.download = name || 'download';
document.body.appendChild(a);
a.click();
setTimeout(() => {
document.body.removeChild(a);
URL.revokeObjectURL(url);
}, 100);
}
function updateSpeed() {
const now = Date.now();
const elapsed = (now - speedRef.current.lastTime) / 1000 || 0.001;
const bytes = receivedBytesRef.current - speedRef.current.lastBytes;
const bps = bytes / elapsed;
speedRef.current.lastTime = now;
speedRef.current.lastBytes = receivedBytesRef.current;
setSpeed(Math.round(bps));
}
// UI
useEffect(() => {
if (signaling) signaling.send({ type: 'list' });
}, [signaling]);
// Provide an option to pick a peer to listen to or wait for an incoming offer
return (
<div style={{ padding: 16 }}>
<h3>Receiver (React + WebRTC)</h3>
<p>Your ID: <b>{myId || 'connecting...'}</b></p>
<div>
<label>Peers:</label>
<select value={targetId} onChange={(e) => setTargetId(e.target.value)}>
<option value=''>Select peer</option>
{peers.map(p => <option key={p} value={p}>{p}</option>)}
</select>
<button onClick={() => signaling && signaling.send({ type: 'list' })}>Refresh</button>
</div>
<div style={{ marginTop: 12 }}>
<p>Status: {status}</p>
<p>Progress: {progress}%</p>
<div style={{ width: '100%', height: 12, background: '#eee' }}>
<div style={{ width: `${progress}%`, height: 12, background: '#2196f3' }} />
</div>
<p>Speed: {(speed/1024).toFixed(2)} KB/s</p>
</div>
</div>
);
}
src/App.js: Small UI to choose role
// src/App.js
import React, { useState } from 'react';
import FileSender from './FileSender';
import FileReceiver from './FileReceiver';
import './index.css';
function App() {
const [role, setRole] = useState('sender');
return (
<div style={{ maxWidth: 900, margin: '0 auto', padding: 24 }}>
<h1>Local LAN File Transfer — ReactJS + WebRTC</h1>
<p>Choose role (device on same WiFi):</p>
<div style={{ marginBottom: 16 }}>
<button onClick={() => setRole('sender')}>Sender</button>
<button onClick={() => setRole('receiver')} style={{ marginLeft: 8 }}>Receiver</button>
</div>
{role === 'sender' ? <FileSender /> : <FileReceiver />}
<hr />
<p style={{ fontSize: 13 }}>
Uses RTCDataChannel for peer-to-peer file transfer. Signaling via WebSocket server (sockets).
</p>
</div>
);
}
export default App;
Here’s the Complete GitHub Link to Build a Local Network File Transfer App in ReactJS Using WebRTC.
WebRTC vs WebSockets for File Transfer: Clear Winner?
| Feature | WebRTC (P2P) | WebSockets (Client–Server) |
| Speed | Extremely fast on LAN | Depends on server bandwidth |
| Security | Encrypted end-to-end | Server can access data |
| Performance | Excellent for file chunks | Can lag with large files |
| Local Network Compatibility | Native P2P support | Server must be reachable |
| Use-Case Recommendation | File transfer, P2P apps | Real-time messaging |
Winner for file transfer: WebRTC (RTCDataChannel)
If you need a LAN-based file transfer app in ReactJS, WebRTC gives unmatched performance and privacy.
Build the Future of Peer-to-Peer Sharing with Our WebRTC Expertise
We help businesses to build fast, secure, and future-ready Local Network File Transfer apps using ReactJS, WebRTC, and WebSockets.
- We provide end-to-end development, architecture, coding, UI, testing, & deployment to launch reliable React WebRTC file sharing applications faster.
- Our vision is to build where the future is headed by combining WebRTC innovation with real-world scalability and business impact.
- Our developers ensure your local file transfer app works smoothly across countries, networks, infrastructures, and device ecosystems.
- We support international clients with complete cross-border assistance, multilingual communication, & smooth currency conversions.
- Our team believes peer-to-peer technology will change the future, and we help businesses adopt WebRTC early to stay future-ready.
Want a Customized WebRTC File Transfer App Built for You? Contact Us Now!
What Are the Pro Features That You Can Add to Turn This Into a Real Product?
- Multi-peer transfers: Send files to multiple devices at once using multiple RTC connections.
- Folder transfer support: Allow users to drag and share entire folders by zipping and transferring them via WebRTC.
- Password-protected sessions: Add passcodes or one-time session keys for extra security.
- QR-based connection: Generate a QR code containing the connection token so users can connect instantly on the same network.
- Drag-and-drop UI: Make file sharing simple with a clean drag-and-drop interface built in React.
Why WebRTC Is the Future of Local File Sharing in ReactJS?
WebRTC delivers pure P2P, meaning it is fast, secure, and scalable without any heavy backend infrastructure.
When combined with ReactJS, developers get a flexible and modern UI framework for building powerful file transfer tools.
For businesses and internal teams, this approach offers:
- No cloud dependency.
- Faster LAN transfers.
- Improved privacy.
- Lower operational costs.
If you want a secure, modern, and high-speed local network file transfer app, ReactJS + WebRTC is the best tech stack to choose.
FAQs
- Yes. WebRTC is usually faster because it uses direct P2P connections. WebSockets always send data through a server.
- For LAN file sharing or local WiFi transfer, WebRTC gives better speed and lower latency.
- You only need a server for signaling, such as exchanging offer/answer (SDP) and ICE candidates.
- After the signaling step, the peer-to-peer connection becomes completely serverless, and the file transfer happens directly.
- Very secure. WebRTC encrypts data with DTLS + SRTP.
- Even in a local network file transfer setup, files are encrypted end-to-end. No server or third party can view the transferred data.
- Yes. You can transfer large files by splitting them into file chunks and sending them via RTCDataChannel.
- This method ensures smooth transfer without browser crashes or memory issues.