При каждом ре-рендере обновляется Audio()
В продессе изучения ReactJS и TypeScript. Пишу плеер, что должен проигрывать музыку из File (приходит через проп от компонента App, а туда из input[type="file"] проходя через FileReader). В процессе разработки сталкивался с проблемами типа удвоение-утроение... и на каждый рендер размножение дорожек или play() не может быть вызван из-за pause()... Нашёл неплохой код на английском stackoverflow с созданием кастомного хука. Есть только проблема - на каждое нажатие кнопки play/pause создаётся новый инстанс Audio() или переуказание src я так понял. Play - играет, а Pause останавливает и при следующем нажатии Play трек играет заново.
Уже и пробовал выносить сам функционал плеера в App.tsx и через проп из плеера брать состояние кнопки (не по приниципам реакта, похоже это и хотелось бы этого избежать). Пробовал не хранить Audio в стейте. Пробовал audio.src в useEffect(() => {}, []); вставялть, но на тот момент оно null и браузер ругается DOMExeption'ом, что источник не валидный.
Хотелось бы ответ развёрнутый, что я не так делаю и как это лучше реализовать. (Использую react 17.0.2, typescript 4.3.4)
Код компонета App.tsx:
import React, { useState, useEffect, useRef } from 'react';
import './App.css';
import CoverBG from "./App/CoverBG";
import PlayList from "./App/PlayList";
import MusicPlayer from "./App/MusicPlayer";
import { IAudio } from './App/interfaces';
import jsmediatags from 'jsmediatags';
const App: React.FC = () => {
const [track, setTrack] = useState<IAudio>({src: null, id: 0,title: null,album: null,author: null});
/* if (player) {
player.addEventListener("canplaythrough", () => set_duration(player.duration), false);
player.ontimeupdate = () => updateTime();
}
const updateTime = () => {
set_currTime(player.currentTime);
}*/
const scanTracks = (ev: React.ChangeEvent<HTMLInputElement>) => {
let target = ev.currentTarget;
let file = target!.files![0];
const reader = new FileReader();
if (target.files && file) {
reader.onload = function (ev2) {
jsmediatags.read(file, {
onSuccess: (result) => {
setTrack({
src: ev2.target!.result as string,
id: 0,
title: result.tags.TPE1.data as string,
album: result.tags.album as string,
author: result.tags.artist as string
});
},
onError: (err) => {
console.log("jsmediatags error: ", err.type, err.info);
}
});
}
reader.readAsDataURL(file);
}
}
return (
<>
<input type="file" multiple id="audio_src" onChange={scanTracks} />
<CoverBG image="https://www.nieuweplaat.nl/wp-content/uploads/2016/08/skillet.jpg"></CoverBG>
<PlayList></PlayList>
<MusicPlayer
audio={track}
></MusicPlayer>
</>
);
}
export default App;
Код компонента MusicPlayer.tsx:
import React, { useState, useEffect, useRef} from 'react';
import './MusicPlayer.css';
import { IAudioProp } from './interfaces';
const useAudio = (file: string) => {
const [playing, set_playing] = useState<boolean>(false);
const [audio] = useState<HTMLAudioElement>(new Audio());
audio.setAttribute('src', file);
console.log("new player render");
const toggle = (): void => set_playing(!playing);
useEffect(() => {
playing ? audio.play() : audio.pause();
}, [playing]);
useEffect(() => {
audio.addEventListener('ended', () => set_playing(false));
return () => {
audio.removeEventListener('ended', () => set_playing(false));
};
}, []);
return [playing, toggle] as const;
}
const MusicPlayer: React.FC<IAudioProp> = (props: IAudioProp) => {
const [playing, toggle] = useAudio(props.audio.src as string);
const evPlayPauseBtn__clicked = (ev: React.MouseEvent<HTMLButtonElement>) => {
toggle();
}
const slider = useRef<HTMLInputElement>(null);
return (
<div className="MusicPlayer">
{ console.log("player render") }
<div className="duration-slider">
<input type="range" min={0} max={100} ref={slider} />
</div>
<div className="data-controls">
<div className="audio-data">
<div className="title">{props!.audio.title ? props!.audio.title : "Unknown"}</div>
<div className="author-album">
<span className="author">{props!.audio.author ? props!.audio.author : "Unknown"}</span> - <span className="album">{props!.audio.album ? props!.audio.album : "Unknown"}</span>
</div>
</div>
<div className="controls">
<button className="btn-prev">
<i className="material-icons">skip_previous</i>
</button>
<button className="btn-play-pause" onClick={evPlayPauseBtn__clicked}>
<i className="material-icons">{playing ? "pause" : "play_arrow"}</i>
</button>
<button className="btn-next">
<i className="material-icons">skip_next</i>
</button>
</div>
</div>
</div>
);
}
export default MusicPlayer;
Интерфейсы:
export interface IAudio {
src: string | null;
id: number;
title: string | null;
album: string | null;
author: string | null;
}
export interface IAudioProp {
audio: IAudio;
}