How to avoid NaN being rendered in audio player duration? - reactjs

I built a cool little audio player and am having issues with data fetching. The page renders before the audio file in the return statement src, here:
<audio ref={audio} src="https://dl.dropbox.com/s/wfhmtvbc5two1wa/1-allen_2991.ogg" alt="oops, something went wrong..."></audio>
The NaN shows up in the duration time represented by this line:
{/* duration */}
<div className={styles.duration}>{(duration && !isNaN(duration)) && calculateTime(duration)}</div>
This above line of code isn't preventing the NaN, so I tried my hand at fetching in the useEffect, shown below but that has made this issue worse.
const [data, setData] = useState([])
--------------------
useEffect(() => {
fetch("https://dl.dropbox.com/s/wfhmtvbc5two1wa/1-allen_2991.ogg").then(
res => setData(res.loadedmetadata)
)
})
--------------------
<audio ref={audio} src={data} alt="oops, something went wrong..."></audio>
If anyone could give it a look and point me in the right direction, id be very grateful. Below I will provide all the code for my component.
import React, { useState, useRef, useEffect } from 'react';
import styles from '../styles/AudioPlayer.module.css';
import {BsArrowClockwise} from 'react-icons/bs';
import {BsArrowCounterclockwise} from 'react-icons/bs';
import {BsPlayCircleFill} from 'react-icons/bs';
import {BsPauseCircleFill} from 'react-icons/bs';
const AudioPlayer = () => {
//state
const [isPlaying, setIsPlaying] = useState(false);
const [duration, setDuration] = useState(0);
const [currentTime, setCurrentTime] = useState(0);
const [data, setData] = useState([])
//refs
const audio = useRef();
const progressBar = useRef();
const progressBarAnimation = useRef();
//effects
useEffect(() => {
const seconds = Math.floor(audio.current.duration);
setDuration(seconds);
progressBar.current.max = seconds;
}, [ audio?.current?.loadedmetadata, audio?.current?.readyState ]);
useEffect(() => {
fetch("https://dl.dropbox.com/s/wfhmtvbc5two1wa/1-allen_2991.ogg").then(
res => setData(res.loadedmetadata)
)
})
//functions & Handlers
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 isPlayingHandler = () => {
const prevValue = isPlaying;
setIsPlaying(!prevValue);
if (!prevValue) {
audio.current.play();
progressBarAnimation.current = requestAnimationFrame(whilePlaying);
} else {
audio.current.pause();
cancelAnimationFrame(progressBarAnimation.current);
};
};
const whilePlaying = () => {
progressBar.current.value = audio.current.currentTime;
progressBarValueTicker();
progressBarAnimation.current = requestAnimationFrame(whilePlaying);
};
const progressHandler = () => {
audio.current.currentTime = progressBar.current.value;
progressBarValueTicker();
};
const progressBarValueTicker = () => {
progressBar.current.style.setProperty('--seek-before-width', `${progressBar.current.value / duration * 100}%`);
setCurrentTime(progressBar.current.value);
}
const backwardFifteen = () => {
console.log(progressBar.current.value)
progressBar.current.value = Number(progressBar.current.value) - 15;
console.log(progressBar.current.value)
progressHandler();
};
const forwardFifteen = () => {
console.log(progressBar.current.value)
progressBar.current.value = Number(progressBar.current.value) + 15;
console.log(progressBar.current.value)
progressHandler();
};
return(
<>
<div>
{/* eventually, a loop component tag will replace the below line to loop all audio file title and descriptions*/}
</div>
<div className={styles.audioWrapper}>
{/* eventually, a loop component tag will replace the below line to loop all audio files*/}
<audio ref={audio} src={data} alt="oops, something went wrong..."></audio>
{/* <audio ref={audio} src="https://dl.dropbox.com/s/wfhmtvbc5two1wa/1-allen_2991.ogg" alt="oops, something went wrong..."></audio> */}
<button className={styles.sideButtons} onClick={backwardFifteen}><BsArrowCounterclockwise />15</button>
<button className={styles.playPauseButton} onClick={isPlayingHandler}>
{ isPlaying ? <BsPauseCircleFill /> : <BsPlayCircleFill /> }</button>
<button className={styles.sideButtons} onClick={forwardFifteen}>15<BsArrowClockwise /></button>
{/* current time */}
<div className={styles.currentTime}>{calculateTime(currentTime)}</div>
{/* progress bar */}
<div>
<input type="range" ref={progressBar} className={styles.progressBar} onChange={progressHandler} defaultValue='0'/>
</div>
{/* duration */}
<div className={styles.duration}>{(duration && !isNaN(duration)) && calculateTime(duration)}</div>
</div>
</>
);
};
export default AudioPlayer;

const onLoadedMetadata = ()=>{
const seconds = Math.floor(audioPlayer.current.duration);
setDuration(seconds);
progressBar.current.max = seconds;
}
<audio ref={audioPlayer} src={audio_file} preload="metadata" onLoadedMetadata={onLoadedMetadata}></audio>

The reason the NaN is rendered is because NaN is a falsy value and will return the value immediately in the expression below.
(duration && !isNaN(duration)) && calculateTime(duration)
// `NaN && !isNaN(NaN)` returns `NaN` because it is falsy
Simply removing the first condition will avoid NaN being rendered.
!isNaN(duration) && calculateTime(duration)
However, the actual duration value will still be NaN and nothing will get rendered. This is because when you check for the audio.current.duration value inside the useEffect the duration hasn't actually updated yet.
To solve this issue, you can listen to the onDurationChange event in the audio element and update the duration state variable when it gets triggered.
// Convert the `useEffect` code into a function instead
const onDurationChangeHandler = (e) => {
const seconds = Math.floor(e.target.duration);
setDuration(seconds);
progressBar.current.max = seconds;
};
<audio
ref={audio}
src="https://dl.dropbox.com/s/wfhmtvbc5two1wa/1-allen_2991.ogg"
alt="oops, something went wrong..."
onDurationChange={onDurationChangeHandler}
></audio>

Related

how to create a timer inside hooks on Reactjs?

I am a starter at React! Started last week ;)
My first project is to create a timer which has a reset function and a second count function.
The reset function is working great, however the timer does not. Which is the best way to do it? It should increase +1s on variable 'second' according to the setTimeout() function.
Is it possible to create a loop on Hooks? I tried to do with the code below, but the page goes down, I think it is because the infinite loop that the code creates;
const [hour, setHour] = useState(4)
const [minute, setMinute] = useState(8)
const [second, setSecond] = useState(12)
// methods
const setTime = (value: string) => {
if (value === 'reset'){
setHour(0);
setMinute(0);
setSecond(0);
}
}
const startTime = () => {
while (second < 60){
setTimeout(() => {
setSecond(second + 1);
}, 1000);
}
};
<div className="d-flex justify-content-center">
<MainButton
variantButton="outline-danger"
textButton="RESET"
functionButton={() => setTime('reset')}
/>
<MainButton
variantButton="outline-success"
textButton="START"
functionButton={() => startTime()}
/>
</div>
Welcome to React! You're very close. setTimeout and setInterval are very similar and for this you can simply use setInterval. No need for a while() loop! Check out this working Sandbox where I created a simple React Hook that you can use in your App.js
https://codesandbox.io/s/recursing-hooks-jc6w3v
The reason your code got caught in an infinite loop is because startTime() function has stale props. Specifically, the second variable is always 0 in this case, because when you defined startTime() on component mount, second was 0. The function doesn't track it's incrementing.
To resolve this issue, instead of:
setSecond(second + 1);
Try using:
setSecond((s) => s += 1);
EDIT* There are many good articles on React Stale Props. Here's one that's helpful: https://css-tricks.com/dealing-with-stale-props-and-states-in-reacts-functional-components/
EDIT** Additional inline examples of the exact issue:
Two changes I would make:
Use setInterval instead of setTimeout in a while() loop.
Create a useTimer hook which handles your timer logic.
App.js
import "./styles.css";
import useTimer from "./useTimer";
export default function App() {
const [setTime, startTime, stopTime, hour, minute, second] = useTimer();
return (
<div>
<div className="d-flex justify-content-center">
<button onClick={() => setTime("reset")}>RESET</button>
<button onClick={startTime}>START</button>
<button onClick={stopTime}>STOP</button>
</div>
<br />
<div>
Hour: {hour} <br />
Minute: {minute} <br />
Second: {second} <br />
</div>
</div>
);
}
useTimer.js
import { useState } from "react";
const useTimer = () => {
const [hour, setHour] = useState(4);
const [minute, setMinute] = useState(8);
const [second, setSecond] = useState(12);
const [timer, setTimer] = useState();
// methods
const clearTimer = () => clearInterval(timer);
const setTime = (value) => {
if (value === "reset") {
setHour(0);
setMinute(0);
setSecond(0);
}
};
const startTime = () => {
if (timer) clearTimer();
const newInterval = setInterval(() => {
setSecond((s) => (s += 1));
}, 1000);
setTimer(newInterval);
};
const stopTime = () => clearTimer();
return [setTime, startTime, stopTime, hour, minute, second];
};
export default useTimer;

How to swipe card programatticaly in react using react-tinder card

I am fetching data from the Backend and loading them in the card using react-tinder-card
Swiping works properly but unable to swipe using the buttons
I follow the documentation but still did not work
Here is the sample
Swiping gestures are working fine.
But when implement by checking documentation things did not work
Things are not working and tomorrow is my project day
import React, { useEffect, useState, useContext, useRef } from "react";
function Wink() {
const [people, setPeople] = useState([]);
const [loading, setLoading] = useState(false);
const [currentIndex, setCurrentIndex] = useState(people.length - 1)
const [lastDirection, setLastDirection] = useState()
// used for outOfFrame closure
const currentIndexRef = useRef(currentIndex)
const childRefs = useMemo(
() =>
Array(people.length)
.fill(0)
.map((i) => React.createRef()),
[]
)
const updateCurrentIndex = (val) => {
setCurrentIndex(val)
currentIndexRef.current = val
}
const canGoBack = currentIndex < people.length - 1
const canSwipe = currentIndex >= 0
// set last direction and decrease current index
const swiped = (direction, nameToDelete, index) => {
setLastDirection(direction)
updateCurrentIndex(index - 1)
}
const outOfFrame = (name, idx) => {
console.log(`${name} (${idx}) left the screen!`, currentIndexRef.current)
// handle the case in which go back is pressed before card goes outOfFrame
currentIndexRef.current >= idx && childRefs[idx].current.restoreCard()
// TODO: when quickly swipe and restore multiple times the same card,
// it happens multiple outOfFrame events are queued and the card disappear
// during latest swipes. Only the last outOfFrame event should be considered valid
}
const swipe = async (dir) => {
if (canSwipe && currentIndex < db.length) {
await childRefs[currentIndex].current.swipe(dir) // Swipe the card!
}
}
// increase current index and show card
const goBack = async () => {
if (!canGoBack) return
const newIndex = currentIndex + 1
updateCurrentIndex(newIndex)
await childRefs[newIndex].current.restoreCard()
}
useEffect(() => {
setLoading(true);
axios
.post("http://localhost:4000/api/all-profile", { email })
.then(function (response) {
setPeople(response.data);
setCurrentIndex(response.data.length);
}
}, []);
return (
<div className="DateMainDiv">
<Header />
<div className="ProfieCards">
{people.map((person) => (
<TinderCard
className="swipe"
key={person.email}
ref={childRefs[index]}
preventSwipe={swipe}
onSwipe={(dir) => swiped(dir, person.name, person.email)}
onCardLeftScreen={onCardLeftScreen}
onCardUpScreen={onCardUpScreen}
>
<div
style={{ backgroundImage: `url(${person.image})` }}
className="Winkcard"
>
<img
onLoad={handleLoad}
src={person.image}
alt="Image"
className="TinderImage"
/>
<h3>
{person.name}{" "}
<IconButton
style={{ color: "#fbab7e" }}
onClick={() => handleOpen(person.email)}
>
<PersonPinSharpIcon fontSize="large" />
{parseInt(person.dist/1000)+"KM Away"}
</IconButton>
</h3>
</div>
</TinderCard>
))}
<SwipeButtonsLeft onClick={()=>{swipe("left")}} />
<SwipeButtonsLeft onClick={()=>{goback()}} />
<SwipeButtonsLeft onClick={()=>{swipe("right")}} />
</div>
</div>
);
}
export default Wink;

Audio is not working properly after playing multiple time in ReactJS

I'm new to React and trying to add some audio to a tenzies game.
The audio is getting weirder every time I click the roll button. And after clicking for a while, the sound is fading away. There is also a warning in the console: 'The AudioContext was not allowed to start.'
I can't find out what causing this issue. Please help!
I don't know how to run react code in StackOverflow. So I'm adding a link to the github repository and live site URL of this game.
Here is the full App.js component's code:
import React from 'react';
import Die from './Components/Die';
import { nanoid } from 'nanoid';
import Confetti from 'react-confetti';
import {Howl} from 'howler';
import winSound from './audio/win.mp3';
import rollSound from './audio/roll.mp3';
import holdSound from './audio/hold.mp3';
export default function App(){
const [dice, setDice] = React.useState(generateNewDice());
const [tenzies, setTenzies] = React.useState(false);
const [audio, setAudio] = React.useState(true);
const [rollCount, setRollCount] = React.useState(0);
const [timer, setTimer] = React.useState(0);
const [timerRunning, setTimerRunning] = React.useState(false);
function holdDieObject(){
return {
value: Math.floor(Math.random()*6) + 1,
isHeld: false,
id: nanoid()
}
}
function generateNewDice(){
let newDice= [];
for(let i=0; i<10; i++){
newDice.push(holdDieObject());
}
return newDice;
}
React.useEffect(()=>{ //Count time per 10 milliseconds when timer is running
let interval;
if(timerRunning){
interval = setInterval(() => {
setTimer((prevTime) => prevTime + 10)
}, 10)
}else{
clearInterval(interval);
}
return () => clearInterval(interval);
},[timerRunning])
React.useEffect(()=>{ //Check all the dice are matched or not
const someDiceHeld = dice.some(die => die.isHeld);
const allDiceHeld = dice.every(die => die.isHeld);
const firstDiceValue = dice[0].value;
const allSameValue = dice.every(die=> die.value === firstDiceValue);
if(someDiceHeld){
setTimerRunning(true);
}
if(allDiceHeld && allSameValue){
setTenzies(true);
// audio && victorySound.play(); // This brings up dependency warning. So moved it to the bottom
setTimerRunning(false)
}
},[dice])
const victorySound = new Howl({
src: [winSound]
})
if(tenzies){
audio && victorySound.play(); // Here
}
const rollDieSound = new Howl({
src: [rollSound]
})
const holdDieSound = new Howl({
src: [holdSound]
})
function holdDice(id){
audio && holdDieSound.play();
setDice(oldDice => oldDice.map(die =>{
return die.id===id ?
{
...die,
isHeld: !die.isHeld
} :
die;
}))
}
function rollDice(){
if(!tenzies){
setDice(oldDice => oldDice.map(die=>{
audio && rollDieSound.play();
return die.isHeld ? die : holdDieObject();
}))
setRollCount(prevCount => prevCount + 1);
}else{
setTenzies(false);
setDice(generateNewDice());
setRollCount(0);
setTimer(0);
}
}
function toggleMute(){
setAudio(prevState => !prevState);
}
function startNewGame(){
setTenzies(false);
setDice(generateNewDice());
}
const minutes = <span>{("0" + Math.floor((timer / 60000) % 60)).slice(-2)}</span>
const seconds = <span>{("0" + Math.floor((timer / 1000) % 60)).slice(-2)}</span>
const milliseconds = <span>{("0" + ((timer / 10) % 100)).slice(-2)}</span>
const dieElements = dice.map((die) => {
return <Die key={die.id}
value={die.value}
isHeld={die.isHeld}
holdDice={()=> holdDice(die.id)}
/>
})
return(
<div>
<main className="board">
<button onClick={toggleMute} className="mute-btn">{audio ? "🔉" : "🔇"}</button>
<h1>Tenzies</h1>
<p>Roll untill the dice are the same. Click each die to freeze it at its current value between rolls.</p>
<div className="die-container">
{dieElements}
</div>
<button onClick={rollDice}>Roll</button>
{tenzies && <div className="scoreboard">
<h2>Congratulations!</h2>
<p className='rollCount'>Rolled: {rollCount}</p>
<p className="rolltime">Time Taken: {minutes}:{seconds}:{milliseconds}</p>
<h3>Your Score: 4500</h3>
<button className='close' onClick={startNewGame}>New Game</button>
</div>}
</main>
{tenzies && <Confetti className="confetti" recycle={false} />}
</div>
)
}

unable to update name and value every 10 seconds

I am trying to implement react counter. every 10 seconds I need to update label and progress bar. But in label I could able to display 1 to 6 in 60 seconds successfully. but in timer due to some issue even though it reaches 60 seconds progress bar percange showing 80% only.
timer logic
const [number, setNumber] = useState(0);
const [progBarVal, setProgBarValr] = useState(0);
useEffect(() => {
if (number >= 6) {
return;
}
const intervalID = setTimeout(() => {
setNumber((t) => t + 1);
setProgBarValr((t) => t + 10);
}, 1000);
return () => clearInterval(intervalID);
}, [number, progBarVal]);
with in the return statement
return{
<div > {number}/6 </div>
<Progress done={progBarVal} />
}
progress bar logic
import React, { useState } from 'react';
import './progress.scss';
const Progress = ({ done }) => {
const [style, setStyle] = useState({});
setTimeout(() => {
const newStyle = {
opacity: 1,
width: `${done}%`,
};
setStyle(newStyle);
}, 200);
return (
<div className='met-prog__progress'>
<div className='met-prog__progress-done' style={style}>
{done}%
</div>
</div>
);
};
export default Progress;
I am trying to do if number is 1 the progBarVal 10 like that.
someone help me to understand where it went wrong.

React useState: unexpected behavior when partitioning infinite feed by time

Context
I have a component that displays an infinite feed of posts. I want to detect the first element that is older than a day so I can display a "posted yesterday" banner above just that element (eventually doing for week and month).
Buggy Solution
I do this by checking if the post is older than a day when rendering my <li> items, but there is some weird behavior when I try to use a boolean state flag to prevent "posted yesterday" being attached to every post older than a day.
Code
Infinite Scroll Component:
export function InfiniteFeed(props: InfiniteFeedProps) {
const [page, setPage] = useState(0);
const [shouldDisplay, setShouldDisplay] = useState(true)
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
const [posts, setPosts] = useState<Post[]>([])
const [hasMore, setHasMore] = useState(false);
const nowMillis = DateTime.local({zone: 'utc'}).toMillis();
// have omitted dataFetching logic + ref observer logic
const timeCheck = (eventTimeMillis: number) => {
if (!shouldDisplay) {
return false
}
if (!olderThanDay(eventTimeMillis, nowMillis)) {
return false
}
setShouldDisplay(false) // setting this renders every time-check(post) to false
return true
}
return (
<div>
<FeedWrapper>
<Banner title="Infinite Feed"/>
<ul style={{padding: 0}}>
{posts.map((post, i) => (
<FeedElement
key={post.id}
ref={(post.length === i + 1) ? lastEventElementRef : () => null}
id={post.id}
display={timeCheck(post.eventTimestamp)} // if true, element prepends "posted yesterday"
// other props
/>
))}
</ul>
</FeedWrapper>
<div></div>
</div>
);
}
Behavior
If I don't do anything with "shouldDisplay" state, the feed correctly prepends every post older than a day with "Posted Yesterday". However, if I set shouldDisplay to false as seen in the code, timeCheck() is always false and "posted yesterday is not displayed".
Ask
Are there any pointers to what is going wrong here?
I had overlooked that setState() causes a re-render, thus making timeCheck() return false every time. To get my desired behavior of a time partitioned feed, I calculated which indices should prepend a time string when I fetched my posts, I then set shouldDisplay() = true if the elements index were in this array. Here is my code
Solution
export function InfiniteFeed(props: InfiniteFeedProps) {
const [page, setPage] = useState(0);
const [shouldDisplay, setShouldDisplay] = useState(true)
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
const [posts, setPosts] = useState<Post[]>([])
const [hasMore, setHasMore] = useState(false);
const [timePartitions, setTimePartitions] = useState<number[]>([]) // indices of time buckets
const now = DateTime.local({zone: 'utc'});
// using luxon library for handling time and using
// Duration api to account for the variable month lengths
const timeBuckets = [
now.minus(Duration.fromObject({month: 1})).toMillis(),
now.minus(Duration.fromObject({week: 1})).toMillis(),
now.minus(Duration.fromObject({day: 1})).toMillis()
];
// have omitted useRef dom observer logic
useEffect(() => {
// ...
CoreApi().post<GetEventsResponse>('posts', request)
.then((res) => {
const allPosts = [...posts, ...res.data.posts]
setPosts(allPosts);
calculateTimePartitions(allEvents)
// ...
})
}, [page])
const shouldDisplay = (index: number): boolean => {
return timePartitions.find(num => num == index) != undefined
}
// calculate the indexes where we want to prepend time banner
const calculateTimePartitions = (eventList: NewsEvent[]) => {
let indexes = [];
let intervals = timeBuckets
for (let i = 0; i < posts.length; i++) {
if (eventList[i].eventTimestamp < intervals[intervals.length - 1] ) {
intervals.pop()
indexes.push(i);
}
}
setTimePartitions(indexes)
}
return (
<div>
<FeedWrapper>
<Banner title="Infinite Feed"/>
<ul style={{padding: 0}}>
{posts.map((post, i) => (
<FeedElement
key={post.id}
ref={(post.length === i + 1) ? lastEventElementRef : () => null}
id={post.id}
display={shouldDisplay(i)} // if true, element prepends "posted yesterday"
// other props
/>
))}
</ul>
</FeedWrapper>
<div></div>
</div>
);
}
Feed element logic
export const FeedElement = forwardRef<any, NewsEvent & FeedElementProps>((props: Post & FeedElementProps, ref) => {
// can be more expensive
const getTimeBucket = () => {
const nowMillis = DateTime.local({zone: 'utc'})
const eventTime = DateTime.fromMillis(props.eventTimestamp, {zone: 'utc'})
const interval = Interval.fromDateTimes(eventTime, nowMillis)
if (interval.length('day') > 1 && interval.length('day') < 2) {
return 'Posted yesterday'
}
if (interval.length('day') > 2 && interval.length('week') < 1) {
return 'Posted this week'
}
if (interval.length('week') > 1 && interval.length('month') < 1) {
return 'Posted this month'
}
else if (interval.length('month') > 1) {
return 'Posted over a month ago'
}
else {
return '';
}
}
return (
<div>
{props.display ? (
<Banner title={getTimeBucket()} />
) : (<></>)}
<div>
<li>
{/*other stuff*/}
</li>
</div>
</div>
)},
);

Resources