Do I have to cancelAnimationFrame before next requestAnimationFrame? - javascript

Here I have a simple timer application in React. Do I need to always call cancelAnimationFrame before calling next requestAnimationFrame in animate method?
I read from somewhere that if I call multiple requestAnimationFrame within single callback, then cancelAnimationFrame is required. But in my example, I am calling single requestAnimationFrame in a single callback.
I got even confused when one said no need while another one said need in this article: https://medium.com/#moonformeli/hi-caprice-liu-8803cd542aaa
function App() {
const [timer, setTimer] = React.useState(0);
const [isActive, setIsActive] = React.useState(false);
const [isPaused, setIsPaused] = React.useState(false);
const increment = React.useRef(null);
const start = React.useRef(null);
const handleStart = () => {
setIsActive(true);
setIsPaused(true);
setTimer(timer);
start.current = Date.now() - timer;
increment.current = requestAnimationFrame(animate);
};
const animate = () => {
setTimer(Date.now() - start.current);
cancelAnimationFrame(increment.current);
increment.current = requestAnimationFrame(animate);
};
const handlePause = () => {
cancelAnimationFrame(increment.current);
start.current = 0;
setIsPaused(false);
};
const handleResume = () => {
setIsPaused(true);
start.current = Date.now() - timer;
increment.current = requestAnimationFrame(animate);
};
const handleReset = () => {
cancelAnimationFrame(increment.current);
setIsActive(false);
setIsPaused(false);
setTimer(0);
start.current = 0;
};
return (
<div className="app">
<div className="stopwatch-card">
<p>{timer}</p>
<div className="buttons">
{!isActive && !isPaused ? (
<button onClick={handleStart}>Start</button>
) : isPaused ? (
<button onClick={handlePause}>Pause</button>
) : (
<button onClick={handleResume}>Resume</button>
)}
<button onClick={handleReset} disabled={!isActive}>
Reset
</button>
</div>
</div>
</div>
);
};
ReactDOM.render(<App/>, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>

No you don't need to cancel the raf in the recursive function, since a raf is just like a setTimeout, but it executes the function in perfect sync with the screen refresh rate, so it executes only once:
const animate = ()=>{
setTimer(Date.now()-start.current)
increment.current = requestAnimationFrame(animate)
}

Related

AudioPlayer does not play after changing track

I want to create an audioplayer on React Nextjs, with some dots, each one corresponding to an audio track. When I click on a dot, it pause the track that currently is playing, if there is one, and play the new one instead.
I have some playing problems.
When I arrive on the page and click on a dots, I need to click it 3 times to play the track.
If I click on a new one, I need to click 2 times to play the new one.
Here is the code :
AudioPlayer.js
import React, { useState, useRef, useEffect } from 'react';
const AudioPlayer = () => {
// state
const [isPlaying, setIsPLaying] = useState(false);
const [duration, setDuration] = useState(0);
const [currentTime, setCurrentTime] = useState(0);
const [currentTrack, setCurrentTrack] = useState(null)
// references
const audioPlayer = useRef(); //reference to our audioplayer
const progressBar = useRef(); //reference to our progressBar
const animationRef = useRef() //reference the animation
useEffect(() => {
const seconds = Math.floor(audioPlayer.current.duration)
setDuration(seconds);
progressBar.current.max = seconds;
}, [audioPlayer?.current?.loadmetadata, audioPlayer?.current?.readyState])
const togglePlayPause = () => {
const prevValue = isPlaying;
setIsPLaying(!prevValue);
if (!prevValue) {
audioPlayer.current.play();
animationRef.current = requestAnimationFrame(whilePlaying);
} else {
audioPlayer.current.pause();
cancelAnimationFrame(animationRef.current);
}
}
const whilePlaying = () => {
progressBar.current.value = audioPlayer.current.currentTime;
setCurrentTime(progressBar.current.value);
animationRef.current = requestAnimationFrame(whilePlaying);
}
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 changeRange = () => {
audioPlayer.current.currentTime = progressBar.current.value;
setCurrentTime(progressBar.current.value);
}
const changeTrack = (e) => {
if (isPlaying) {}
setCurrentTrack(e.target.value)
console.log(e.target.value)
togglePlayPause();
}
return (
<>
<input ref={audioPlayer} className='dots' value='/piste1.mp3' onClick={e => changeTrack(e)}></input>
<input ref={audioPlayer} className='dots left-8' value='/piste2.mp3' onClick={e => changeTrack(e)}></input>
<div>
<audio ref={audioPlayer} src={currentTrack} preload='metadata'></audio>
<button className="mx-5" onClick={togglePlayPause}>
{isPlaying ? "Pause" : "Play"}
</button>
{/* Current time */}
<div>{calculateTime(currentTime)}</div>
{/* progress bar */}
<div>
<input type='range' defaultValue="0" ref={progressBar} onChange={changeRange} />
</div>
{/* duration */}
<div>{(duration && !isNaN(duration)) && calculateTime(duration)}</div>
</div>
</>
)
}
export default AudioPlayer
Why is the audioPlayer being passed as a ref to 3 different elements. That looks like it will produce issues. Only your audio element should have audioPlayer as a ref.
No need for togglePlayPause function in changeTrack. That code looks like it's causing quite some issues as well. Anyways, your chanfeTrack code should look like this:
const changeTrack = (evt) => {
setCurrentTrack(evt.target.value)
};
If the audio doesn't immediately start playing, make sure the onCanPlay event is set to play the audio using something like evt.target.play()

How to test useRef and functions defined inside of useEffect (using Jest)?

I would like to ask for help to improve my skills about test and code quality.
I read a ton of guides and docs and I was not able to find an example of how to do a better code or test in a clean way.
My component has two useRef and it add some listeners to allow the user resize the div element. it works as expected! But I wish to ensure (by tests) that functions onMouseDownRightResize and onMouseMoveRightResize adjust the component using the correct data.
Could someone please show me:
how to test onMouseDownRightResize or onMouseMoveRightResize once the were defined inside the useEffect?
how to test if ref.current is null once he it is inside of the useEffect
Here is my code and a link to a code pen
https://codepen.io/getjv/pen/LYBZPrp
const { useState, useEffect, useRef } = React;
const App = () => {
const ref = useRef(null);
const refRight = useRef(null);
useEffect(() => {
const resizeableEle = ref.current;
if (!resizeableEle) {
return;
}
const styles = window.getComputedStyle(ref.current);
let width = parseInt(styles.width, 10);
let x = 0;
const onMouseMoveRightResize = (event) => {
const dx = event.clientX - x;
x = event.clientX;
width = width + dx;
resizeableEle.style.width = `${width}px`;
};
const onMouseUpRightResize = (event) => {
document.removeEventListener("mousemove", onMouseMoveRightResize);
};
const onMouseDownRightResize = (event) => {
x = event.clientX;
resizeableEle.style.left = styles.left;
resizeableEle.style.right = "";
document.addEventListener("mousemove", onMouseMoveRightResize);
document.addEventListener("mouseup", onMouseUpRightResize);
};
const resizerRight = refRight.current;
if (!resizerRight) {
return;
}
resizerRight.addEventListener("mousedown", onMouseDownRightResize);
return () => {
resizerRight.removeEventListener(
"mousedown",
onMouseDownRightResize
);
};
}, []);
return (
<div className="App">
<div ref={ref} className="flex w-40 border border-red-500 m-5">
<div className="w-full">move the green -> </div>
<div
ref={refRight}
className="bg-green-500 w-2 cursor-col-resize"
></div>
</div>
</div>
);
};
ReactDOM.render(<App />, document.getElementById("app"));

How to fix 'Assertion occurred after test had finished' with Qunit

I have a function for an animation that uses a callback. The function uses promises:
export function createExpandCollapseCallback(
containerSelector,
toExpandCollapseSelector,
animationDuration
) {
return function () {
if ( animating ) {
return;
}
animating = true;
if ( console.time ) {
console.time( 'animation' );
}
const container = document.querySelector( containerSelector );
const toExpandCollapse = container.querySelector(
toExpandCollapseSelector
);
toExpandCollapse.style.display = 'block';
let animation;
if ( expanded ) {
const moveUp = ( moved ) => -moved;
animation = animateHeight(
toExpandCollapse,
animationDuration,
moveUp
);
} else {
const moveDown = ( moved, height ) => moved - height;
animation = animateHeight(
toExpandCollapse,
animationDuration,
moveDown
);
}
animation.then( () => {
animating = false;
if ( console.timeEnd ) {
console.timeEnd( 'animation' );
}
updateExpanded( ! expanded );
} );
};
}
function animateHeight( children, animationDuration, changeTop ) {
const height = children.clientHeight;
const frameDuration = ( 1 / 60 ) * 1000; // 60 fps
return new Promise( ( resolve ) => {
const slideSomeMore = function ( moved, computationTime = 0 ) {
const start = window.performance.now();
const next = start + frameDuration;
while ( moved < height ) {
if ( window.performance.now() < next ) {
continue;
}
const incrementPerMs = height / animationDuration;
const uncomputedIncrement = incrementPerMs * computationTime;
moved = moved + ( incrementPerMs + uncomputedIncrement ) * 1.2;
children.style.top = `${ changeTop( moved, height ) }px`;
const end = window.performance.now();
setTimeout( () => slideSomeMore( moved, end - start ), 0 );
return;
}
resolve();
};
slideSomeMore( 0 );
} );
}
I have set up Qunit in an HTML file. First I render UI in a playground div inside the HTML file and then call the animation callback. My goal is to simulate the animation and verify that the simulation duration is between the animation-duration(3rd parameter passed to the function) + the error margin. I first render a UI AND then perform the animation. Here is the Html file:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>Rusty Inc. Org Chart WordPress Plugin JavaScript Tests</title>
<link rel="stylesheet" href="https://code.jquery.com/qunit/qunit-2.7.1.css">
</head>
<body>
<div id="qunit"></div>
<div id="qunit-fixture"></div>
<div >
<h1>Here is the playground</h1>
<div id="playground"></div>
</div>
<script src="https://code.jquery.com/qunit/qunit-2.7.1.js"></script>
<script type="module">
import { subscribe, updateTree, updateSecretURL, getTree , createExpandCollapseCallback } from '../framework.js';
import { ui } from '../ui.js';
const q = QUnit;
q.module( 'Framework' );
q.test('animation duraion is as much provided', assert=>{
const tree = {"id":1,"name":"Rusty Corp.","emoji":"πŸ•","parent_id":null,"children":[{"id":2,"name":"Food","emoji":"πŸ₯©","parent_id":1,"children":[{"id":"cltp","name":"new","emoji”:β€πŸ€’β€,”parent_id":2,"children":[]}]},{"id":3,"name":"Canine Therapy","emoji":"😌","parent_id":1,"children":[{"id":4,"name":"Massages","emoji":"πŸ’†","parent_id":3,"children":[]},{"id":5,"name":"Games","emoji":"🎾","parent_id":3,"children":[]},{"id":"8xza","name":"lol","emoji":"πŸ˜€","parent_id":3,"children":[]},{"id":"lctu","name":"new","emoji":"πŸ”₯","parent_id":3,"children":[]}]}]};
updateTree( tree );
const secretURL= "random.com"
const insertionElement = document.getElementById("playground")
ui(tree,secretURL,true,insertionElement)
const pluginUI = document.querySelector(`#ui > button`)
console.log('lel', pluginUI );
const durationTime = 1500;
const errorMargin = 200;
const dur2 = 3000
const expandCollapse = createExpandCollapseCallback( '#ui > .team','.children', durationTime );
const done = assert.async();
const start = window.performance.now();
expandCollapse()
const end = window.performance.now();
assert.ok( end - start < durationTime + errorMargin, 'someshit untrue' );
done();
} )
</script>
</body>
</html>
When I run the test I get an alternating message between the testing passing and failing due to assertion being called after the test has finished. This happens every time I refresh, If the test passes and I hit refresh it fails and passes if I refresh again. In this manner, It alternates between passing and failing every time I refresh.
Here are the screenshots for both states:
passing test screenshot
Failing test screensht

How to drag carousel on swipe in React

I'm a junior dev and am struggling with an issue where there is a carousel with about 10 cards on top of the page. All cards do not fit at once on the screen, so there are 2 arrows in the corner to scroll them (arrow left and arrow right). When you click arrow right, the cards scroll to the end and when you click arrow left they move backwards from right to left.
How can I make it that when you click mouse down and hold the images and drag them they move left or right? I need to make them move not only with arrow clicks but with mouse drag also...
Do i need to use some library (i found react-hammerjs, but didn't find any good docs/examples how to use it)?
Thank you in advance for your help
Here's some code to reference:
export const CardsBlock = () => {
const scrollContainer = useRef(null)
const [scrolled, setScrolled] = useState(false)
const dispatch = useDispatch()
useEffect(() => {
const resizeListener = (e) => {
if (e.target.outerWidth <= sizes.mobile) {
setScrolled(null)
} else {
setScrolled(false)
}
}
window.addEventListener('resize', resizeListener)
return () => {
window.removeEventListener('resize', resizeListener)
}
}, [])
useEffect(() => {
if (scrolled) {
scrollTo(scrollContainer.current, scrollContainer.current.offsetWidth)
} else {
scrollTo(scrollContainer.current, 0)
}
}, [scrolled])
return (
<Well>
<Container>
<Wrapper padding={'0 0 16px'} justify={'space-between'} align={'center'}>
<TitleText width={'auto'}>{translator('specilization.title', true)}</TitleText>
<ArrowsContainer>
<Icon
onClick={() => setScrolled(false)}
cursor={'pointer'}
type={'arrow_left'}
color={scrolled ? 'black4' : 'black1'}
/>
<Icon
onClick={() => setScrolled(true)}
cursor={'pointer'}
type={'arrow_right'}
color={scrolled ? 'black1' : 'black4'}
/>
</ArrowsContainer>
</Wrapper>
<SpecializationsInnerContainer ref={scrollContainer}>
{specializations.map((specialization) =>
<SpecializationCard
key={specialization.id}
image={specialization.image}
title={specialization.title}
color={'black1'}
borderColor={'transparent'}
onClick={() => handleOnClick(specialization.id)}
/>
)}
<SpecializationCard borderColor={'black15'} image={'image.png'} isAll onClick={openSpecializationFilter} title={translator('specilization.all', true)} color={'transparent'}/>
</SpecializationsInnerContainer>
</Container>
</Well>
)
}
Since you did not provide any snippet or source code, here is some basic example. It should get you started.
const slider = document.querySelector('.items');
let isDown = false;
let startX;
let scrollLeft;
slider.addEventListener('mousedown', (e) => {
isDown = true;
startX = e.pageX - slider.offsetLeft;
scrollLeft = slider.scrollLeft;
});
slider.addEventListener('mouseleave', () => {
isDown = false;
});
slider.addEventListener('mouseup', () => {
isDown = false;
});
slider.addEventListener('mousemove', (e) => {
if(!isDown) return;
e.preventDefault();
const x = e.pageX - slider.offsetLeft;
const walk = (x - startX) * 3; //scroll-fast
slider.scrollLeft = scrollLeft - walk;
});
https://codepen.io/toddwebdev/pen/yExKoj
During more research found an answer in 'react-swipeable' npm module
https://www.npmjs.com/package/react-swipeable
As the arrow icons onclick logic is already defined here, added the same logic to react swipeable component:
<Swipeable onSwipedLeft={() => setScrolled(true)} onSwipedRight={() => setScrolled(false)} trackMouse={true}>
<SpecializationsInnerContainer ref={scrollContainer}>
{specializations.map((specialization) =>
<SpecializationCard
key={specialization.id}
image={specialization.image}
title={specialization.title}
color={'black1'}
borderColor={'transparent'}
onClick={() => handleOnClick(specialization.id)}
/>
)}
<SpecializationCard borderColor={'black15'} image={'image.png'} isAll onClick={openSpecializationFilter} title={translator('specilization.all', true)} color={'transparent'}/>
</SpecializationsInnerContainer>
</Swipeable>

"useRefs" variable an initial value of a div reference?

Im trying to build an audio progess bar with react hooks. I was following a tutorial with react class based components but got a little lost with refs.
How can I give my useRef variables an initial value of the div ref when the page loads?
As soon as I start playing then the I get an error saying cant read offsetwidth of null. Obviously timeline ref is null as it doesnt have an initial value. How can I connect it to the div with id of timeline in the useEffect hook?
const AudioPlayer = () => {
const url = "audio file";
const [audio] = useState(new Audio(url));
const [duration, setDuration] = useState(0);
const [currentTime, setCurrentTime] = useState(0)
let timelineRef = useRef()
let handleRef = useRef()
useEffect(() => {
audio.addEventListener('timeupdate', e => {
setDuration(e.target.duration);
setCurrentTime(e.target.currentTime)
let ratio = audio.currentTime / audio.duration;
let position = timelineRef.offsetWidth * ratio;
positionHandle(position);
})
}, [audio, setCurrentTime, setDuration]);
const mouseMove = (e) => {
positionHandle(e.pageX);
audio.currentTime = (e.pageX / timelineRef.offsetWidth) * audio.duration;
};
const mouseDown = (e) => {
window.addEventListener('mousemove', mouseMove);
window.addEventListener('mouseup', mouseUp);
};
const mouseUp = (e) => {
window.removeEventListener('mousemove', mouseMove);
window.removeEventListener('mouseup', mouseUp);
};
const positionHandle = (position) => {
let timelineWidth = timelineRef.offsetWidth - handleRef.offsetWidth;
let handleLeft = position - timelineRef.offsetLeft;
if (handleLeft >= 0 && handleLeft <= timelineWidth) {
handleRef.style.marginLeft = handleLeft + "px";
}
if (handleLeft < 0) {
handleRef.style.marginLeft = "0px";
}
if (handleLeft > timelineWidth) {
handleRef.style.marginLeft = timelineWidth + "px";
}
};
return (
<div>
<div id="timeline" ref={(timeline) => { timelineRef = timeline }}>
<div id="handle" onMouseDown={mouseDown} ref={(handle) => { handleRef = handle }} />
</div>
</div>
)
}
The useRef() hook returns a reference to an object, with the current property. The current property is the actual value useRef points to.
To use the reference, just set it on the element:
<div id="timeline" ref={timelineRef}>
<div id="handle" onMouseDown={mouseDown} ref={handleRef} />
And then to use it, you need to refer to the current property:
let position = current.timelineRef.offsetWidth * ratio;
And positionHandle - you shouldn't actually set styles on elements in React in this way. Use the setState() hook, and set the style using JSX.
const positionHandle = (position) => {
let timelineWidth = timelineRef.current.offsetWidth - handleRef.current.offsetWidth;
let handleLeft = position - timelineRef.current.offsetLeft;
if (handleLeft >= 0 && handleLeft <= timelineWidth) {
handleRef.current.style.marginLeft = handleLeft + "px";
}
if (handleLeft < 0) {
handleRef.current.style.marginLeft = "0px";
}
if (handleLeft > timelineWidth) {
handleRef.current.style.marginLeft = timelineWidth + "px";
}
};
In addition, the ref can be also used for other values, such as the new Audio(url), and be extracted from the current property:
const { current: audio } = useRef(new Audio(url));

Categories

Resources