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

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"));

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()

Do I have to cancelAnimationFrame before next requestAnimationFrame?

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)
}

How to render the actual value of the scroll percentage?

I have this piece of code
import React from 'react'
const App = () => {
function getScrollPercent() {
var h = document.documentElement,
b = document.body,
st = 'scrollTop',
sh = 'scrollHeight';
var scrollPercent = Math.round((h[st]||b[st]) / ((h[sh]||b[sh]) - h.clientHeight) * 100);
//get the percentage of scroll
return (isNaN(scrollPercent) ? '' : scrollPercent)
}
return (
<p>{`${getScrollPercent()}%`}</p>
)
the problem is when I scroll the scrollPercent doesn't refresh in real time but only when the scroll stop, so scrollPercent can pass from 0% to 100% and not display all the numbers between 0 and 100, so the question is how can I modify my piece of code to display the actual value of scrollPercent even when I scroll
You will need to use a useEffect to add an event listener that will listen for the scroll event. Then, you can register a function that will re-calculate the scroll position when the user scrolls up or down the page.
import React, { useEffect, useState } from "react";
function getScrollPercent() {
var h = document.documentElement,
b = document.body,
st = "scrollTop",
sh = "scrollHeight";
var scrollPercent = Math.round(
((h[st] || b[st]) / ((h[sh] || b[sh]) - h.clientHeight)) * 100
);
//get the percentage of scroll
return isNaN(scrollPercent) ? "" : scrollPercent;
}
const App = () => {
// this represents the current calculated scroll percentage
// maybe initialize this to something other than empty string
const [scrollPercentage, setScrollPercentage] = useState("");
useEffect(() => {
function handleScroll() {
const newScrollPercentage = getScrollPercent();
// calculate and set the new scroll percentage
setScrollPercentage(newScrollPercentage);
}
window.addEventListener("scroll", handleScroll, true);
// clean up event listener when component unmounts
return () => {
window.removeEventListener('scroll', handleScroll, true);
}
}, []);
// this contains extra styling to demo, so that you can see the
// scroll value change when scrolling, probably remove these style props
return (
<div style={{ height: "3000px" }}>
<p style={{ position: "fixed" }}>{`${scrollPercentage}%`}</p>
</div>
);
};
export default App;
Codesandbox Link to example: Codesandbox
You need to use window.addEventListener to reach what you want.
import React from "react";
const App = () => {
const [scroll, setScroll] = React.useState("");
function getScrollPercent() {
var h = document.documentElement,
b = document.body,
st = "scrollTop",
sh = "scrollHeight";
var scrollPercent = Math.round(
((h[st] || b[st]) / ((h[sh] || b[sh]) - h.clientHeight)) * 100
);
setScroll(isNaN(scrollPercent) ? "" : scrollPercent);
}
React.useEffect(() => {
window.addEventListener("scroll", getScrollPercent);
return () => {
window.removeEventListener("scroll", getScrollPercent);
};
}, []);
return (
<div style={{ height: "1000px" }}>
<p style={{ position: "fixed" }}>{`${scroll}%`}</p>
</div>
);
};
export default App;
Codesanbox Link: https://codesandbox.io/s/unruffled-field-3l3z8?file=/src/App.js:0-664

Custom useOneLineText React Hook that adjusts font size and always keeps text on one line

I'm trying to create a custom hook that will keep the text on one line, this is what I've got so far:
import { useEffect, useState, useRef } from "react";
export default function useOneLineText(initialFontSize, text) {
const containerRef = useRef(null);
const [adjustedFontSize, setAdjustedFontSize] = useState(initialFontSize);
useEffect(() => {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
const containerWidth = containerRef.current.getBoundingClientRect().width;
let currentFontSize = initialFontSize;
do {
ctx.font = `${currentFontSize}px "Lato"`;
currentFontSize -= 1;
} while (containerWidth < ctx.measureText(text).width);
setAdjustedFontSize(currentFontSize);
}, []);
return { containerRef, adjustedFontSize, text };
}
And the usage:
import React, { useState } from "react";
import "./style.css";
import useOneLineText from "./useOneLineText";
const initialFontSize = 32;
export default function App() {
const [shortText, setShortText] = useState("Hello Peaceful World!");
const { containerRef, adjustedFontSize, text } = useOneLineText(
initialFontSize,
shortText
);
return (
<div>
<h1
ref={containerRef}
style={{ width: "100px", fontSize: `${adjustedFontSize}px` }}
>
{text}
</h1>
<h1 style={{ width: "100px", fontSize: `${initialFontSize}px` }}>
Hello Cruel World!
</h1>
<button onClick={() => setShortText("Hello World!")}>Remove Peace</button>
</div>
);
}
I was wondering why is my font not updating when trying to change the text with the Remove Peace button. I'm also not sure if looping inside useEffect and decreasing the font size by 1 is the best way to go, is it possible to calculate the adjusted font size without the loop? Here is my working example.
Well, your code already works as it seems. From your example, the initial font size of 32px changes to a 10px on mount. Also, your effect only runs on mount.
Without removing the word, it would also already satisfy the containerWidth < ctx.measureText(text).width.
If you want the effect to happen when the containerWidth or textWidth changes, then you can add more states and add them on your useEffect dependency list.
const [container, setContainerState] = useState(null);
const [textWidth, setTextWidth] = useState(null);
useEffect(() => {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
const refWidth = containerRef.current.getBoundingClientRect().width;
let currentFontSize = initialFontSize;
if (
textWidth !== ctx.measureText(text).width ||
containerWidth !== refWidth
) {
do {
ctx.font = `${currentFontSize}px "Lato"`;
currentFontSize -= 1;
} while (refWidth < ctx.measureText(text).width);
setContainerWidth(containerWidth);
setTextWidth(textWidth);
setAdjustedFontSize(currentFontSize);
}
}, [textWidth, containerWidth]);
For a faster adjusting of font size, you can check this answer out from a jquery question that uses binary search to help you find the right width quicker?
Auto-size dynamic text

"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