npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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-serial

Usage

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 receivedData at most once per ~16 ms (roughly one frame), so a chatty device does not cause a render per chunk. receivedData entries are chunks as delivered by the port, not lines; use onChunk for your own framing.
  • connect() while already connecting joins the in-flight attempt; while connected or disconnecting it records an invalid-state error.
  • When the device is removed (the port fires disconnect), status returns to "idle" and port is reset. A read error that preceded the removal is kept in error.
  • disconnect() releases the reader and waits for queued writes before calling port.close(), which rejects while a stream is locked. If close() fails, the port stays connected so disconnect() 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