Awaiting an arbitrary trigger in JavaScript/React
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:
const {
startRecording,
stopRecording,
getAudioData,
// etc
} = useAudioRecorder();
The hook would set up a MediaRecorder
instance
and startRecording
and stopRecording
would call start
and
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
, 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:
import { useMemo, useRef } from "react";
export function useAwaitTrigger<T = void>(): {
wait: () => Promise<T>;
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<T>((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).