Skip to main content

Hooks for Web MIDI API

Three hooks, one on top of the next. useMIDIAccess asks the browser for permission, and the other two listen to whatever it hands back.

All of them follow the devices: a keyboard plugged in after permission was granted starts working on its own, with nothing to rebuild.

useMIDIAccess

Source

Requests MIDI access in the browser. The first argument chooses whether to ask on mount; pass false to ask later, from a button the user presses.

const { request, midiAccess, error, inputs } = useMIDIAccess(false)

return (
<>
{midiAccess ? null : <button onClick={() => request()}>Connect MIDI</button>}
{error && <p>{error}</p>}
<ul>
{inputs.map((input) => (
<li key={input.id}>{input.name}</li>
))}
</ul>
</>
)

inputs is the list of connected devices, and it changes as they come and go, so a device list needs no statechange listener of its own.

error is one of NOT_SUPPORTED (the browser has no Web MIDI API), PERMISSION_DENIED (the user said no — asking again is worthwhile) or UNAVAILABLE (anything else). A failed request leaves any access already granted in place.

Pass { sysex: true } to request for system exclusive messages. Browsers treat that as a separate, more sensitive permission, so ask for it only when the app actually reads or sends sysex.

useMIDIInput

Source

Handles the channel voice messages of every connected input. Handlers are given as an object, and every one of them is given the channel last, as 0–15 (MIDI channels are written 1–16 on hardware, so add one before showing it to anyone).

useMIDIInput(midiAccess, {
onNoteOnEvent: (note, velocity, channel) => play(note, velocity / 127),
onNoteOffEvent: (note, channel) => stop(note),
onControlChangeEvent: (controller, value) => {
if (controller === 1) setModulation(value / 127)
},
onPitchBendEvent: (value) => {
// 0-16383, centred at PITCH_BEND_CENTER (8192)
setBend((value - PITCH_BEND_CENTER) / PITCH_BEND_CENTER)
},
})
handlerarguments
onNoteOnEventnote, velocity (1–127), channel
onNoteOffEventnote, channel
onControlChangeEventcontroller, value (0–127), channel
onProgramChangeEventprogram, channel
onPitchBendEventvalue (0–16383), channel
onAftertouchEventnote, pressure, channel
onChannelPressureEventpressure, channel

A note on with a velocity of 0 is how most devices say note off, so it arrives as onNoteOffEvent.

Writing the handlers inline is fine: they are read fresh on every event, so a re-render never detaches anything.

System messages — clock, sysex, and the rest of 0xf00xff — carry no channel and are not decoded here. Use useMIDIMessage for those.

useMIDIMessage

Source

The raw midimessage events, for when you need more detail than useMIDIInput gives.

useMIDIMessage(midiAccess, (event) => {
console.log([...event.data])
})