Using react-query for all kind of async logic
Using @tanstack/react-query for REST calls performed by axios is a well known pattern. But you can use it for all kind of async logic. Here I will present how to use it with the browser speech synthesis API.
To make it easier to use the browser speech synthesis API I use the easy-speech library that abstracts away some complexity and browser diffrences.
First thing we need to do is to put the easy-speech speak method into use. Read the easy-speech documentation for how to init and use since this is out of scope for this blog post.
SpeechService.ts
import EasySpeech, { SpeechSynthesisVoice } from 'easy-speech';
export interface SpeakParams {
text: string;
voice: SpeechSynthesisVoice;
}
const speak = ({ text, voice }: SpeakParams): Promise<void> => {
console.log('About to say:', text, ', using voice:', voice, 'of type', typeof voice);
return new Promise<void>((resolve, reject) => {
EasySpeech.speak({
text: text,
voice,
pitch: 1,
rate: 1,
volume: 1,
// there are more events, see the API for supported events
//boundary: (e: any) => console.debug('boundary reached', e)
})
.then(() => {
console.debug('Done saying:', text, ', using voice:', voice);
resolve();
})
.catch((error: any) => {
console.error('Failed saying:', text, ', using voice:', voice, error);
reject(error);
});
});
};
...
const SpeechService = {
speak,
...
};
export default SpeechService;
Now lets connect this async speak method with react-query
useSpeak.ts
import { useMutation } from "@tanstack/react-query";
import SpeechService, { SpeakParams } from "../services/SpeechService";
interface UseSpeakParams {
onSuccess?: () => void;
onError?: (error: Error) => void;
}
const performSpeak = async (params: SpeakParams) => {
try {
return await SpeechService.speak(params);
} catch (error: any) {
throw error;
}
};
export const useSpeak = ({onSuccess = () => {},
onError = () => {},
}: UseSpeakParams) => {
const mutator = useMutation({mutationFn: performSpeak,
onSuccess: () => {
onSuccess();
},
onError: (error: Error) => {
onError(error);
},
retry: false,
});
return [
mutator.mutate,
{ isSaving: mutator.isPending, ...mutator },
] as const;
};
And finally lets put the useSpeak hook into work in a react component.
SayHelloComponent.tsx
import React from "react";
import { useSpeak } from "../hooks/useSpeak";
import { SpeechSynthesisVoice } from "easy-speech";
import Select, { SingleValue } from "react-select";
import { useVoices } from "../hooks/useVoices";
import { SpeechSynthesisVoiceData } from "../services/SpeechService";
export const SayHelloComponent = () => {
const [text, setText] = React.useState<string>("Hi there! Are you ready?");
const [voiceData, setVoiceData] = React.useState<
SpeechSynthesisVoiceData | undefined
>();
const [availableVoices, {isUseVoicesError, useVoicesError}] = useVoices();
const [speak] = useSpeak({
onError: (error: Error) => {
console.error("Failed speak:", error);
},
});
React.useEffect(() => {
if (isUseVoicesError && useVoicesError) {
console.log("Failed detect voices", useVoicesError);
}
}, [isUseVoicesError, useVoicesError]);
const speech = () => {
if (voiceData) {
speak({ text, voice: voiceData.voice });
}
};
return (
<div style=>
<Select
id="language"
value={voiceData}
options={availableVoices as any}
onChange={(value: SingleValue<SpeechSynthesisVoiceData>) => {
if (value && value.voice) {
setVoiceData(value);
}
}}
/>
<input
type="text"
value={text}
style=
onChange={(event) => {
setText(event?.target?.value || "");
}}
></input>
<button
style=
disabled={
!voiceData || !availableVoices || availableVoices.length === 0
}
onClick={() => {
speech();
}}
>
Speak
</button>
</div>
);
};
export default SayHelloComponent;
Source code this blog post is based on could be found here.