I have a component in React that takes a URL and shows a button that lets the user stop/pause an audio file. This component is used multiple times on the page.
The button starts the audio just fine, however it audio.pause() does not stop the stream.
I am using Next.js, however I am importing the component like so:
const HistoryRow = dynamic(() => import('../components/HistoryRow'), { ssr: false });
Anyone know why this doesn't stop the stream? I can confirm it's entering the if statement just fine.
import { useState } from 'react';
import m from 'moment';
import path from 'path';
function HistoryRow({ data }) {
const [playing, setPlaying] = useState(false);
const audio = new Audio(`${window.location.origin}/audio-responses/${data.timestamp}.wav`);
audio.onended = function () {
setPlaying(false);
};
audio.onpause = function () {
setPlaying(false);
};
audio.onplay = function () {
setPlaying(true);
};
audio.onerror = function () {
setPlaying(false);
};
function execute() {
if (playing) {
audio.pause();
} else {
audio.play();
}
}
const stopSvg = (
<svg> /** svg data ** / </svg>
);
const playSvg = (
<svg> /** svg data ** / </svg>
);
return (
<div className="text-base flex leading-5 font-medium text-blue-600 truncate" onClick={() => execute()}>
{data.type === 'command' ? (playing ? stopSvg : playSvg) : null}
</div>
);
}
export default HistoryRow;
Whenever you state changes, the whole component is rerendered. This means that every variable in the component is rewritten. So you create an Audio instance, then start playing, the component rerenders and a new Audio instance is created en therefor you've lost the binding to the previous instance.
You can use the useRef hook to create a reference to the Audio instance that persists over the entire lifespan of the component. So it will never change unless you explicitly tell it to. You can access the instance with the current property on the returned useRef value.
Since React is state driven I would suggest using the useEffect hook to listen for changes in the playing state and either start or stop playing based on the value of the playing state, that's the other way around as you have it currently.
function HistoryRow({ data }) {
const [playing, setPlaying] = useState(false);
const [hasError, setHasError] = useState(false);
const audio = useRef(new Audio(`${window.location.origin}/audio-responses/${data.timestamp}.wav`));
audio.current.onended = function () {
setPlaying(false);
};
audio.current.onplay = function () {
setHasError(false);
};
const handleClick = () => {
setPlaying(playing => !playing);
});
useEffect(() => {
if (playing) {
audio.current.play().then(() => {
// Audio is playing.
}).catch(error => {
setHasError(true);
});
} else if (!hasError) {
audio.current.pause();
}
}, [playing, hasError]);
const stopSvg = (
<svg> /* svg data */ </svg>
);
const playSvg = (
<svg> /* svg data */ </svg>
);
return (
<div
className="text-base flex leading-5 font-medium text-blue-600 truncate"
onClick={handleClick}>
{data.type === 'command' ? (playing ? stopSvg : playSvg) : null}
</div>
);
}
Related
This question already has answers here:
Why React useState with functional update form is needed?
(5 answers)
Closed 6 months ago.
I am using a context to hold a list of all my notifications. When a notification is created, the context's state is set to a new list with this new notification added. The app re-renders, loops, and renders all the notifications to the screen. When a notification is due, the context is updated to filter out that notification.
However, the notifications have an animation so they can slide to the right before they are removed, which I made so it yields to update the notifications list until the animation is done. Unfortunately, if I close multiple notifications at once, some of them comeback because they are all simultaneously trying to update with an old list.
I don't know where to go.
Notification Context:
import { createContext, useContext, useState } from "react"
import Notification from "../components/Notification"
const NotificationsContext = createContext()
const SetNotificationsContext = createContext()
export function useNotifications() {
return useContext(NotificationsContext)
}
export function useSetNotifications() {
return useContext(SetNotificationsContext)
}
export default function NotificationsProvider({children}) {
const [notifications, setNotifications] = useState([])
return (
<NotificationsContext.Provider value={notifications}>
<SetNotificationsContext.Provider value={setNotifications}>
<div className="notifications-wrapper">
{notifications.map(note => {
return <Notification key={note.id} note={note}/>
})}
</div>
{children}
</SetNotificationsContext.Provider>
</NotificationsContext.Provider>
)
}
Notification Component:
import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'
import { useNotifications, useSetNotifications } from '../contexts/NotificationsProvider'
import useInterval from '../hooks/useInterval'
function Notification({note}) {
const noteRef = useRef()
const notifications = useNotifications()
const setNotifications = useSetNotifications()
const [width, setWidth] = useState(0)
const [delay, setDelay] = useState(null)
// grow progress bar
useInterval(
useCallback(
() => setWidth(oldWidth => oldWidth + 1),
[setWidth]
),
delay)
// sliding left animation
useEffect(() => {
// console.log("slided left")
noteRef.current.onanimationend = () => {
noteRef.current.classList.remove("slide-left")
noteRef.current.onanimationend = undefined
}
noteRef.current.classList.add("slide-left")
}, [])
const handleStartTimer = useCallback(() => {
console.log("timer STARTED")
setDelay(10)
}, [setDelay])
const handlePauseTimer = useCallback(() => {
console.log("timer PAUSED")
setDelay(null)
}, [setDelay])
// filter off notification and sliding right animation
const handleCloseNotification = useCallback(() => {
console.log("slided right / removed notification")
handlePauseTimer()
noteRef.current.onanimationend = () => {
setNotifications([...notifications.filter(listNote => listNote.id !== note.id)])
}
noteRef.current.classList.add("slide-right")
}, [note.id, notifications, setNotifications, handlePauseTimer])
// notification is due
useLayoutEffect(() => {
if (width >= noteRef.current.clientWidth - 10) {
handleCloseNotification()
}
}, [width, handleCloseNotification, handlePauseTimer])
// start timer when notification is created
useEffect(() => {
handleStartTimer()
return handlePauseTimer
}, [handleStartTimer, handlePauseTimer])
return (
<div ref={noteRef} className={`notification-item ${note.type === "SUCCESS" ? "success" : "error"}`}
onMouseOver={handlePauseTimer} onMouseLeave={handleStartTimer}>
<button onClick={handleCloseNotification} className='closing-button'>✕</button>
<strong>{note.text}</strong>
<div className='notification-timer' style={{"width":`${width}px`}}></div>
</div>
)
}
export default Notification
Change your set notifications line to:
setNotifications(prevNotifications => [...prevNotifications.filter(listNote => listNote.id !== note.id)])
This will ensure you never use a stale value.
I'm trying to configure video.js in a next.js project but it doesn't work.
At the beginning of loading the player appears black and disappears suddenly.
In the console there is a warning saying the following:
"video.es.js?31bb:228 VIDEOJS: WARN: The supplied element is not included in the DOM"
The player component code is as follows:
import React from 'react';
import videojs from 'video.js';
import 'video.js/dist/video-js.css';
export const ThorPlayer = (props) => {
const videoRef = React.useRef(null);
const playerRef = React.useRef(null);
const {options, onReady} = props;
React.useEffect(() => {
// Make sure Video.js player is only initialized once
if (!playerRef.current) {
const videoElement = videoRef.current;
if (!videoElement) return;
const player = playerRef.current = videojs(videoElement, options, () => {
player.log('player is ready');
onReady && onReady(player);
});
// You can update player in the `else` block here, for example:
} else {
player.autoplay(options.autoplay);
player.src(options.sources);
}
}, [options, videoRef]);
// Dispose the Video.js player when the functional component unmounts
React.useEffect(() => {
const player = playerRef.current;
return () => {
if (player) {
player.dispose();
playerRef.current = null;
}
};
}, [playerRef]);
return (
<div data-vjs-player>
<video ref={videoRef} className='video-js vjs-big-play-centered' style={{ width: "800px", height:"400px" }}/>
</div>
);
}
export default ThorPlayer;
And the page that implements the component is this:
import React from 'react'
import ThorPlayer from '../../components/_ThorPlayer'
export default function index() {
const playerRef = React.useRef(null);
const videoJsOptions = {
autoplay: true,
controls: true,
responsive: true,
fluid: true,
sources: [{
src: 'https://obj-gravscale.zadarazios.com:443/v1/AUTH_f57eb386f52e4dc0bcdf19764aecc205/ct/bl_se.mp4',
type: 'video/mp4'
}]
};
const handlePlayerReady = (player) => {
playerRef.current = player;
// You can handle player events here, for example:
player.on('waiting', () => {
player.log('player is waiting');
});
player.on('dispose', () => {
player.log('player will dispose');
});
};
return (
<>
<h1>Teste de Player: </h1>
<ThorPlayer options={videoJsOptions} onReady={handlePlayerReady} />
<div></div>
</>
)
}
Could someone who has already implemented video.js in next.js help me with this problem?
Are you using React 18 by any chance? I was running into the same problem as you and it was caused by the fact that useEffect fires twice in development mode.
As a workaround until video.js 7.20 is released, you can use the technique described in this comment.
In my existing react component, I need to render another react component for a specific time period.
As soon as the parent component mounts/or data loads, the new-component (or child component) should be visible after 1-2 seconds and then after another few seconds, the new-component should be hidden. This needs to be done only if there is no data available.
This is what currently I've tried to achieve:
import React, { useState, useEffect } from "react";
function App() {
const [showComponent, setShowComponent] = useState(false);
const sampleData = [];
useEffect(() => {
if (sampleData.length === 0) {
setTimeout(() => {
setShowComponent(true);
}, 1000);
}
}, [sampleData]);
useEffect(() => {
setTimeout(() => {
setShowComponent(false);
}, 4000);
}, []);
const componentTwo = () => {
return <h2>found error</h2>;
};
return <>First component mounted{showComponent && componentTwo()}</>;
}
export default App;
The current implementation is not working as expected. The new-component renders in a blink fashion.
Here is the working snippet attached:
Any help to resolve this is appreciated!
Every time App renders, you create a brand new sampleData array. It may be an empty array each time, but it's a different empty array. Since it's different, the useEffect needs to rerun every time, which means that after every render, you set a timeout to go off in 1 second and show the component.
If this is just a mock array that will never change, then move it outside of App so it's only created once:
const sampleData = [];
function App() {
// ...
}
Or, you can turn it into a state value:
function App() {
const [showComponent, setShowComponent] = useState(false);
const [sampleData, setSampleData] = useState([]);
// ...
}
I have modified the code to work, hope this how you are expecting it to work.
import React, { useState, useEffect } from "react";
const sampleData = [];
// this has to be out side or passed as a prop
/*
reason: when the component render (caued when calling setShowComponent)
a new reference is created for "sampleData", this cause the useEffect run every time the component re-renders,
resulting "<h2>found error</h2>" to flicker.
*/
function App() {
const [showComponent, setShowComponent] = useState(false);
useEffect(() => {
if (sampleData.length === 0) {
const toRef = setTimeout(() => {
setShowComponent(true);
clearTimeout(toRef);
// it is good practice to clear the timeout (but I am not sure why)
}, 1000);
}
}, [sampleData]);
useEffect(() => {
if (showComponent) {
const toRef = setTimeout(() => {
setShowComponent(false);
clearTimeout(toRef);
}, 4000);
}
}, [showComponent]);
const componentTwo = () => {
return <h2>found error</h2>;
};
return <>First component mounted{showComponent && componentTwo()}</>;
}
export default App;
You can try this for conditional rendering.
import { useEffect, useState } from "react";
import "./styles.css";
const LoadingComponent = () => <div>Loading...</div>;
export default function App() {
const [isLoading, setLoading] = useState(true);
const [isError, setIsError] = useState(false);
const onLoadEffect = () => {
setTimeout(() => {
setLoading(false);
}, 2000);
setTimeout(() => {
setIsError(true);
}, 10000);
};
useEffect(onLoadEffect, []);
if (isLoading) {
return <LoadingComponent />;
}
return (
<div className="App">
{isError ? (
<div style={{ color: "red" }}>Something went wrong</div>
) : (
<div>Data that you want to display</div>
)}
</div>
);
}
I needed to do imperatively control rendering an animation component and make it disappear a few seconds later. I ended up writing a very simple custom hook for this. Here's a link to a sandbox.
NOTE: this is not a full solution for the OP's exact use case. It simply abstracts a few key parts of the general problem:
Imperatively control a conditional render
Make the conditional "expire" after duration number of milliseconds.
import { useState } from "react";
function Music(){
const [pauseToggle, setpauseToggle] = useState(false)
const music = new Audio(require(`./Alan.mp3`));
console.log(music);
let isPlaying = false;
const player = () => {
pauseToggle ? setpauseToggle(false) : setpauseToggle(true);
if(isPlaying){
music.pause();
isPlaying = false;
}else{
music.play();
isPlaying = true;
}
}
return(
<div>
<button onClick={player}>{pauseToggle?"=":">"}</button>
</div>
)
}
export default Music;
I'm unable to pause the audio. IInstead it gets played twice when I try to pause.
I tried to get help from this Unable to pause audio in Reactjs but it didn't help.
Any other solution for pausing audio file?
I believe the music variable should be stateful so it's retained across renders.
const [music] = useState(new Audio(require(`./Alan.mp3`)));
and isPlaying should probably be found by querying the Audio object (music) and/or updated by listening for play/pause state changes.
I'd simplify state to a single boolean: your audio is either playing or not, so there's no need for redundancy here. let isPlaying = false; isn't state, so it's not persistent across renders and doesn't do anything pauseToggle doesn't already achieve.
In fact, audio elements have a .paused property that makes the separate state boolean redundant, but for React rendering/lifecycle purposes the extra boolean is handy.
Since the audio is a side effect to the rendering, I'd make it a ref, then trigger play/pause functions based on when playing changes in a useEffect.
A bit of a bonus, but it's probably worth handling the onended event to set playing to false when the track ends, for starters.
const {useEffect, useRef, useState} = React;
const MusicPlayer = ({url}) => {
const [playing, setPlaying] = useState(false);
const audioRef = useRef(new Audio(url));
useEffect(() => {
const handler = () => setPlaying(false);
audioRef.current.addEventListener("ended", handler);
return () =>
audioRef.current.removeEventListener("ended", handler)
;
}, [audioRef.current]);
useEffect(() => {
audioRef.current[playing ? "play" : "pause"]();
}, [playing]);
return (
<div>
<button onClick={() => setPlaying(!playing)}>
{playing ? "=" : ">"}
</button>
</div>
);
};
const url = "https://upload.wikimedia.org/wikipedia/en/a/a9/Webern_-_Sehr_langsam.ogg";
ReactDOM.createRoot(document.querySelector("#app"))
.render(<MusicPlayer url={url}/>);
button {
font-size: 2em;
}
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<div id="app"></div>
Putting the audio into a hook helps make it reusable and keeps your parent component clean:
const {useEffect, useRef, useState} = React;
const useAudio = url => {
const [playing, setPlaying] = useState(false);
const audioRef = useRef(new Audio(url));
useEffect(() => {
const handler = () => setPlaying(false);
audioRef.current.addEventListener("ended", handler);
return () =>
audioRef.current.removeEventListener("ended", handler)
;
}, [audioRef.current]);
useEffect(() => {
audioRef.current[playing ? "play" : "pause"]();
}, [playing]);
return [playing, setPlaying];
};
const MusicPlayer = ({url}) => {
const [playing, setPlaying] = useAudio(url);
return (
<div>
<button onClick={() => setPlaying(!playing)}>
{playing ? "=" : ">"}
</button>
</div>
);
};
const url = "https://upload.wikimedia.org/wikipedia/en/a/a9/Webern_-_Sehr_langsam.ogg";
ReactDOM.createRoot(document.querySelector("#app"))
.render(<MusicPlayer url={url} />);
button {
font-size: 2em;
}
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<div id="app"></div>
import React, { Component } from 'react'
import { Button, Input, Icon,Dropdown,Card} from 'semantic-ui-react'
import { Link } from 'react-router-dom'
import $ from 'jquery'
import styles from './Home.scss'
import Modal from './Modal.jsx'
import MakeChannelModal from './MakeChannelModal.jsx'
class Music extends React.Component {
constructor(props) {
super(props);
this.state = {
play: false,
pause: true
};
this.url = "http://streaming.tdiradio.com:8000/house.mp3";
this.audio = new Audio(this.url);
}
play(){
this.setState({
play: true,
pause: false
});
console.log(this.audio);
this.audio.play();
}
pause(){
this.setState({ play: false, pause: true });
this.audio.pause();
}
render() {
return (
<div>
<button onClick={this.play}>Play</button>
<button onClick={this.pause}>Pause</button>
</div>
);
}
}
export default Music
This is the code that I am using to play the sound with url (this.url) in my react app. When I press the play button, it gives me an error
Uncaught TypeError: Cannot read property 'setState' of undefined
I am not sure why this is happpening since I don't see any undefined states. A;; states have been declared.
I am new to react so I might be missing something very important.
Please help!
ES6 class properties syntax
class Music extends React.Component {
state = {
play: false
}
audio = new Audio(this.props.url)
componentDidMount() {
audio.addEventListener('ended', () => this.setState({ play: false }));
}
componentWillUnmount() {
audio.removeEventListener('ended', () => this.setState({ play: false }));
}
togglePlay = () => {
this.setState({ play: !this.state.play }, () => {
this.state.play ? this.audio.play() : this.audio.pause();
});
}
render() {
return (
<div>
<button onClick={this.togglePlay}>{this.state.play ? 'Pause' : 'Play'}</button>
</div>
);
}
}
export default Music;
Hooks version (React 16.8+):
import React, { useState, useEffect } from "react";
const useAudio = url => {
const [audio] = useState(new Audio(url));
const [playing, setPlaying] = useState(false);
const toggle = () => setPlaying(!playing);
useEffect(() => {
playing ? audio.play() : audio.pause();
},
[playing]
);
useEffect(() => {
audio.addEventListener('ended', () => setPlaying(false));
return () => {
audio.removeEventListener('ended', () => setPlaying(false));
};
}, []);
return [playing, toggle];
};
const Player = ({ url }) => {
const [playing, toggle] = useAudio(url);
return (
<div>
<button onClick={toggle}>{playing ? "Pause" : "Play"}</button>
</div>
);
};
export default Player;
Update 03/16/2020: Multiple concurrent players
In response to #Cold_Class's comment:
Unfortunately if I use multiple of these components the music from the other components doesn't stop playing whenever I start another component playing - any suggestions on an easy solution for this problem?
Unfortunately, there is no straightforward solution using the exact codebase we used to implement a single Player component. The reason is that you somehow have to hoist up single player states to a MultiPlayer parent component in order for the toggle function to be able to pause other Players than the one you directly interacted with.
One solution is to modify the hook itself to manage multiple audio sources concurrently. Here is an example implementation:
import React, { useState, useEffect } from 'react'
const useMultiAudio = urls => {
const [sources] = useState(
urls.map(url => {
return {
url,
audio: new Audio(url),
}
}),
)
const [players, setPlayers] = useState(
urls.map(url => {
return {
url,
playing: false,
}
}),
)
const toggle = targetIndex => () => {
const newPlayers = [...players]
const currentIndex = players.findIndex(p => p.playing === true)
if (currentIndex !== -1 && currentIndex !== targetIndex) {
newPlayers[currentIndex].playing = false
newPlayers[targetIndex].playing = true
} else if (currentIndex !== -1) {
newPlayers[targetIndex].playing = false
} else {
newPlayers[targetIndex].playing = true
}
setPlayers(newPlayers)
}
useEffect(() => {
sources.forEach((source, i) => {
players[i].playing ? source.audio.play() : source.audio.pause()
})
}, [sources, players])
useEffect(() => {
sources.forEach((source, i) => {
source.audio.addEventListener('ended', () => {
const newPlayers = [...players]
newPlayers[i].playing = false
setPlayers(newPlayers)
})
})
return () => {
sources.forEach((source, i) => {
source.audio.removeEventListener('ended', () => {
const newPlayers = [...players]
newPlayers[i].playing = false
setPlayers(newPlayers)
})
})
}
}, [])
return [players, toggle]
}
const MultiPlayer = ({ urls }) => {
const [players, toggle] = useMultiAudio(urls)
return (
<div>
{players.map((player, i) => (
<Player key={i} player={player} toggle={toggle(i)} />
))}
</div>
)
}
const Player = ({ player, toggle }) => (
<div>
<p>Stream URL: {player.url}</p>
<button onClick={toggle}>{player.playing ? 'Pause' : 'Play'}</button>
</div>
)
export default MultiPlayer
Example App.js using the MultiPlayer component:
import React from 'react'
import './App.css'
import MultiPlayer from './MultiPlayer'
function App() {
return (
<div className="App">
<MultiPlayer
urls={[
'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3',
'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-2.mp3',
'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-3.mp3',
]}
/>
</div>
)
}
export default App
The idea is to manage 2 parallel arrays:
your audio sources (built from the urls props you pass to the parent component ; the urls props is an array of strings (your MP3 URLs))
an array tracking the state of each player
The toggle method updates the player state array based on the following logic:
if there is a player currently active (i.e. audio is playing) and this active player is not the player targeted by the toggle method, revert that player's playing state to false, and set the targeted player's playing state to true [you clicked on 'play' while another audio stream was already playing]
if the player currently active is the player targeted by the toggle method, simply revert the targeted player's playing state to false [you clicked on 'pause']
if there is no player currently active, simply set the targeted player's state to true [you clicked on 'play' while no audio stream was currently playing]
Note that the toggle method is curried to accept the source player's index (i.e. the index of the child component where the corresponding button was clicked).
Actual audio object control happens in useEffect as in the original hook, but is slightly more complex as we have to iterate through the entire array of audio objects with every update.
Similarly, event listeners for audio stream 'ended' events are handled in a second useEffect as in the original hook, but updated to deal with an array of audio objects rather than a single such object.
Finally, the new hook is called from the parent MultiPlayer component (holding multiple players), which then maps to individual Players using (a) an object that contains the player's current state and its source streaming URL and (b) the toggle method curried with the player's index.
CodeSandbox demo
You can also accomplish this by using the useSound hook.
To do this, first install the npm package:
npm install use-sound
Imports:
import useSound from 'use-sound'
import mySound from '../assets/sounds/yourSound.mp3' // Your sound file path here
Usage example 1
A simple approach..
function MyButton(){
const [playSound] = useSound(mySound)
return (
<button onClick={() => playSound()}>
Play Sound
</button>
)
}
Usage example 2
In this setup we can control the volume. Also, playSound() will be called inside the handleClick() function, allowing you to do more things on click than just playing a sound.
function MyButton(){
const [playSound] = useSound(mySound, { volume: 0.7 }) // 70% of the original volume
const handleClick = () => {
playSound()
// maybe you want to add other things here?
}
return (
<button onClick={() => handleClick()}>
Play Sound
</button>
)
}
For more info click here or here
I faced a different problem with this implementation of the answer.
It seemed the browser was continuously trying to download the sound on every re-render.
I ended up using useMemo for the Audio with no dependencies which causes the hook to only ever once create the Audio and never attempt to recreate it.
import {useMemo, useEffect, useState} from "react";
const useAudio = url => {
const audio = useMemo(() => new Audio(url), []);
const [playing, setPlaying] = useState(false);
const toggle = () => setPlaying(!playing);
useEffect(() => {
playing ? audio.play() : audio.pause();
},
[playing]
);
useEffect(() => {
audio.addEventListener('ended', () => setPlaying(false));
return () => {
audio.removeEventListener('ended', () => setPlaying(false));
};
}, []);
return [playing, toggle];
};
export default useAudio;
I got some problems following these steps when working with Next Js because Audio is HTMLElement tag, eventually, it was rendering me a big fat error, so I decided to study more and the result for it in my project was the following:
//inside your component function.
const [audio] = useState( typeof Audio !== "undefined" && new Audio("your-url.mp3")); //this will prevent rendering errors on NextJS since NodeJs doesn't recognise HTML tags neither its libs.
const [isPlaying, setIsPlaying] = useState(false);
To handle the player, I made a useEffect:
useEffect(() => {
isPlaying ? audio.play() : audio.pause();
}, [isPlaying]);
You will manage the state "isPlaying" according to the functions you make so far.
I'm a bit late to the party here but piggy backing off of 'Thomas Hennes':
One problem people looking at this will run into is, if you try to use this code verbatim in an app with multiple pages, they are not going to have a nice time. Since state is managed at the component, you can play, navigate and play again.
To get around that you want to have your component push it's state up to App.js instead and manage the state there.
Allow me to show what I mean.
My player component looks like this:
import React, { Component } from 'react'
class MusicPlayer extends Component {
render() {
const { playing } = this.props.player;
return (
<div>
<button onClick={this.props.toggleMusic.bind(this, playing)}>{playing ? "Pause" : "Play"}</button>
</div>
);
}
};
export default MusicPlayer;
Then in my App.js it looks something like this (using a TODO list sample app):
import React, { Component } from 'react';
import { BrowserRouter as Router, Route } from 'react-router-dom'
import './App.css';
import Header from './componets/layout/Header'
import Todos from './componets/Todos'
import AddTodo from './componets/AddTodo'
import About from './componets/pages/About'
import MusicPlayer from './componets/MusicPlayer'
import axios from 'axios';
class App extends Component {
constructor(props) {
super(props);
this.state = { playing: false, todos: [] }
this.audio = new Audio('<YOUR MP3 LINK HERE>');
}
componentDidMount(){
axios.get('https://jsonplaceholder.typicode.com/todos')
.then(res => this.setState({ playing: this.state.playing, todos: res.data }))
}
toggleComplete = (id) => {
this.setState({ playing: this.state.playing, todos: this.state.todos.map(todo => {
if (todo.id === id){
todo.completed = !todo.completed
}
return todo
}) });
}
delTodo = (id) => {
axios.delete(`https://jsonplaceholder.typicode.com/todos/${id}`)
.then(res => this.setState({ playing: this.state.playing, todos: [...this.state.todos.filter(todo => todo.id !== id)] }));
}
addTodo = (title) => {
axios.post('https://jsonplaceholder.typicode.com/todos', {
title,
completed: false
})
.then(res => this.setState({ playing: this.state.playing, todos: [...this.state.todos, res.data]}))
}
toggleMusic = () => {
this.setState({ playing: !this.state.playing, todos: this.state.todos}, () => {
this.state.playing ? this.audio.play() : this.audio.pause();
});
}
render() {
return (
<Router>
<div className="App">
<div className="container">
<Header />
<Route exact path="/" render={props => (
<React.Fragment>
<AddTodo addTodo={this.addTodo} />
<Todos todos={this.state.todos} toggleComplete={this.toggleComplete} delTodo={this.delTodo} />
</React.Fragment>
)} />
<Route path="/About" render={props => (
<React.Fragment>
<About />
<MusicPlayer player={this.state} toggleMusic={this.toggleMusic} />
</React.Fragment>
)} />
</div>
</div>
</Router>
);
}
}
export default App;
Uncaught TypeError: Cannot read property 'setState' of undefined
The error occurs because of how the this keyword works in JavaScript. I think the Audio should play just fine if we solve that issue.
If you do a console.log(this) inside play() you will see that this it is undefined and that's why it throws that error, since you are doing this.setState().Basically the value of this inside play() depends upon how that function is invoked.
There are two common solutions with React:
Using bind() to set the value of a function's this regardless of how it's called:
constructor(props) {
super(props);
this.play() = this.play.bind(this);
}
Using arrow functions which don't provide their own this binding
<button onClick={() => {this.play()}}>Play</button>
Now you will have access to this.setState and this.audio inside play(), and the same goes for pause().
You can try this, it work on me
var tinung = `${window.location.origin}/terimakasih.ogg`;
var audio = document.createElement("audio");
audio.autoplay = true;
audio.load();
audio.addEventListener(
"load",
function() {
audio.play();
},
true
);
audio.src = tinung;