First of all, sorry for my english, it's not my first langage. Also I'm pretty new to coding so please be patient (and please be as clear as if you where talking to a rubber duck XD).
I'm trying to display mp3 from Hygraphs assets I uploaded. I want to map them so that if anybody add a file later, a new audioplayer will appear on my page.
The thing is my players are all playing the same mp3 even if I have several url, and/or I have errors such as
index.js:41 Uncaught TypeError: Cannot read properties of undefined (reading 'duration')
I think it's because of refs or something like that, but I don't know how to fix it at all.
My component:
function OnePodcast({ posts }) {
const listOfPodcasts = posts.filter((post) => post.node.categories.some((category) => category.nom === 'Podcasts'));
// State
const [isPlaying, setIsPlaying] = useState(false);
const [currentTime, setCurrentTime] = useState(0);
const [duration, setDuration] = useState(0);
// References
const audioPlayer = useRef(); // reference the audioplayer component
const progressBar = useRef(); // reference the progress bar
const animationRef = useRef(); // reference the animation
const onLoadedMetadata = () => {
setDuration(Math.floor(audioPlayer.current.duration));
};
useEffect(() => {
const seconds = Math.floor(audioPlayer.current.duration);
setDuration(seconds);
progressBar.current.max = seconds;
}, [audioPlayer?.current?.loadedmetadata, audioPlayer?.current?.readyState]);
const calculateTime = (secs) => {
const minutes = Math.floor(secs / 60);
const returnedMinutes = minutes < 10 ? `0${minutes}` : `${minutes}`;
const seconds = Math.floor(secs % 60);
const returnedSeconds = seconds < 10 ? `0${seconds}` : `${seconds}`;
return `${returnedMinutes}:${returnedSeconds}`;
};
const togglePlayPause = () => {
const prevValue = isPlaying;
setIsPlaying(!prevValue);
if (!prevValue) {
audioPlayer.current.play();
animationRef.current = requestAnimationFrame(whilePlaying);
}
else {
audioPlayer.current.pause();
cancelAnimationFrame(animationRef.current);
}
};
const whilePlaying = () => {
progressBar.current.value = audioPlayer.current.currentTime;
changePlayerCurrentTime();
animationRef.current = requestAnimationFrame(whilePlaying);
};
const changeRange = () => {
audioPlayer.current.currentTime = progressBar.current.value;
changePlayerCurrentTime();
};
const changePlayerCurrentTime = () => {
progressBar.current.style.setProperty('--seek-before-width', `${progressBar.current.value / duration * 100}%`);
setCurrentTime(progressBar.current.value);
};
const backThirty = () => {
progressBar.current.value = Number(progressBar.current.value - 30);
changeRange();
};
const forwardThirty = () => {
const newTime = audioPlayer.current.currentTime + 30;
// Needed this to fix the wrong number of secs added to the player
if (newTime <= duration) {
audioPlayer.current.currentTime = newTime;
progressBar.current.value = newTime;
changePlayerCurrentTime();
}
};
return (
<Container>
{listOfPodcasts.map((podcast) => (
<section className="onePodcast">
<div className="audioPlayers-block">
<article className="audioPlayer">
<h3 className="header">{podcast.node.titre}</h3>
<RichText
content={podcast.node.contenu.raw}
/>
<div className="audioPlayer__player">
{/* Map to display assets files */}
{podcast.node.fichier && podcast.node.fichier.map((fichier) => (
<audio ref={audioPlayer} src={fichier.url} preload="metadata" onLoadedData={onLoadedMetadata} />
))}
{/* Buttons for desktop */}
<div className="audioPlayer__player-btn displayNoneMobile">
<button type="button" onClick={backThirty} className="audioPlayer__btn"><TbPlayerTrackPrevFilled /> </button>
<button type="button" onClick={togglePlayPause} className="audioPlayer__main-btn">
{isPlaying ? <TbPlayerPauseFilled /> : <TbPlayerPlayFilled /> }
</button>
<button type="button" onClick={forwardThirty} className="audioPlayer__btn"><TbPlayerTrackNextFilled /> </button>
</div>
<div className="audioPlayer__player-bar">
{/* current time */}
<div className="audioPlayer__currentTime">{calculateTime(currentTime)}</div>
{/* Progress bar */}
<div>
<input type="range" className="audioPlayer__progressBar" defaultValue="0" ref={progressBar} onChange={changeRange} />
</div>
{/* duration */}
<div className="audioPlayer__duration">{(duration && !Number.isNaN(duration)) && calculateTime(duration)}</div>
</div>
{/* Buttons for mobile */}
<div className="audioPlayer__player-btn displayNoneDesktop">
<button type="button" onClick={backThirty} className="audioPlayer__btn"><TbPlayerTrackPrevFilled /> </button>
<button type="button" onClick={togglePlayPause} className="audioPlayer__main-btn">
{isPlaying ? <TbPlayerPauseFilled /> : <TbPlayerPlayFilled /> }
</button>
<button type="button" onClick={forwardThirty} className="audioPlayer__btn"><TbPlayerTrackNextFilled /> </button>
</div>
</div>
</article>
</div>
</section>
))}
</Container>
);
}
To be honest, I asked ChatGPT for help since I couldn't find anything online and here is the last code it proposed (with the same error "Uncaught TypeError: Cannot read properties of undefined (reading 'duration')"). Every other solutions it proposed were wrong the same way with a different error in "Cannot read properties of undefined (reading 'XXX').
Thank you soooo much for your help!
function OnePodcast({ posts }) {
const listOfPodcasts = posts.filter((post) => post.node.categories.some((category) => category.nom === 'Podcasts'));
// State
const [isPlaying, setIsPlaying] = useState(false);
const [currentTime, setCurrentTime] = useState(0);
const [duration, setDuration] = useState(0);
// References
const audioPlayer = useRef(listOfPodcasts.map(() => createRef())); // reference the audioplayer component
const progressBar = useRef(listOfPodcasts.map(() => createRef())); // reference the progress bar
const animationRef = useRef(listOfPodcasts.map(() => null)); // initialize with null values
const onLoadedMetadata = (index) => {
setDuration(Math.floor(audioPlayer.current[index].current.duration));
};
useEffect(() => {
const seconds = Math.floor(audioPlayer.current[0].current.duration);
setDuration(seconds);
progressBar.current[0].max = seconds;
}, [audioPlayer?.current?.[0]?.loadedmetadata, audioPlayer?.current?.[0]?.readyState]);
const calculateTime = (secs) => {
const minutes = Math.floor(secs / 60);
const returnedMinutes = minutes < 10 ? `0${minutes}` : `${minutes}`;
const seconds = Math.floor(secs % 60);
const returnedSeconds = seconds < 10 ? `0${seconds}` : `${seconds}`;
return `${returnedMinutes}:${returnedSeconds}`;
};
const togglePlayPause = (index) => {
const prevValue = isPlaying;
setIsPlaying(!prevValue);
const currentPlayer = audioPlayer.current[index].current;
if (!prevValue) {
currentPlayer.play();
animationRef.current[index] = requestAnimationFrame(() => whilePlaying(index));
} else {
currentPlayer.pause();
cancelAnimationFrame(animationRef.current[index]);
}
};
const whilePlaying = (index) => {
progressBar.current[index].current.value = audioPlayer.current[index].current.currentTime;
changePlayerCurrentTime(index);
animationRef.current[index] = requestAnimationFrame(() => whilePlaying(index));
};
const changeRange = (index) => {
audioPlayer.current[index].current.currentTime = progressBar.current[index].current.value;
changePlayerCurrentTime(index);
};
const changePlayerCurrentTime = (index) => {
progressBar.current[index].current.style.setProperty('--seek-before-width', `${progressBar.current[index].current.value / duration * 100}%`);
setCurrentTime(progressBar.current[index].current.value);
};
const backThirty = (index) => {
progressBar.current[index].current.value = Number(progressBar.current[index].current.value - 30);
changeRange(index);
};
const forwardThirty = (index) => {
const newTime = audioPlayer.current[index].current.currentTime + 30;
if (newTime <= duration) {
audioPlayer.current[index].current.currentTime = newTime;
progressBar.current[index].current.value = newTime;
changePlayerCurrentTime(index);
}
};
return (
<Container>
{listOfPodcasts.map((podcast, index) => (
<section className="onePodcast" key={index}>
<div className="audioPlayers-block">
<article className="audioPlayer">
<h3 className="header">{podcast.node.titre}</h3>
<RichText content={podcast.node.contenu.raw} />
<div className="audioPlayer__player">
{/* map to display assets' files */}
{podcast.node.fichier &&
podcast.node.fichier.map((fichier, index) => (
<audio
key={index}
ref={audioPlayer.current[index]}
src={fichier.url}
preload="metadata"
onLoadedData={() => onLoadedMetadata(index)}
/>
))}
{/* desktop btn */}
<div className="audioPlayer__player-btn displayNoneMobile">
<button type="button" onClick={() => backThirty(index)} className="audioPlayer__btn">
<TbPlayerTrackPrevFilled />{' '}
</button>
<button
type="button"
onClick={() => togglePlayPause(index)}
className="audioPlayer__main-btn"
>
{isPlaying ? <TbPlayerPauseFilled /> : <TbPlayerPlayFilled />}
</button>
<button type="button" onClick={() => forwardThirty(index)} className="audioPlayer__btn">
<TbPlayerTrackNextFilled />{' '}
</button>
</div>
<div className="audioPlayer__player-bar">
{/* current time */}
<div className="audioPlayer__currentTime">{calculateTime(currentTime)}</div>
{/* Barre de progression */}
<div>
<input
type="range"
className="audioPlayer__progressBar"
defaultValue="0"
ref={progressBar.current[index]}
onChange={() => changeRange(index)}
/>
</div>
{/* duration */}
<div className="audioPlayer__duration">
{(duration && !Number.isNaN(duration)) && calculateTime(duration)}
</div>
</div>
{/* mobile btn */}
<div className="audioPlayer__player-btn displayNoneDesktop">
<button type="button" onClick={() => backThirty(index)} className="audioPlayer__btn">
<TbPlayerTrackPrevFilled />{' '}
</button>
<button
type="button"
onClick={() => togglePlayPause(index)}
className="audioPlayer__main-btn"
>
{isPlaying ? <TbPlayerPauseFilled /> : <TbPlayerPlayFilled />}
</button>
<button type="button" onClick={() => forwardThirty(index)} className="audioPlayer__btn">
<TbPlayerTrackNextFilled />{' '}
</button>
</div>
</div>
</article>
</div>
</section>
))}
</Container>
);
}
The bug in your React component arises because you're trying to use a single ref for multiple audio players. Refs in React are meant to reference a single DOM element, and when you use the same ref for a map function, it will only point to the last element in the map. Therefore, all controls end up controlling the last audio player, causing the behavior you're experiencing.
Try something like this:
This is a high-level example, and you will need to adjust your event handlers like
onLoadedMetadata
,whilePlaying
,changeRange
, etc., to accept the index parameter and use it to manipulate the state and refs of the correct audio player.You would also need to make sure that you're updating the state correctly in an immutable way, which I've demonstrated in the
togglePlayPause
function with the spread operator[...players]
. This ensures that you're not mutating the state directly but creating a new state array that React can use to determine what needs to be re-rendered.