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
SourceRequests 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
SourceHandles 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)
},
})
| handler | arguments |
|---|---|
onNoteOnEvent | note, velocity (1–127), channel |
onNoteOffEvent | note, channel |
onControlChangeEvent | controller, value (0–127), channel |
onProgramChangeEvent | program, channel |
onPitchBendEvent | value (0–16383), channel |
onAftertouchEvent | note, pressure, channel |
onChannelPressureEvent | pressure, 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 0xf0–0xff — carry no
channel and are not decoded here. Use useMIDIMessage for those.
useMIDIMessage
SourceThe raw midimessage events, for when you need more detail than
useMIDIInput gives.
useMIDIMessage(midiAccess, (event) => {
console.log([...event.data])
})