10 KiB
@suaveplan/react-midi - React 19 adapter for the Web MIDI API: , hooks (useMidi, useMidiInput, useMidiOutput, useMidiMessages, useMidiDevices, useNRPN, useRPN, useCC14), and small UI primitives (MidiDeviceList, MidiActivityIndicator).
React 19 adapter for the Web MIDI API. Provides a strict-mode-safe
<MidiProvider> that requests MIDIAccess exactly once, seven
typed hooks for common consumption patterns, and two small UI
primitives (<MidiDeviceList>, <MidiActivityIndicator>).
Documentation tree
- midi-activity-indicator —
<MidiActivityIndicator>is a small visual primitive that "blinks" on inbound MIDI traffic. It subscribes to the selec…- midi-activity-indicator —
<MidiActivityIndicator>is a small visual primitive that "blinks" on inbound MIDI traffic. It subscribes to the selec…
- midi-activity-indicator —
- midi-device-list —
<MidiDeviceList>is a small, accessible list of the available MIDI devices for a chosen kind (inputs or outputs). It…- midi-device-list —
<MidiDeviceList>is a small, accessible list of the available MIDI devices for a chosen kind (inputs or outputs). It…
- midi-device-list —
- error-codes — Defines the stable
REACT_MIDI.*error-code namespace published by@suaveplan/react-midi. Every code is generated vi…- error-codes — Defines the stable
REACT_MIDI.*error-code namespace published by@suaveplan/react-midi. Every code is generated vi…
- error-codes — Defines the stable
- hooks — Internal helper that returns the live
MidiContextValuefor the nearest<MidiProvider>ancestor. It throws `ReactMid…- use-cc14 —
useCC14(selector)exposes the consolidated 14-bit CC change for the selected input. Controllers that need finer resol…- use-cc14 —
useCC14(selector)exposes the consolidated 14-bit CC change for the selected input. Controllers that need finer resol…
- use-cc14 —
- use-midi —
useMidi()is the most general hook in@suaveplan/react-midi. It returns the liveMidiContextValuesurface as `{ s…- use-midi —
useMidi()is the most general hook in@suaveplan/react-midi. It returns the liveMidiContextValuesurface as `{ s…
- use-midi —
- use-midi-devices —
useMidiDevices()returns the live{ inputs, outputs }device-list view, re-rendering whenever the provider's `state…- use-midi-devices —
useMidiDevices()returns the live{ inputs, outputs }device-list view, re-rendering whenever the provider's `state…
- use-midi-devices —
- use-midi-input —
useMidiInput(selector)returns a typedMidiPortLikeadapter over a single Web MIDI input. The adapter parses raw by…- use-midi-input —
useMidiInput(selector)returns a typedMidiPortLikeadapter over a single Web MIDI input. The adapter parses raw by…
- use-midi-input —
- use-midi-messages —
useMidiMessages(selector, options?)exposes a bounded ring buffer of the most recent parsed MIDI messages from the se…- use-midi-messages —
useMidiMessages(selector, options?)exposes a bounded ring buffer of the most recent parsed MIDI messages from the se…
- use-midi-messages —
- use-midi-output —
useMidiOutput(selector)returns a typed handle for a single Web MIDI output. The handle exposes bothsend(bytes)(r…- use-midi-output —
useMidiOutput(selector)returns a typed handle for a single Web MIDI output. The handle exposes bothsend(bytes)(r…
- use-midi-output —
- use-nrpn —
useNRPN(selector)wraps the universal NRPN reducer from themidi-contractmodule and exposes the consolidated `NRPN…- use-nrpn —
useNRPN(selector)wraps the universal NRPN reducer from themidi-contractmodule and exposes the consolidated `NRPN…
- use-nrpn —
- use-rpn —
useRPN(selector)wraps the universal RPN reducer and exposes the consolidatedRPNChangeas React state. RPN ("Regis…- use-rpn —
useRPN(selector)wraps the universal RPN reducer and exposes the consolidatedRPNChangeas React state. RPN ("Regis…
- use-rpn —
- use-midi-context — Internal helper that returns the live
MidiContextValuefor the nearest<MidiProvider>ancestor. It throws `ReactMid…
- use-cc14 —
- codec —
codec.tsprovides minimaldecodeMessage/encodeMessagehelpers for the MIDI 1.0 wire format used internally by `… - midi-message — Defines the local mirror of the
@suaveplan/midiMidiMessagediscriminated union plus its peer types (Channel, `Mi… - reducers — Three small reducers that fold the four-CC NRPN, RPN, and 14-bit CC "dances" into single consolidated events. The Web M…
- port — Bridges the native Web MIDI
WebMIDIInput/WebMIDIOutputshapes into the structuralMidiPortLikeconsumed by the…- web-midi-port — Bridges the native Web MIDI
WebMIDIInput/WebMIDIOutputshapes into the structuralMidiPortLikeconsumed by the…
- web-midi-port — Bridges the native Web MIDI
- midi-context — Defines the two React contexts that wire
<MidiProvider>to its descendant hooks.MidiReactContextcarries the resol… - midi-provider —
<MidiProvider>is the root context provider for@suaveplan/react-midi. It calls `navigator.requestMIDIAccess({ syse… - telemetry — Owns the two OTel counters published by
@suaveplan/react-midi:react_midi_messages_received_totalfor parsed inboun…- telemetry — Owns the two OTel counters published by
@suaveplan/react-midi:react_midi_messages_received_totalfor parsed inboun…
- telemetry — Owns the two OTel counters published by
- test-setup — Per-package Vitest bootstrap loaded via
setupFilesbefore the first test in a worker runs. Its only job is to registe… - test-utils — Provides the deterministic in-memory implementations of the Web MIDI shape used throughout the
@suaveplan/react-midi…
Install
bun add @suaveplan/react-midi
Peer deps: react@^19, react-dom@^19.
Permission model
Web MIDI is gated behind navigator.requestMIDIAccess. <MidiProvider>
calls it once on mount and surfaces the outcome as a status:
loading— request in flight.granted—MIDIAccessresolved.denied— user rejected, or the runtime threw (e.g.NotAllowedError).unsupported—navigator.requestMIDIAccessis undefined (Firefox, sandboxed iframes, etc.).
SysEx access is opt-in via <MidiProvider sysex>. The provider is
StrictMode-safe — it tracks an in-flight ref so React's double-mount
never produces two parallel requests.
Quickstart
import {
MidiProvider,
useMidi,
useMidiInput,
MidiDeviceList,
} from "@suaveplan/react-midi";
function Workspace() {
const { status } = useMidi();
if (status === "loading") return <p>Connecting MIDI…</p>;
if (status === "denied") return <p>MIDI permission denied.</p>;
if (status === "unsupported")
return <p>MIDI not available in this browser.</p>;
return (
<>
<MidiDeviceList kind="input" />
<NoteLogger />
</>
);
}
function NoteLogger() {
const port = useMidiInput("first");
useEffect(() => {
if (port === null) return;
return port.onMessage((msg) => {
if (msg.type === "noteOn") console.log("note", msg.note);
});
}, [port]);
return null;
}
export default function App() {
return (
<MidiProvider>
<Workspace />
</MidiProvider>
);
}
Device-picker example
function DevicePicker() {
const [id, setId] = useState<string | null>(null);
return (
<>
<MidiDeviceList
kind="input"
selectedId={id}
onSelect={setId}
/>
{id !== null ? <NoteLogger portId={id} /> : null}
</>
);
}
Hooks reference
useMidi()—{ status, access, inputs, outputs, error }.useMidiInput(id | "first")— typedMidiPortLikewith parsedMidiMessageevents.useMidiOutput(id | "first")— typed output handle withsendMessage(msg)(encoded) and rawsend(bytes).useMidiMessages(id, { capacity?, filter? })— ring buffer of recent messages for log/debug UIs.useMidiDevices()—{ inputs, outputs }, re-renders onstatechange.useNRPN(id),useRPN(id),useCC14(id)— high-resolution reducers folded into React state.
Error codes
REACT_MIDI.{ACCESS_DENIED, UNSUPPORTED_PLATFORM, PORT_NOT_FOUND, SYSEX_NOT_PERMITTED, NO_PROVIDER}. Thrown as ReactMidiError
instances; permission failures are surfaced as a status value
rather than thrown so consumers can render friendly fallbacks.
Telemetry
Two OTel counters tagged with port:
react_midi_messages_received_total{port} and
react_midi_messages_sent_total{port}.
On the @suaveplan/midi dependency
This package ships a local mirror of the @suaveplan/midi
MidiMessage contract in src/midi-contract/. Once
@suaveplan/midi graduates to workspace:^, the local mirror is
replaced by a re-export from that package without any shape change.