Files
2026-05-27 23:29:53 +01:00

3.1 KiB

@suaveplan/react-midi — Web MIDI API React adapter

  • Target path: packages/ui/react/react-midi/
  • Tier: browser/react
  • Date: 2026-05-23
  • Status: PROPOSED

Why

@suaveplan/midi ships universal-tier MIDI types and a MidiPortLike structural contract but never touches the platform. To consume MIDI in a browser app, React components need:

  1. Permission gating (navigator.requestMIDIAccess({ sysex })) with promise-shaped state for Suspense + ErrorBoundary integration.
  2. Port enumeration with live add/remove (devices plug/unplug at runtime).
  3. Per-port input/output hooks that wire into the universal decoder/encoder and surface typed MidiMessage events.
  4. SysEx opt-in (separate permission scope).
  5. Safe StrictMode behaviour (double-mount + cleanup must not double-bind).

This package owns the Web MIDI bridge so every downstream React surface (control-surface runtime, react-control-surface editor, react-audio-player's hardware-controller integration) shares one set of hooks instead of reimplementing permission/state plumbing.

What changes

  1. NEW package @suaveplan/react-midi at packages/ui/react/react-midi/.
  2. <MidiProvider> — context provider that triggers navigator.requestMIDIAccess exactly once (StrictMode-safe), exposes the MIDIAccess state, supports sysex: boolean prop, surfaces permission denial as a typed MidiPermissionError.
  3. Hooks:
    • useMidi() — returns { access, status: "loading"|"granted"|"denied"|"unsupported", inputs: MidiPortLike[], outputs: MidiPortLike[] }.
    • useMidiInput(portId | "first"): MidiPortLike | null — picks a single input; subscribers receive parsed MidiMessage events via port.onMessage.
    • useMidiOutput(portId | "first"): MidiPortLike | null — sends bytes via the universal encodeMessage.
    • useMidiMessages(portId, filter?) — returns the most recent N messages (ring buffer) filtered by message type. For UI display.
    • useMidiDevices() — returns the live { inputs, outputs } array, re-renders on statechange.
    • useNRPN(portId) / useRPN(portId) / useCC14(portId) — high-resolution-controller reducers exposed as React hooks.
  4. <MidiDeviceList> + <MidiActivityIndicator> — small UI primitives for picker / activity visualisation. Each is composable; consumers pass their own renderProps for custom styling.
  5. Connect-state events — fires onDeviceAdd / onDeviceRemove via context, plus per-port state reads.
  6. Error codes — REACT_MIDI.{ACCESS_DENIED, UNSUPPORTED_PLATFORM, PORT_NOT_FOUND, SYSEX_NOT_PERMITTED} via defineCodes.
  7. OTel instrumentation — counter react_midi_messages_received_total{port}, counter react_midi_messages_sent_total{port}.

Impact

  • New package depending on @suaveplan/midi@workspace:^ and React 19 peer dep.
  • Browser-tier — uses navigator.requestMIDIAccess. Falls back gracefully (status: "unsupported") on platforms that lack Web MIDI (Safari without flag, Firefox, sandboxed contexts).
  • Unblocks @suaveplan/react-control-surface (CC bindings drive plugin parameters via useParameter from @suaveplan/react-audio).