React hooks and context provider for the Web Serial API.
Live demo: React Web Serial Monitor (Chromium-based browsers, a USB serial device such as an Arduino required)
npm install react-web-serial- Serial Monitor — Minimal serial monitor app with an Arduino echo sketch
import { SerialProvider, useSerialPort } from "react-web-serial";
const SerialMonitor = () => {
const {
isSerialSupported,
status,
isConnected,
isReading,
receivedData,
error,
connect,
disconnect,
write,
startReading,
stopReading,
clearError,
clearReceivedData,
} = useSerialPort({
options: { baudRate: 9600 },
});
if (!isSerialSupported) {
return <p>Web Serial API is not supported in this browser.</p>;
}
const connectAndRead = async () => {
if (await connect()) startReading();
};
return (
<div>
<p>status: {status}</p>
<button type="button" onClick={connectAndRead} disabled={status !== "idle"}>
Connect
</button>
<button type="button" onClick={disconnect} disabled={!isConnected}>
Disconnect
</button>
<button type="button" onClick={() => startReading()} disabled={!isConnected || isReading}>
Start Reading
</button>
<button type="button" onClick={stopReading} disabled={!isReading}>
Stop Reading
</button>
<button type="button" onClick={() => write("hello")} disabled={!isConnected}>
Send "hello"
</button>
<button type="button" onClick={clearReceivedData}>Clear</button>
{error && (
<p>
Error: {error.message} <button type="button" onClick={clearError}>Dismiss</button>
</p>
)}
<ul>
{receivedData.map((entry) => (
<li key={entry.id}>
{entry.mode === "text" ? entry.value : Array.from(entry.value).join(", ")}
</li>
))}
</ul>
</div>
);
};
const App = () => (
<SerialProvider>
<SerialMonitor />
</SerialProvider>
);navigator.serial.requestPort() must be called from a user gesture. To reconnect to a device the user has already granted access to (for example on page load), pass a port from getPorts():
const { connect, getPorts } = useSerialPort({ options: { baudRate: 9600 } });
useEffect(() => {
getPorts().then(([port]) => {
if (port) connect({ port });
});
}, [connect, getPorts]);While reading, receivedData is republished up to about 60 times per second, and every component that calls useSerialPort re-renders with it. Components that only need the connection (a toolbar, a status line) can use useSerialConnection instead; components that show the data use useSerialData. Both read the same store snapshot, so they never disagree.
const Toolbar = () => {
const { status, connect, disconnect } = useSerialConnection({ options: { baudRate: 9600 } });
// not re-rendered when data arrives
...
};
const Log = () => {
const { receivedData, clearReceivedData } = useSerialData();
...
};Context provider. Wrap your app or component tree with this. The serial state lives in a store outside React that the provider observes with useSyncExternalStore.
<SerialProvider>{children}</SerialProvider>Returns the connection state and actions together with the received data (everything listed under State and Actions below). Re-renders on every change, including data flushes.
Same parameters as useSerialPort. Returns the State and Actions below except receivedData and clearReceivedData. Not re-rendered when receivedData changes.
Returns { receivedData, clearReceivedData }.
All parameters are optional defaults. An argument passed to connect() or startReading() overrides them field by field.
| Name | Type | Description |
|---|---|---|
options |
SerialOptions |
Options passed to port.open() (e.g. { baudRate: 9600 }). Required by connect(), here or as its argument |
requestOptions |
SerialPortRequestOptions |
Filters for navigator.serial.requestPort() |
maxReceivedDataCount |
number |
Max entries in receivedData buffer. Default: 1000 |
mode |
"text" | "binary" |
Data mode for startReading. Default: "text" |
| Name | Type | Description |
|---|---|---|
isSerialSupported |
boolean |
Whether the browser supports Web Serial API |
status |
"idle" | "connecting" | "connected" | "disconnecting" |
Connection status |
isConnecting |
boolean |
status === "connecting" |
isConnected |
boolean |
status === "connected" |
port |
SerialPortInfo | null |
Connected port (exposes getInfo and forget) |
isUserCancelled |
boolean |
User dismissed the port selection dialog on the last connect() |
isReading |
boolean |
A read loop is running |
receivedData |
SerialReceivedDataEntry[] |
Received data buffer (useSerialPort / useSerialData) |
error |
Error | null |
Last error, kept for display until cleared or the next successful action |
Every action reports its outcome through its return value; error additionally keeps the last failure for display.
| Name | Type | Description |
|---|---|---|
connect |
(params?: SerialConnectParams) => Promise<boolean> |
Open the chooser (or use params.port) and open the port. Resolves true when connected |
disconnect |
() => Promise<boolean> |
Close the port. Resolves false only when close() failed; the port is kept so the call can be retried |
write |
(data: string | Uint8Array) => Promise<boolean> |
Write data to the port. Writes are serialized |
startReading |
(options?: StartReadingOptions) => boolean |
Start a read loop. Returns false if it could not start |
stopReading |
() => Promise<void> |
Stop the read loop and release the reader |
getPorts |
() => Promise<SerialPort[]> |
Ports the user has already granted access to |
clearError |
() => void |
Clear error |
clearReceivedData |
() => void |
Clear the received data buffer (useSerialPort / useSerialData) |
type SerialDataMode = "text" | "binary";
// `id` increases per entry and is never reused, so it works as a React key
// (several chunks can share one timestamp). It is unique within one
// SerialProvider; combine it with a port name if you merge lists from
// several providers.
type SerialReceivedDataEntry =
| { id: number; mode: "text"; timestamp: Date; value: string }
| { id: number; mode: "binary"; timestamp: Date; value: Uint8Array };
interface SerialConnectParams {
options?: SerialOptions;
requestOptions?: SerialPortRequestOptions;
port?: SerialPort; // from getPorts(); skips the chooser
}
interface StartReadingOptions {
maxReceivedDataCount?: number;
mode?: SerialDataMode;
onChunk?: (chunk: Uint8Array) => void; // raw chunks, before decoding and buffering
}
// Errors raised by the library itself; browser errors (DOMException) pass through.
class SerialError extends Error {
code:
| "not-available"
| "options-required"
| "invalid-state"
| "not-connected"
| "not-writable"
| "already-reading";
}- Serial state lives outside React in a store that the provider observes with
useSyncExternalStore. Re-renders and StrictMode's double effects never open, close or re-lock the port. - Received chunks are published to
receivedDataat most once per ~16 ms (roughly one frame), so a chatty device does not cause a render per chunk.receivedDataentries are chunks as delivered by the port, not lines; useonChunkfor your own framing. connect()while already connecting joins the in-flight attempt; while connected or disconnecting it records aninvalid-stateerror.- When the device is removed (the port fires
disconnect),statusreturns to"idle"andportis reset. A read error that preceded the removal is kept inerror. disconnect()releases the reader and waits for queued writes before callingport.close(), which rejects while a stream is locked. Ifclose()fails, the port stays connected sodisconnect()can be retried.
Web Serial API is available in Chromium-based browsers (Chrome, Edge, Opera). See Can I use for details.
MIT