react-web-serial
v2.0.0
Published
React hooks and context provider for the Web Serial API
Readme
react-web-serial
React hooks and context provider for the Web Serial API.
Install
npm install react-web-serialUsage
Examples
- Serial Monitor — Minimal serial monitor app with an Arduino echo sketch
Basic
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>
);Reconnecting without the chooser
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]);Keeping connection-only components out of the data updates
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();
...
};API
<SerialProvider>
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>useSerialPort(params?)
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.
useSerialConnection(params?)
Same parameters as useSerialPort. Returns the State and Actions below except receivedData and clearReceivedData. Not re-rendered when receivedData changes.
useSerialData()
Returns { receivedData, clearReceivedData }.
Parameters
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" |
State
| 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 |
Actions
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) |
Types
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";
}Behavior notes
- 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.
Browser support
Web Serial API is available in Chromium-based browsers (Chrome, Edge, Opera). See Can I use for details.
License
MIT
