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

@a4smanjorg5/react-serial

v0.4.0

Published

A tiny React hook for talking to serial devices from the browser via the Web Serial API.

Readme

@a4smanjorg5/react-serial

A tiny React hook for talking to serial devices from the browser via the Web Serial API.

useSerial handles port selection, opening the connection, continuously reading the incoming stream into React state, writing data back, and cleanly tearing everything down on unmount.

Requirements

  • React ^18 || ^19 (peer dependency)
  • A browser that supports the Web Serial API — Chrome / Edge (desktop). The hook will alert and bail out where navigator.serial is unavailable.
  • The Web Serial API only works in a secure context (HTTPS or localhost) and must be triggered by a user gesture (e.g. a button click).

Installation

npm install @a4smanjorg5/react-serial
# or
pnpm add @a4smanjorg5/react-serial
# or
yarn add @a4smanjorg5/react-serial

Usage

import { useSerial } from '@a4smanjorg5/react-serial'

export function SerialMonitor() {
  const { isConnected, receivedData, connectSerial, disconnectSerial, sendData } =
    useSerial()

  return (
    <div>
      {isConnected ? (
        <button onClick={disconnectSerial}>Disconnect</button>
      ) : (
        // Must be called from a user gesture
        <button onClick={() => connectSerial({ baudRate: 9600 })}>Connect</button>
      )}

      <pre>{receivedData}</pre>

      <button
        disabled={!isConnected}
        onClick={() => sendData(new TextEncoder().encode('ping\n'))}
      >
        Send "ping"
      </button>
    </div>
  )
}

By default incoming bytes are decoded as UTF-8 text, so receivedData is a string.

API

const {
  info,
  isConnected,
  receivedData,
  ports,
  connectSerial,
  disconnectSerial,
  sendData,
} = useSerial<T = string>(onData?, options?)

Parameters

| Parameter | Type | Description | | --- | --- | --- | | onData | (data?: Uint8Array, prev?: Uint8Array \| T) => T | Reducer called for every chunk read from the port. Receives the raw bytes and the previous accumulated value, and returns the new value stored in receivedData. Defaults to UTF-8 decoding each chunk to a string. | | options | SerialPortRequestOptions | Passed to navigator.serial.requestPort(), e.g. { filters: [{ usbVendorId: 0x2341 }] }, to limit the devices shown in the picker. |

Return value

| Field | Type | Description | | --- | --- | --- | | info | Partial<SerialPortInfo> \| undefined | USB vendor/product IDs (usbVendorId, usbProductId) of the connected port, or undefined when disconnected. Memoized, so it is safe to use in dependency arrays. | | isConnected | boolean | Whether a port is currently open. Becomes false automatically if the device is unplugged. | | receivedData | T \| undefined | Latest value produced by onData (a string by default). | | ports | SerialPort[] | Ports the user has already granted access to. Kept in sync with the browser's connect / disconnect events. | | connectSerial | (options?: SerialOptions, grantedPort?: SerialPort) => void | Opens a port, defaulting to { baudRate: 9600 }. Without grantedPort it prompts the user to pick one (must be invoked from a user gesture); pass an entry of ports to reconnect without a prompt. If a port is already open it is closed first, so calling it again switches device or applies new options without isConnected / info ever going empty. A different port is opened before the current one is released, so the current connection is kept if the picker is dismissed or the new port fails to open. Reopening the same port has to close it first, and isConnected becomes false only if that reopen fails. Ignored while a connect/disconnect is in progress. | | disconnectSerial | () => void | Cancels the reader, discards writes that have not finished sending yet and closes the port cleanly. | | sendData | (message: Uint8Array) => void | Writes bytes to the device. Writes are queued and sent in order. No-op when the port is not writable. |

connectSerial, disconnectSerial and sendData keep the same identity between renders. The port is closed automatically when the component unmounts, and the hook is safe to render on the server (it does nothing where navigator is unavailable).

SerialOptions

Passed to port.open():

| Field | Type | Default | | --- | --- | --- | | baudRate | number | 9600 | | dataBits | number | — | | stopBits | number | — | | parity | 'none' \| 'even' \| 'odd' \| 'mark' \| 'space' | — | | bufferSize | number | — | | flowControl | 'none' \| 'hardware' | — |

Custom data handling

Pass an onData reducer to accumulate or transform incoming bytes. For example, to keep a running log of all decoded text:

const decoder = new TextDecoder()

const { receivedData } = useSerial<string>((data, prev = '') =>
  prev + decoder.decode(data),
)

Because the return type is generic, you can also build up structured state (e.g. parsing newline-delimited frames) by typing the hook accordingly: useSerial<string[]>(...).

Multiple devices

SerialProvider manages several devices at once, each identified by a string key. It renders no DOM of its own: every entry of devices gets its own useSerial connection, and useSerialDevice(key) reads it from anywhere below the provider.

import { SerialProvider, useSerialDevice } from '@a4smanjorg5/react-serial'

const parseWeight = (data?: Uint8Array, prev = 0) => prev + (data?.length ?? 0)

export function App() {
  return (
    <SerialProvider
      devices={{
        printer: {},
        scale: {
          onData: parseWeight,
          options: { baudRate: 115200, parity: 'even' },
          requestOptions: { filters: [{ usbVendorId: 0x2341 }] },
          onConnectionChange: ({ key, type, info }) =>
            console.log(key, type, info?.usbVendorId),
        },
      }}
    >
      <Scale />
    </SerialProvider>
  )
}

function Scale() {
  const { isConnected, receivedData, connectSerial, disconnectSerial } =
    useSerialDevice<number>('scale')

  return isConnected ? (
    <button onClick={disconnectSerial}>Disconnect ({receivedData})</button>
  ) : (
    // Uses the `options` of this device. Must be called from a user gesture
    <button onClick={() => connectSerial()}>Connect</button>
  )
}

SerialProvider

| Prop | Type | Description | | --- | --- | --- | | devices | Record<string, SerialDeviceConfig> | The devices to manage, keyed by their identifier. Use {} for a device with no configuration. | | children | ReactNode | Anything that calls useSerialDevice. |

Each device accepts:

| Field | Type | Description | | --- | --- | --- | | onData | (data?: Uint8Array, prev?: unknown) => unknown | Same reducer as the first argument of useSerial. Defaults to UTF-8 decoding. | | options | SerialOptions | Options this device is opened with by connectSerial(). Falls back to { baudRate: 9600 }. | | requestOptions | SerialPortRequestOptions | Same as the second argument of useSerial, e.g. to filter the picker by vendor. | | onConnectionChange | (event: SerialConnectionEvent) => void | Called when the device connects or disconnects. |

onConnectionChange receives { key, type: 'connect' | 'disconnect', info }. A disconnect is reported however it came about: disconnectSerial(), the device being unplugged, the stream ending by itself, the device being removed from devices, or the provider unmounting. In that case info is that of the port that was just released. It is not called on mount, nor when connectSerial switches to another port while staying connected. It always runs the latest callback, so it does not need to be memoized.

useSerialDevice

const { info, isConnected, receivedData, ports, connectSerial, disconnectSerial, sendData } =
  useSerialDevice<T = string>(key)

It returns the same shape as useSerial, bound to key, with one difference: connectSerial is (grantedPort?: SerialPort) => void and takes no SerialOptions, since those are set by options in the device config. Without grantedPort it prompts the user to pick a port (must be invoked from a user gesture); pass an entry of ports to reconnect without a prompt. connectSerial, disconnectSerial and sendData keep the same identity. A key that is not (yet) in devices gives an idle device (isConnected: false), and calling connectSerial or sendData on it logs an error and does nothing. It throws when used outside a SerialProvider.

Notes:

  • Removing a device from devices closes its port.
  • A device added to devices can be connected once the provider has rendered it, not from the same handler that added it.
  • A port can only be open once, so two keys cannot share it.
  • devices may hold functions, so in Next.js render SerialProvider from a client component.

Development

This package builds with tsup and lints/formats with Biome.

npm run build   # one-off build (ESM + CJS + type declarations) into dist/
npm run start   # build in watch mode
npm run lint    # checks code quality

License

MIT