Oscillator built with react hooks won't stop - reactjs

I'm trying to play around for the first time with the WebAudio API together with React.
My idea was to build a simple button that, once clicked, would start or stop a sound.
With the following code, I'm always getting the error "Failed to execute 'disconnect' on 'AudioNode': the given destination is not connected."
How can I fix it?
Thanks!
import { useState } from 'react';
function App() {
const [ dataPlaying, setDataPlaying ] = useState(false)
const AudioContext = window.AudioContext || window.webkitAudioContext
const audioContext = new AudioContext()
let osc = audioContext.createOscillator()
osc.type = 'sine'
osc.frequency.value = 880
const createOscillator = () => {
if (dataPlaying === false) {
osc.start()
osc.connect(audioContext.destination)
setDataPlaying(true)
} else {
osc.disconnect(audioContext.destination)
setDataPlaying(false)
}
}
return (
<div className="App">
<button
onClick={() => createOscillator() }
data-playing={ dataPlaying }>
<span>Play/Pause</span>
</button>
</div>
);
}
export default App;

Here's my attempt to resolve the connection error.
Create the AudioContext external to the component.
Use an useRef hook to store an audio context to persist through rerenders.
Use an useEffect hook to instantiate an oscillator and manage audo context connection.
Use start/stop toggler to suspend or resume the context instead of connect/disconnect from it.
Updated Code
import React, { useEffect, useRef, useState } from "react";
const AudioContext = window.AudioContext || window.webkitAudioContext;
export default function App() {
const [dataPlaying, setDataPlaying] = useState(false);
const audioContextRef = useRef();
useEffect(() => {
const audioContext = new AudioContext();
const osc = audioContext.createOscillator();
osc.type = "sine";
osc.frequency.value = 880;
// Connect and start
osc.connect(audioContext.destination);
osc.start();
// Store context and start suspended
audioContextRef.current = audioContext;
audioContext.suspend();
// Effect cleanup function to disconnect
return () => osc.disconnect(audioContext.destination);
}, []);
const toggleOscillator = () => {
if (dataPlaying) {
audioContextRef.current.suspend();
} else {
audioContextRef.current.resume();
}
setDataPlaying((play) => !play);
};
return (
<div className="App">
<button onClick={toggleOscillator} data-playing={dataPlaying}>
<span>{dataPlaying ? "Pause" : "Play"}</span>
</button>
</div>
);
}

Related

Calling react hook from a function on another page

I am making a chess game and I need to integrate a count down timer, e.g - so when it's the white players turn their clock is counting down until they move.
To avoid changing a lot more code that is not present in this question ,I would like to keep function "move" as is, though I could change CountDownTimer if needed,
For now I would like to know how to call the react hook setTimerOn in the CountDownTimer component from the function move,then I can work out the rest myself.
From what I have read you cannot do this with react hooks? so just looking for
anything that works, if that means changing CountDownTimer.
export function move(from, to, promotion) {
let tempMove = { from, to }
if (member.piece === chess.turn()) {
const legalMove = chess.move(tempMove)
if (legalMove) {
updateGame()
//i would like to access CountDownTimer and call setTimerOn(true)
// i can work out the code for white and black / start and stop myself afterwards
}
}
,
}
import React from 'react'
function CountDownTimer(){
const [time, setTime] = React.useState(0);
const [timerOn, setTimerOn] = React.useState(false);
React.useEffect(() => {
let interval = null;
if (timerOn) {
interval = setInterval(() => {
setTime((prevTime) => prevTime + 10);
}, 10);
} else if (!timerOn) {
clearInterval(interval);
}
return () => clearInterval(interval);
}, [timerOn]);
return (
<div className="Timers">
<h2>Stopwatch</h2>
<div id="display">
<span>{("0" + Math.floor((time / 60000) % 60)).slice(-2)}:</span>
<span>{("0" + Math.floor((time / 1000) % 60)).slice(-2)}:</span>
<span>{("0" + ((time / 10) % 100)).slice(-2)}</span>
</div>
<div id="buttons">
{!timerOn && time === 0 && (
<button onClick={() => setTimerOn(true)}>Start</button>
)}
{timerOn && <button onClick={() => setTimerOn(false)}>Stop</button>}
{!timerOn && time > 0 && (
<button onClick={() => setTime(0)}>Reset</button>
)}
{!timerOn && time > 0 && (
<button onClick={() => setTimerOn(true)}>Resume</button>
)}
</div>
</div>
);
});
export default CountDownTimer; ```
Based on React documentation, React Hook can be called from function components or other hooks.
In your situation, you should consider the utilization of React Context. You need to move up timerOn state and setTimerOn method as context values. So that, all components which are wrapped by the context provider can utilize the values.
First, create some helpers for managing context.
// TimerOn.js
import React, { createContext, useContext } from 'react';
// create a context
const TimerOnContext = createContext();
// create a hook to provide context value
export function useTimerOn() {
const contextValue = useContext(TimerOnContext);
return contextValue;
}
// custom provider that will wrap your main components
export function TimerOnProvider({ children }) {
const [timerOn, setTimerOn] = React.useState(false);
return (
<TimerOnContext.Provider value={{ timerOn, setTimerOn }}>
{children}
</TimerOnContext.Provider>
);
}
For instance, I create two simple components to demonstrate the timer component and caller component.
// CountDownTimer.js
import React from "react";
import { useTimerOn } from "./TimerOn";
export default function CountDownTimer() {
const { timerOn } = useTimerOn();
// detect changes
React.useEffect(() => {
if (timerOn) {
console.log('timer is on');
} else {
console.log('timer is off');
}
}, [timerOn]);
return (
<div>{timerOn ? 'timer on' : 'timer off'}</div>
);
}
// MoveCaller.js
import React from "react";
import { useTimerOn } from "./TimerOn";
export default function MoveCaller() {
const { timerOn, setTimerOn } = useTimerOn();
// move then set timer
const move = () => {
setTimerOn(!timerOn);
}
return (
<div>
<button type="button" onClick={move}>
Move
</button>
</div>
);
}
Then, you can wrap all main components with the provider component. So, the move function in a component can change the state of timerOn and be read by another component.
import React from 'react';
import CountDownTimer from './CountDownTimer';
import MoveCaller from './MoveCaller';
import { TimerOnProvider } from './TimerOn';
export default function ChessApp() {
return (
<TimerOnProvider>
<CountDownTimer />
<MoveCaller />
</TimerOnProvider>
);
}

reactjs proper use of functions and hooks - I get errors

I finally got a function to compile, but when I call it from within an event handler errors start flying!
The calling code and function is below.
The objective was to get a function to return a 'assettype' as a string and use that String to make a make routing decision.
What am I missing here?
Any chance of some guidance.
Here are the errors:
Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
You might have mismatching versions of React and the renderer (such as React DOM)dom16.14.0 and react 64.2
You might be breaking the Rules of Hooks
You might have more than one copy of React in the same app
See https://reactjs.org/warnings/invalid-hook-call-warning.html for tips about how to debug and fix this problem.
▶ 2 stack frames were collapsed.
...
GetAssetTypeNameFunction
C:/React-Springboot-CRUD-App/react-frontend/src/Services/GetAssetTypeNameFunction.js:7
const GetAssetTypeNameFunction = (props) =>{
const { assettype_assettypeId } = props;
> const [assetType,setAssetType] = useState()
AssetTypeService.getAssetTypeById(assettype_assettypeId).then( (res) =>
setAssetType(res.data));View compiled
ListAssetsComponent.editAssets
C:/React-Springboot-CRUD-App/react-frontend/src/components/ListAssetsComponent.jsx:62
editAssets(assetsid,assettype_assettypeId){
> if (GetAssetTypeNameFunction(assettype_assettypeId) === "Tower")
{
this.props.history.push(`/add-assetstower/${assetsid}/${this.props.match.params.sitemasterid}`);
}
...
Button in a Rendered List. onClick calls code that references function 'GetAssetTypeNameFunction'
...
<button onClick={ () => this.editAssets(assets.assetsid, assets.assettype_assettypeId)} className="btn btn-info">Update </button>
editAssets(assetsid,assettype_assettypeId){
if (GetAssetTypeNameFunction(assettype_assettypeId) === "Tower")
{this.props.history.push(`/add-assetstower${assetsid}/this.props.match.params.sitemasterid`);}
}
...
Finally the GetAssetTypeNameFunction function that is called:
...
import React, { useState} from 'react';
import AssetTypeService from './AssetTypeService'
const GetAssetTypeNameFunction = (props) =>{
// destructuring
const { assettype_assettypeId } = props;
const [assetType,setAssetType] = useState()
AssetTypeService.getAssetTypeById(assettype_assettypeId).then( (res) =>
setAssetType(res.data));
const arrayMap = assetType.map((post)=>{
return(
<ul>
{post.assettypeName}
</ul>
);})
return (
{arrayMap}
);
}
export default GetAssetTypeNameFunction;
...
I am new to the use of functions. I saw a reference that said: "Do not call in event handlers".
try this
import React, { useState} from 'react';
import AssetTypeService from './AssetTypeService'
const GetAssetTypeNameFunction = (props) =>{
const { assettype_assettypeId } = props;
const [assetType,setAssetType] = useState([])
useEffect(() => {
AssetTypeService.getAssetTypeById(assettype_assettypeId).then( (res) =>
setAssetType(res.data)).catch((error) => {
console.error('Error:', error);
});
}, [])
return (
<>
{
assetType.length > 0 ? assetType.map((post) => {
<ul>
{post.assettypeName}
</ul>
})
:
null
}
</>
)
export default GetAssetTypeNameFunction ;

unable to show remaining time in wavesurfer.js

import React,{ useEffect, useRef, useState } from "react";
import { useDispatch, useSelector } from 'react-redux'
import { format, formWaveSurferOptions } from '../../utils/generic'
import { receiveCurrentTrack, receiveTrackRef , togglePlay} from '../../redux/action/currentTrack'
import './_playlist.scss'
import WaveSurfer from "wavesurfer.js";
import {Link} from 'react-router-dom';
const PlayList = (props) => {
const dispatch = useDispatch()
const { track, queue, currentTrack, index } = props
const waveformRef = useRef(null);
const waveRef = useSelector((state)=>state.currentTrack.waveRef)
const wavesurfer = useRef(null);
const currentTime = useRef(null)
const [duration, setDuration] = useState()
useEffect(() => {
const options = formWaveSurferOptions(waveformRef.current);
wavesurfer.current = WaveSurfer.create(options);
wavesurfer.current.load(track.url);
wavesurfer.current.on("ready", ()=> {
if(wavesurfer.current){
wavesurfer.current.setVolume(0)
setDuration(format(wavesurfer.current.getDuration()))
}
});
return () => wavesurfer.current.destroy();
}, [track.url]);
useEffect(()=>{
if(currentTrack){
dispatch(receiveTrackRef(wavesurfer))
}
return ()=> waveRef?.current.stop()
},[currentTrack.track?.url])
const handleClick = () => {
dispatch(receiveTrackRef(wavesurfer))
if(currentTrack){
dispatch(togglePlay())
}
else {
waveRef?.current.stop()
dispatch(receiveCurrentTrack(track, queue));
}
};
return (
<div
className={ currentTrack ? "playlist-item selected playlist" : "playlist-item playlist"}
onClick={handleClick}
>
<div># {index} :</div>
<Link to='/track'>{track.title}</Link>
<div>{track.artist}</div>
<div><span>0.00</span> / {duration && duration}</div>
<div className="wave-form" id="waveform" onChange={()=>console.log('hhh')} ref={waveformRef} />
<div>{duration && duration}</div>
</div>
);
};
export default PlayList;
React-Wavesurfer was unmaintained, so i have moved on to the wavesurfer.js but the issue arrises the now how i can detect the audio current time and show to the page e.g. 0:01 / 3.02, i have use wavesurfer.on('audioprocess') but i can not detect it, so if any one knows the solution please help, thankyou :)
From the Official Doc :
getCurrentTime() – Returns current progress in seconds.
There's also a audioprocess event as you mentioned that fires continuously as the audio plays.
So by combining these together, we have this:
let current;
wavesurfer.on('audioprocess', () => {
current = wavesurfer.getCurrentTime();
});
Note that your wavesurfer player is assigned to the name wavesurfer.current. so the code should be wavesurfer.current.on('audioprocess', () => {});

The React useState() hooks stores undefined always even the data that is to be stored logs correctly using console.log();

Here is the where I am having the problem,
const handleCLick = () => {
const parsedId = getYouTubeID(videoLink);
console.log(parsedId);
setVideoId(parsedId);
console.log(videoId);
}
Here when I am trying to log the 'parsedId' it logs the data correctly
ioNng23DkIM
And after using the setVideoId() function when I try to log the value it returns undefined
undefined
Here is a snap shot of the log output.
Home.js code:
import React, { useRef, useState } from "react";
import { Link } from "react-router-dom";
import getYouTubeID from 'get-youtube-id';
function Home(props) {
const [videoLink, setVideoLink] = useState();
const [isBool, setBool] = useState(false);
const [videoId, setVideoId] = useState();
const urlRef = useRef();
const handleChange = (event) => {
setVideoLink(event.target.value);
if (urlRef.current.value === '') {
alert('Please enter a URL');
setBool(true);
} else {
setBool(false);
}
}
const handleCLick = () => {
const parsedId = getYouTubeID(videoLink);
console.log(parsedId);
setVideoId(parsedId);
console.log(videoId);
}
return (
<section className="homeLayout">
<div className="logo-display">
<img className="logo-img" alt="logo" src="./logo.png" />
<h1>WatchIt</h1>
</div>
<div className="searchlayer">
<form>
<input ref={urlRef} id="videoLink" placeholder="Enter the youtube video URL:" onBlur={handleChange} required />
<Link style={{ pointerEvents: isBool ? 'none' : 'initial' }} to={`/play?=${videoId}`} onClick={handleCLick}>Play</Link>
</form>
</div>
</section>
);
}
export default Home;
You can use useEffect to solve your problem.
Use effect will listen to you state change n then you can perform logic in there.
The problem you're facing is because setState will set the value eventually, not immediately (Usually this means the update will be visible when the component is rendered again). If you want to do something after the value is set, you need to use useEffect.
Splitting your handleClick we get,
const handleCLick = () => {
const parsedId = getYouTubeID(videoLink);
console.log(parsedId);
setVideoId(parsedId); // Queue the change for `videoId`
}
useEffect(() => {
console.log(videoId);
}, [videoId]); // Call this function when the value of `videoId` changes

Counter increment on setInterval

import React from 'react';
import {Plugins} from '#capacitor/core';
import {useState, useEffect} from 'react';
import {db} from './Firebase';
const Maps = () => {
const [lat, setLat] = useState(0);
const [long, setLong] = useState(0);
const [count, setCount] = useState (0);
const Counter = () => {
setCount(count + 1)
console.log(count)
}
const Location = () => {
Plugins.Geolocation.getCurrentPosition().then(
result => setLat ( result.coords.latitude)
)
Plugins.Geolocation.getCurrentPosition().then(
result => setLong (result.coords.longitude)
)
}
const interval = () => {
setInterval (() =>
{
Location();
Counter();
}, 5000 );
}
return (
<div>
<div>
<button onClick = {interval}>
Get Location
</button>
</div>
<div>
{long}
</div>
<div>
{lat}
</div>
</div>
)
}
export default Maps;
I'm trying to get the counter to increment on every iteration of setInterval, through the counter function, but when I log count, it does not increment and always remains as 0.
I've tried running setCount itself within setInterval without any success, it still does not increment count.
Its a stale closure. Change to this setCount(prevCount => prevCount + 1).
Using the updater form of set state like above, you can guarantee that you will be using the most recent value of state.
You can think of it as count in your function being a snapshot of what its value was when the setInterval was declared. This will stop your updates from appearing to work.
In addition, setting state is async, so the console.log(count) will most likely not reflect the new value. Log in an effect or outside the function body to see the updated value each render.
A note about your implementation:
You are creating a setInterval each time the button is clicked. This could lead to some interesting side-effects if clicked more than once. If you click the button twice for example, you will have two setIntervals running every 5 seconds.
In addition to #BrianThompson answer. Try this to avoid innecessary rerenders
import React from 'react';
import {Plugins} from '#capacitor/core';
import {useState, useEffect} from 'react';
import {db} from './Firebase';
const Maps = () => {
const [state, setState] = useState({
latLng:{lat:0,lng:0},
counter: 0
})
const interval = useRef()
//Use camelCase for methods
const location = () => {
Plugins.Geolocation.getCurrentPosition().then(
result => setState ( ({counter}) => {
counter = counter+1
console.log(counter)
return ({
latLng: {
lat: result.coords.latitude,
lng: result.coords.longitude
},
counter
})
})
)
}
const startInterval = () => {
if(interval.current) return;
interval.current = setInterval (() => {
location();
}, 5000 );
}
const stopInterval = () ={
clearInterval(interval.current)
interval.current = null
}
useEffect(()=>{
//Because interval is causing state updates, remember to clear interval when component will unmount
return stopInterval
},[])
return (
<div>
<div>
<button onClick = {startInterval}>
Get Location
</button>
</div>
<div>
{state.latLng.lng}
</div>
<div>
{state.latLng.lat}
</div>
</div>
)
}

Resources