Facing Problems in Converting to Functional Components in React - javascript

This is a very small two-page project, so this won't take much time.
I am trying to convert this Github repo from class-based component to functional component. I am very close but the logic is just not working properly.
The useState hook especially, as in the values are not getting saved. So I tried a different approach.
This is the expected output which is the live demo of the original project:
https://matt-eric.github.io/web-audio-fft-visualization-with-react-hooks/
And this is where I am. This is the sandbox link.
Ignore the The error you provided does not contain a stack trace. error for now. Click on the x and refresh the small project window (not your browser tab) a couple of times until the audio plays on refresh. This is because google stops you from playing music on load.
I want to audio to play with the click of the button and not on load. But it is not working.
Thank whoever goes and looks into it.

There's some cleanup needed but this is working. https://codesandbox.io/s/xenodochial-wildflower-c5b35
All I really did was put all the functions in a useCallback made audioFile a ref, and then made the toggleAudio function which either plays or pauses the audio depending on its current state. One of the biggest problems I saw was that you were trying to initialize the audio on click, but that really should be done on mount, then the audio just starts when you click. Also if you initialize on every click then it causes errors because it's already initialized.
Let me know if you have any questions!

Using your sandbox, I found a couple of things missing:
You needed to memoize your audoFile (since you create a new audio and it never changes)
Your functions need to be stable, and therefore need to be react hook functions, specifically useCallback functions.
In your onClick function in the demo (the start button) you called to initalizeAudioAnalyzer but that was already intialized with your useEffect on functionCont.jsx and doesn't need to be initialized again. Once I removed this, it all worked.
Here is the fixed up code that is now playing the audio:
functionCont.jsx:
import React, { useCallback, useEffect, useMemo, useState } from "react";
import VisualDemo from "./functionViz";
// import VisualDemo from "./VisualDemo";
import soundFile from "../audio/water.mp3";
const FunctionCont = () => {
const audioFile = useMemo(() => {
const audio = new Audio();
audio.crossOrigin = "anonymous";
return audio;
} , []);
const [audioData, setAudioData] = useState();
let frequencyBandArray = [...Array(25).keys()];
const initializeAudioAnalyser = useCallback(() => {
const audioContext = new AudioContext();
const source = audioContext.createMediaElementSource(audioFile);
const analyser = audioContext.createAnalyser();
audioFile.src = soundFile;
analyser.fftSize = 64;
source.connect(audioContext.destination);
source.connect(analyser);
setAudioData(analyser);
audioFile.play();
}, [audioFile]);
useEffect(() => {
initializeAudioAnalyser();
}, [initializeAudioAnalyser]);
const getFrequencyData = useCallback((styleAdjuster) => {
const bufferLength = audioData.frequencyBinCount;
const amplitudeArray = new Uint8Array(bufferLength);
audioData.getByteFrequencyData(amplitudeArray);
styleAdjuster(amplitudeArray);
}, [audioData]);
return (
<div>
<VisualDemo
frequencyBandArray={frequencyBandArray}
getFrequencyData={getFrequencyData}
// audioData={audioData}
audioFile={audioFile}
/>
</div>
);
};
export default FunctionCont;
functionViz.jsx
import React, { useRef, useEffect, useState } from "react";
import Paper from "#material-ui/core/Paper";
import IconButton from "#material-ui/core/IconButton";
import Tooltip from "#material-ui/core/Tooltip";
import EqualizerIcon from "#material-ui/icons/Equalizer";
import { makeStyles } from "#material-ui/core/styles";
import "../stylesheets/App.scss";
const useStyles = makeStyles((theme) => ({
flexContainer: {
display: "flex",
flexWrap: "wrap",
justifyContent: "center",
paddingTop: "25%"
}
}));
const VisualDemo = (props) => {
const classes = useStyles();
const amplitudeValues = useRef(null);
function adjustFreqBandStyle(newAmplitudeData) {
amplitudeValues.current = newAmplitudeData;
let domElements = props.frequencyBandArray.map((num) =>
document.getElementById(num)
);
for (let i = 0; i < props.frequencyBandArray.length; i++) {
let num = props.frequencyBandArray[i];
domElements[
num
].style.backgroundColor = `rgb(0, 255, ${amplitudeValues.current[num]})`;
domElements[num].style.height = `${amplitudeValues.current[num]}px`;
}
}
function runSpectrum() {
props.getFrequencyData(adjustFreqBandStyle);
requestAnimationFrame(runSpectrum);
}
function handleStartButtonClick() {
requestAnimationFrame(runSpectrum);
}
return (
<div>
<div>
<Tooltip title="Start" aria-label="Start" placement="right">
<IconButton
id="startButton"
onClick={handleStartButtonClick}
// disabled={!!props.audioData ? true : false}
>
<EqualizerIcon />
</IconButton>
</Tooltip>
</div>
<div className={classes.flexContainer}>
{props.frequencyBandArray.map((num) => (
<Paper
className={"frequencyBands"}
elevation={4}
id={num}
key={num}
/>
))}
</div>
</div>
);
};
export default VisualDemo;

Related

manage a single instance of a Nextjs Component seperatly for lottie Animations?

i am building a nextjs component called SocialMediaIcon = this component should recive a single lottie json path and render a lottie svg, when the user hovers on the animation it should start playing and when the mouse leaves it should reset the animation to position 0 ,
import { useEffect, useRef, useState } from 'react'
import { LottiePlayer } from 'lottie-web'
function SocialMediaIcon({ iconPath }) {
const ref = useRef(null)
const anim = useRef(null)
const [lottie, setLottie] = useState(null)
useEffect(() => {
import('lottie-web').then((Lottie) => setLottie(Lottie.default))
}, [])
useEffect(() => {
if (lottie && ref.current) {
anim.current = lottie.loadAnimation({
container: ref.current,
renderer: 'svg',
loop: true,
autoplay: false,
// path to your animation file, place it inside public folder
animationData: iconPath,
})
return () => anim.current.destroy()
}
}, [lottie])
const handleAnimation = () => {
lottie.play()
}
const handlemouseOut = () => {
lottie.goToAndStop(0)
}
return (
<div
ref={ref}
className="lottie h-20 rounded-lg bg-white p-2"
onMouseEnter={handleAnimation}
onMouseLeave={handlemouseOut}
></div>
)
}
export default SocialMediaIcon
and this is how i am rendering the component in the parrent component :
<SocialMediaIcon iconPath={githubJson} />
<SocialMediaIcon iconPath={twitterJson} />
the problem i'm having is that when i hover over one component all the components are playing the animation instead of just the one that has been hovered
i have already tried :
1.using a anim = useRef() hook and passing it to lottie.play(anim.current) which resulted in no animation being played at all
2.i also tried passing animation to lottie play lottie.play(lottie.animation) thinking every instance has its own animation and the animation did not play .
it would be greatly appreciated if you could help me understand this on a deeper level, i think im really missing a important concept not sure if its about lottie or nextjs in genral.

How to synchronize data in a React app? The data rendered doesn't match the data accessed in the function call

I'm making a React app that requests data from an API and renders the results as cards. The issue I'm having is that I added a 'favorite' star icon to each card, that when clicked, will become highlighted gold, and an additional click will revert it to grey. This is controlled by a Boolean value stored in a React Hook for the local component. The color change works properly, however when I want to access the boolean state variable to continue the rest of the operation, the local state value that I keep accessing seems to be the 'previous' state. I can't figure out how to fix this.
Reading up on the documentation leads me to think that I'm accessing stale state data or stale closures. I'm still having trouble fixing this so I would appreciate some assistance, here's what I have.
import React, { useState } from 'react'
import { FontAwesomeIcon } from '#fortawesome/react-fontawesome'
import { faStar} from '#fortawesome/free-solid-svg-icons'
export const Card = ({ item, addOn, onDelete }) => {
//flags if the star was clicked or not
const [favored, setFavored] = useState(false);
//update favorite component
const updateFavorites = (item) => {
//here thet favored state is toggled
setFavored(!favored);
//further attempts to access favored results in the wrong favored being accessed
if(favored){
console.log(`value of favored is: ${favored}, the start should be gold`);
}else{
console.log(`value of favored is: ${favored}, the star should be grey`);
}
}
return (
<div className = 'card' key = {item.id} >
<FontAwesomeIcon
icon = {faStar}
className = {favored ? 'star favored' : 'star'}
onClick = {(e) => updateFavorites(item)}
/>
<div className = 'card-info'>
<img
src = {item.image_url}
alt = ''
style = {{height: '15rem', width: '5rem'}}
/>
<div>
<h3>{item.name}</h3>
<p className = "description">{item.description}</p>
</div>
</div>
</div>
)
}
Edit: I solved my issue by doing the following:
const updateFavorites = (item) => {
//here thet favored state is toggled
setFavored(favored => {
favored = !favored;
if(favored){
console.log('adding a favorite');
addOn(item);
}else{
console.log('removing a favorite');
onDelete(item);
};
return favored;
});
}
However, I replaced it by using useEffect as per the answers.
Fact is that state values are used by functions based on their current closures, and state updates will reflect in the next re-render by which the existing closures are not affected, but new ones are created. Now in the current state, the values are obtained by existing closures, and when a re-render happens, the closures are updated based on whether the function is recreated again or not.
You can use following solutions.
useEffect(() => {
if (favored) {
console.log(`value of favored is: ${favored}, the start should be gold`);
} else {
console.log(`value of favored is: ${favored}, the star should be grey`);
}
}, [favored,item]);
You should import useEffect and move the logic from the event handler to the useEffect hook:
import React, { useState, useEffect } from 'react'
import { FontAwesomeIcon } from '#fortawesome/react-fontawesome'
import { faStar} from '#fortawesome/free-solid-svg-icons'
export const Card = ({ item, addOn, onDelete }) => {
const [favored, setFavored] = useState(false);
const updateFavorites = () => {
setFavored(!favored);
}
// Move the desired logic here and add 'favored' and 'item' to your dependency array:
useEffect(() => {
if(favored){
console.log(`value of favored is: ${favored}, the start should be gold`);
}else{
console.log(`value of favored is: ${favored}, the star should be grey`);
}
}, [favored, item])
return (
<div className = 'card' key = {item.id} >
<FontAwesomeIcon
icon = {faStar}
className = {favored ? 'star favored' : 'star'}
onClick = {() => updateFavorites()}
/>
<div className = 'card-info'>
<img
src = {item.image_url}
alt = ''
style = {{height: '15rem', width: '5rem'}}
/>
<div>
<h3>{item.name}</h3>
<p className = "description">{item.description}</p>
</div>
</div>
</div>
)
}

Playing audio in Safari using .play()

I'm looking to play an audio file using custom controls to trigger the .play() method on a custom button. To be clear, I'm not trying to have the audio auto-play. Everything works perfectly in Chrome, but in Safari I get the error:
Unhandled Promise Rejection: NotAllowedError: The request is not
allowed by the user agent or the platform in the current context,
possibly because the user denied permission.
The project is built using React and React Router, so I'm wondering if it's possibly fixable in my useEffect() hook. I've tried enabling controls on the audio element and using CSS to remove them from the DOM, but no luck.
import React, { useState, useRef, useEffect } from "react";
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
import { gsap } from "gsap";
function RadioPlayerNav(props) {
const audioEl = useRef(null);
const [isPlaying, setIsPlaying] = useState(false);
const playingTitle = document.querySelector(".radio-player-nav .title p");
const toPX = (value) => {
return (parseFloat(value) / 100) * (/vh/gi.test(value) ? window.innerHeight : window.innerWidth);
};
const radioPlayerGSAP = gsap.to(".radio-player-nav .title p", {
x: toPX("-5vw"),
duration: 4,
ease: "none",
yoyo: true,
repeat: -1,
delay: 1,
repeatDelay: 1,
paused: true,
});
useEffect(() => {
if (isPlaying) {
audioEl.current.play();
radioPlayerGSAP.play();
// radioPlayerGSAP.reversed(4, false);
} else {
audioEl.current.pause();
}
}, [isPlaying]);
return (
<div className="radio-player-nav">
<div className="radio-player-controls">
<audio src="src/current-radio-mix.mp3" ref={audioEl} preload="auto"></audio>
<i
className={isPlaying ? "fas fa-pause cursor-hover" : "fas fa-play cursor-hover"}
onClick={() => {
setIsPlaying(!isPlaying);
}}
></i>
<div className="title">
<p>MIXED FEELINGS M0001</p>
</div>
<a href="src/current-radio-mix.mp3" download="Mixed Feelings M0001">
<i className="fas fa-download cursor-hover"></i>
</a>
</div>
</div>
);
}
export default RadioPlayerNav;
You can find the full github repo for the project here: https://github.com/nallstott/mixed-feelings/tree/master
Turns out, safari requires you to use useLayoutEffect instead of useEffect to accomplish this. I'm leaving the post up since I didn't see anything previously that gave the answer, along with the article that solved it for me in case anyone else has this issue with <audio> on safari.
https://lukecod.es/2020/08/27/ios-cant-play-youtube-via-react-useeffect/
I also play audio in my app, and it cycles through them. I was able to get around this by placing my audio files in a map, and use a separate function to call the audio to play.
import hold_female from '../../assets/audio/Female/hold_female.mp3';
import exhale_female from '../../assets/audio/Female/exhale_female.mp3';
import inhale_female from '../../assets/audio/Female/inhale_female.mp3';
import hold_male from '../../assets/audio/Male/hold_male.mp3';
import exhale_male from '../../assets/audio/Male/exhale_male.mp3';
import inhale_male from '../../assets/audio/Male/inhale_male.mp3';
//Props here...
createAudio('Exhale_female', exhale_female); //These place the audio into a map under the name provided.
createAudio('Inhale_female', inhale_female);
createAudio('Hold_female', hold_female);
createAudio('Exhale_male', exhale_male);
createAudio('Inhale_male', inhale_male);
createAudio('Hold_male', hold_male);
const BreatheTest: FC<BreathingProps> = ({ gender }) => {
const [stageText, setStageText] = useState<string>('Inhale');
const [index, setIndex] = useState<number>(0);
const [milliseconds, setMilliseconds] = useState<number>(0); //Set to 0 so the audio plays right away and there is no delay.
const captions = ['Inhale', 'Hold', 'Exhale', 'Hold'];
const playAudioFiles = () => {
playAudio(`${stageText}_${gender}`);
};
useEffect(() => {
const timeout = setTimeout(() => {
stopAll(); //stop all the previous audio files if they are running.
setStageText(captions[index]);
setIndex(index === 3 ? 0 : index + 1);
setMilliseconds(isSafari ? 4500 : 4350);//Sets the timeout to the time of the audio files.
playAudioFiles(); //Plays the audio files as the useEffect runs
}, milliseconds);
return () => clearTimeout(timeout);
}, [index]);
//... render method and everything else.
}
My app is for controlling breathing, and this is how I have gotten past the error you are seeing. From what I have read, iOS just requires some kind of trigger to start any media, audio or video. Putting the play function into a series of play functions kind of satisfies Safari.
It may not work for you, or how your code works, but if this is where we can discuss how we got around iOS's audio control, this is another way.

How to change background based on page url react js?

I have a simple app with a few pages, now I would like to change the background color based on page URL
using react js,
What is expected?:
When a pathname is /movies I want to change the background to red
Here is what I have so far
import React from 'react'
function Testing() {
const[moviesUrlBackgroundColor, setMoviesUrlBackgroundColor] = useState('green');
const getMoviesUrl = window.location.pathname;
if(getMoviesUrl == '/movies'){
setMoviesUrlBackgroundColor('red');
}else{
setMoviesUrlBackgroundColor('green');
}
return (
<div>
<Container style={{backgroundColor:moviesUrlBackgroundColor}}>
Testing
</Container>
</div>
)
}
export default Testing
const Container = styled.div`
background-color:green
`;
Unfortunately, I am getting the following URL
app.js:38323 Uncaught Invariant Violation: Too many re-renders. React limits the number of renders to prevent an infinite loop.
What do I need to do this working?
You should have an extra check to make sure you have set background color or not.Your current code is causing rerender infinte times
import React from 'react'
function Testing() {
const[moviesUrlBackgroundColor, setMoviesUrlBackgroundColor] = useState('green');
const [bgFlag, setbgFlag] = useState(false);
const getMoviesUrl = window.location.pathname;
if(!bgFlag){
setMoviesUrlBackgroundColor(getMoviesUrl == '/movies' ? 'red' : 'green')
setbgFlag(true)
}
return (
<div>
<Container style={{backgroundColor:moviesUrlBackgroundColor}}>
Testing
</Container>
</div>
)
}
export default Testing
const Container = styled.div`
background-color:green
`;
Use an useEffect block so you can perform side-effects effectively.
useEffect(() => {
if(getMoviesUrl === '/movies'){
console.log("running")
setMoviesUrlBackgroundColor('red');
}else{
setMoviesUrlBackgroundColor('green');
}
},[getMoviesUrl]);
The problem is that you call setMoviesUrlBackgroundColor without wrapping in an effect which results in getting called recursively.
In order to fix this, you just simple set state as needed in this case is your pathname has been changed:
React.useEffect(() => {
if (getMoviesUrl == '/movies'){
setMoviesUrlBackgroundColor('red');
} else{
setMoviesUrlBackgroundColor('green');
}
}, [getMoviesUrl])
Ok, I think I got it, you're creating an infinite loop on your first if statement:
if (getMoviesUrl == '/movies') {
// every time you change state, that causes your component
// to re-render, and when it re-renders again you're checking
// changing your state AGAIN, so it's an infinite loop
setMoviesUrlBackgroundColor('red');
}
I'll would recommend using react-route for this and getting the url from params, and then update the background color on componentDidMount or useEffect hook when component mounts for the first time, to prevent infinite loops.
Window.location.pathname was resetting state each render, so it needs to be placed within the useEffect hook to prevent re-renders.
Also, this would be a good use-case to pass props to your styled component.
Also included this in the code below.
Here's a link to a codesandbox I made with the solution.
https://codesandbox.io/s/musing-mirzakhani-njmsh?file=/src/random.js:0-620
import React, { useState, useEffect } from "react";
import styled from "styled-components";
const Container = styled.div`
background: ${(props) => props.backgroundColor || "green"};
`;
const Testing = () => {
const [moviesUrlBackgroundColor, setMoviesUrlBackgroundColor] = useState(
"green"
);
useEffect(() => {
const getMoviesUrl = window.location.pathname;
if (getMoviesUrl === "/movies") {
setMoviesUrlBackgroundColor("yellow");
}
}, [moviesUrlBackgroundColor]);
return (
<div>
<Container backgroundColor={moviesUrlBackgroundColor}>Test</Container>
</div>
);
};
export default Testing;
Cheers!

getElementById after dynamically adding it in React

I am adding Cards dynamically in my React functional component. Cards are stored in State. I map them and give id to each of them. OnClick on those Cards I get their id successfully. Now I want to getElementById to change Card color:
function Clicked(pressedGifId) {
if (pressedGifId === 'correctGif') CorrectMatch();
else WrongMatch();
}
function CorrectMatch(pressedGifId) {
// / THERE I GET Element: null
console.log('Element:', document.getElementById(pressedGifId));
}
function WrongMatch() {
console.log('wrong a match!');
}
export default function GameObject(props) {
const addedToGameGif = [];
const [pressedGifId, gifPressed] = useState(null);
const [photoCards, setPhotoCards] = useState([]);
useEffect(() => {
Clicked(pressedGifId);
}, [pressedGifId]);
// add randomly picked photos to addedToGameGif array
// ...
addedToGameGif.map(gifId =>
photoCards.push(
<Card id={gifId} onClick={() => gifPressed(gifId)}>
text
</Card>,
),
);
return <div>{photoCards}</div>;
}
I tried learning refs but they are only for class components. So how do I reach my element by id in React?
You can use ref in functional component as well. There is a hook called useRef.
Note: Never interact directly with DOM until or unless there is no api available in react to solve the problem for that particular use case.
In react it's not recommended to interact directly with dom. Always use react apis to interact with dom. React is designed to hide the DOM because they want to abstract the DOM away. By using the DOM directly you break the abstraction and make your code brittle to changes introduced in the library.
React is maintaining a virtual DOM if we make any changes in actual DOM directly then react will not be aware of this change and this can lead to some unexpected behavior .
import React, {useState, useRef} from 'react';
export default function GameObject(props) {
const addedToGameGif = [];
const [pressedGifId, gifPressed] = useState(null);
const [photoCards, setPhotoCards] = useState([]);
const elemRef = useRef(null);
useEffect(() => {
Clicked(pressedGifId);
}, [pressedGifId]);
// add randomly picked photos to addedToGameGif array
// ...
addedToGameGif.map(gifId =>
photoCards.push(
<Card ref={elemRef} id={gifId} onClick={() => gifPressed(gifId)}>
text
</Card>
)
);
return <div>{photoCards}</div>;
}
Example from official docs.
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// `current` points to the mounted text input element
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}

Categories

Resources