Reputation: 4839
I have the following functional React component that is using wavesurfer.js
import * as React from "react"
import * as WaveSurfer from 'wavesurfer.js'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faPlayCircle, faPauseCircle } from '@fortawesome/free-regular-svg-icons'
import { IconDefinition } from '@fortawesome/fontawesome-svg-core'
interface WaveformPlayerProps {
audioUrl: string
}
const WaveformPlayer = (props: WaveformPlayerProps) => {
const [playing, setPlaying] = React.useState(false)
const [audioContext, setAudioContext] = React.useState(new AudioContext())
const [waveform, setWaveform] = React.useState(null)
const waveformRef = React.useRef();
React.useEffect(() => {
if (waveformRef.current) {
const wavesurfer = WaveSurfer.create({
audioContext: audioContext,
container: waveformRef.current
});
wavesurfer.on('finish', togglePlayPause)
wavesurfer.load(props.audioUrl)
setWaveform(wavesurfer)
}
}, [])
function togglePlayPause(): void {
if (audioContext.state == "suspended") {
audioContext.resume()
}
waveform.playPause()
setPlaying(!playing)
}
function stateButtonIcon(): IconDefinition {
if (playing) {
return faPauseCircle
} else {
return faPlayCircle
}
}
return (
<div>
<button id="waveformAudioControl" className="button is-large" onClick={togglePlayPause}>
<FontAwesomeIcon icon={stateButtonIcon()} />
</button>
<div ref={waveformRef}>
</div>
</div>
)
}
export default WaveformPlayer
I think the only way I can access a WaveSurfer object outside of useEffect
is by using useState
however this doesn’t feel right to me as the WaveSurfer component shouldn’t completely re-render if some state changes in the component (e.g. user presses play/pause button). This leads me to think that my approach is not correct?
Edit: forgot to mention that waveform.playPause()
fails because it is null when the play button is clicked.
Upvotes: 1
Views: 2560
Reputation: 31
import React, { Fragment, useState, useEffect } from 'react'
import WaveSurfer from 'wavesurfer.js'
import audioFile from './../assets/sample-wave-file-5MB.wav'
const WaveSurferStatelessForm = () => {
let [isPlaying, setIsPlaying] = useState(false)
let [waveSurfer, setWaveSurfer] = useState(null)
useEffect(() => {
setWaveSurfer(WaveSurfer.create({
container: '#waveform'
}))
}, [])
useEffect(() => {
if(waveSurfer) {
waveSurfer.load(audioFile)
}
}, [waveSurfer])
const togglePlayPause = () => {
waveSurfer.playPause()
setIsPlaying(!isPlaying)
}
return (
<Fragment>
<div id="waveform" ></div>
<button onClick={() => togglePlayPause()}>{isPlaying ? '||' : '+'}</button>
</Fragment>
)
}
export default WaveSurferStatelessForm
Your waveform.playPause()
fails after clicking the play button because you are updating state, playing
, which will cause your component to re-render. Since your useEffect runs only on the first render, once the state changes your waveform will be set to null and useEffect will not re-instantiate it when the component re-renders. To solve this you need to either set the waveform to state, so it is always instantiated, or add the playing
variable inside the []
so useEffect will re-instantiate the waveform each time the component re-renders. My solution above uses the former with a detailed explanation below.
Set the waveSurfer object after the first render, this way the <div id="waveform" ></div>
will be mounted to the DOM before you create the waveSurfer object.
useEffect(() => {
setWaveSurfer(WaveSurfer.create({
container: '#waveform'
}))
}, [])
Now that the waveSurfer object is set, we need to load the audio. This useEffect
will run twice, but only load the audio once the waveSurfer object has been set, which will be the last time this useEffect
is called. Prior to this, the waveSurfer object is null.
useEffect(() => {
if(waveSurfer) {
waveSurfer.load(audioFile)
}
}, [waveSurfer])
As a note, useEffect
will always run after the first render, then on any state change passed to the []
. If there are no []
it will run after every render. So you don't need to check if the div exists if (waveformRef.current)
it will always exist because useEffect
is executing after the first render. In a statefull or class
component this is similar to componentDidMount
Upvotes: 1
Reputation: 4839
The best I could come up with is declaring const waveform = React.useRef<WaveSurfer | null>(null)
and using waveform.current
in the rest of the file.
Upvotes: 0