2.5 KiB
provider/midi-context
Purpose
Defines the two React contexts that wire <MidiProvider> to its
descendant hooks. MidiReactContext carries the resolved
MidiContextValue — status, WebMIDIAccess, any caught error, the
sysex opt-in flag, and a monotonically-incrementing stateVersion
counter bumped on each Web MIDI statechange event. The presence of
stateVersion lets useSyncExternalStore-based hooks observe live
device plug/unplug changes without each hook holding its own
statechange listener.
MidiRequestContext is a per-tree override slot for the
navigator.requestMIDIAccess function. Tests provide a deterministic
in-memory implementation here; production renders leave it unset and
the provider falls back to the real global. This is a real injection
seam — not a mock — so the integration path remains exercised
verbatim under both real and fake transports.
Features
- Two distinct contexts so hooks that only need the resolved access
re-render only on
stateVersionbumps, not on injection changes. nullsentinel onMidiReactContexttriggersREACT_MIDI.NO_PROVIDERin every public hook.MidiStatusis the four-value lifecycle:loading,granted,denied,unsupported.
Usage: Basic
import { MidiReactContext } from "@suaveplan/react-midi";
import { useContext } from "react";
function StatusBadge() {
const ctx = useContext(MidiReactContext);
return <span>{ctx?.status ?? "no-provider"}</span>;
}
Usage: Advanced
import { MidiProvider, MidiRequestContext } from "@suaveplan/react-midi";
// Inject a deterministic fake in tests:
function fakeRequest() {
return Promise.resolve(fakeAccessShape);
}
<MidiRequestContext.Provider value={fakeRequest as never}>
<MidiProvider sysex>{children}</MidiProvider>
</MidiRequestContext.Provider>;
API Reference
MidiStatus = "loading" | "granted" | "denied" | "unsupported".MidiContextValue { status, access, error, sysex, stateVersion }.MidiReactContext: React.Context<MidiContextValue | null>.MidiRequestContext: React.Context<WebMIDIRequestFn | null>.
Implementation Notes
stateVersion exists because the Web MIDI statechange event fires
mutations on MIDIAccess.inputs / outputs in place — the underlying
Map identity does not change. useSyncExternalStore therefore needs
a sentinel value that does change to trigger a re-render; the
provider increments stateVersion inside the statechange callback.