--- title: "Awaiting an arbitrary trigger in JavaScript/React" status: published date: 2025-07-07T09:11:39+0100 --- ## Context I was recently working on a little web-based audio recorder tool. To abstract away all the code dealing with the `MediaRecorder` API, I made a React hook that you could use like this: ```tsx const { startRecording, stopRecording, getAudioData, // etc } = useAudioRecorder(); ``` The hook would set up a [`MediaRecorder`][mediarecorder] instance and `startRecording` and `stopRecording` would call [`start`][mr-start] and [`stop`][mr-stop]. Calling `start(timeslice)` will cause a `dataavailable` event to be triggered every `timeslice` milliseconds. I then had an event listener that adds the new data `Blob` to an array in a `useRef`. So far, everything was pretty straight-forward. Next, I wanted `getAudioData` to give me all the audio recorded thus far, but `MediaRecorder` doesn't have a method for that. The easy solution would be to return whatever data is currently stored in the audio chunks ref and set the `timeslice` value to something low, like 100ms. Then, when you call `getAudioData`, the data you get is at most 100ms out of date. But this also means you have an event handler pointlessly firing every 100ms. Fortunately, there is a method called [`requestData`][mr-request], but all it does is queue up another `dataavailable` event. It does not, itself, return the requested data, or tell you when the `dataavailable` event has been handled. ## The solution So I wanted a way to call `requestData` on my `MediaRecorder` and `await` until my event handler fired and updated the audio chunks ref with the latest data. The solution I picked was to create a new `Promise`, "steal" its `resolve` function from the callback passed into the constructor, and store it in a `useRef`. As long as the ref is in scope for the event handler, the event handler can now resolve our promise. This is what it looks like when you put it all in its own hook: ```ts import { useMemo, useRef } from "react"; export function useAwaitTrigger(): { wait: () => Promise; resolve: (val: T) => void; } { const resolveRef = useRef<(val: T) => void | null>(null); return useMemo( () => ({ wait: async () => { let resolve: (val: T) => void; const promise = new Promise((r) => { resolve = r; }); resolveRef.current = resolve!; return promise; }, resolve: (val: T) => { if (resolveRef.current) { const resolve = resolveRef.current; resolveRef.current = null; resolve(val); } }, }), [] ); } ``` You can use this hook for all sorts of things, like `await`ing an onclick event on a button. This same general technique also works to get an `async function*` from an event listener (which, idk, might be useful for some reason). [mediarecorder]: https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder [mr-start]: https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder/start [mr-stop]: https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder/stop [mr-request]: https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder/requestData