❀✿❀ SuperLaserNino ✿❀✿

Awaiting an arbitrary trigger in JavaScript/React

7 July 2025

445 words

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 awaiting 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).