Improve UX with a sound hook in your Typescript React application

Jakub Patočka - Jul 23 - - Dev Community

See how to implement a sound hook into your React Typescript application.

Subsequent samples are written in TypeScript (v5), Preact (v10) and Vite (v4), but the API notation is identical to React (v18).

Introduction

I’ve always been passionate about web development and decided to challenge myself with browser game development. Creating an engaging and interactive game required attention to many details, one of which was the implementation of sound in the game. This led me to create my own React/Preact audio hook called useAudio. It’s nothing difficult, but it’s not something a developer would deal with on a daily basis.

How it will be used

I knew I would need something where I could pass a list of audio files and play them one at a time. For example, I want to make a clinking sound when clicked on a button and whistle when there is an error. I imagined something like:

// Simple initialization of sound to individual events.
const sound = useAudio({ map: SFX_MAP });
const handleClick = () => sound.play('click');
const handleError = () => sound.play('error');
Enter fullscreen mode Exit fullscreen mode

I knew that I would also want to implement background music and I would not want to write another hook for it. So I imagined what other settings I might want to pass to the hook:

// This is how I would like to initialize the background music.
const music = useAudio({ 
    map: MUSIC_MAP, 
    volume: 0.25, 
    loop: true 
});

// Play on mount. Do not care about unmount.
useEffect(() => {
    music.play('ambient'); 
}, []);
Enter fullscreen mode Exit fullscreen mode

Writing a hook

I started by creating types. UseAudioMap for the sound map and UseAudoOptions for setting the hook.

// Audio map type.
type UseAudioMap = Record<string, HTMLAudioElement>;

// Audio settings interface.
interface UseAudoOptions {
    map: UseAudioMap, // Map of audio files.
    volume?: number, // Audio volume (0-1).
    loop?: boolean // Play once/infinite times.
}
Enter fullscreen mode Exit fullscreen mode

Then I created a map of the sounds that I would like to use in the application in the given situations.

// Map of SFX audio files.
const SFX_MAP = {
    click : new Audio('./click.mp3'),
    error: new Audio('./error.mp3'),
} satisfies UseAudioMap;

// Map of background music audio files.
const MUSIC_MAP = {
    ambient : new Audio('./ambient-music.mp3'),
    dramatic: new Audio('./dramatic-atmo.mp3'),
} satisfies UseAudioMap;
Enter fullscreen mode Exit fullscreen mode

Next, I wrote a concrete implementation of a simple hook for sound that solves at least basic use-cases.

/**
 * Creates controllable sound instance.
 * @returns { play, pause } functions
 */
const useAudio = ({ map, volume = 1, loop = false }: UseAudoOptions) => {
    const sound = useRef<HTMLAudioElement>();

    // Stop audio on unmount.
    useEffect(() => {
        return () => { pause(); }
    }, []);

    // Play sound per key.
    const play = (key: keyof typeof map) => {
        pause();
        sound.current = map[key];
        sound.current.load(); // reinitializes audio
        sound.current.volume = volume;
        sound.current.loop = loop;
        sound.current.play();
    }

    // Stop current audio.
    const pause = () => {
        sound.current?.pause();
    }

    // Return controls.
    return { play, pause }
}
Enter fullscreen mode Exit fullscreen mode

Now I can use the hook exactly as I imagined at the beginning.

const sound = useAudio({ map: SFX_MAP });
const music = useAudio({ 
    map: MUSIC_MAP, 
    volume: 0.25, 
    loop: true 
});
Enter fullscreen mode Exit fullscreen mode

Conclusion

Adding sound changed the game. Users said it was more immersive and fun. Although this is not a programmingly complex solution and could certainly be tweaked endlessly, I thought it was interesting to share the idea that the UI can make sounds on the web as well :).

.
Terabox Video Player