React: window event subscription - reactjs

I am getting into React and trying to create some kind of keyboard trainer app.
I want to add a keypress event listener to the window object to get the letters, and then update the state I created with useState() hook.
So I add the event listener inside useEffect hook. with the custom handler. But the setPhrase function doesn't seem to work well in this case.
Expected result:
After each correct key press the phrase.written to add this key as text, phrase.left to substring by this letter;
Actual result:
State is renewed every time, so the state doesn't update after setPhrase call
The code:
export default () => {
const [initialPhrase] = useState("Test Phrase");
const [phrase, setPhrase] = useState({
left: initialPhrase,
written: "",
});
const handleKeyPress = (event) => {
const requiredLetter = phrase.left.charAt(0);
if (requiredLetter === event.key) {
setPhrase({
written: phrase.written + event.key,
left: phrase.left.substring(1, phrase.left.length),
});
}
console.debug(phrase, requiredLetter, event.key, phrase);
};
useEffect(() => {
window.addEventListener("keypress", handleKeyPress);
return () => window.removeEventListener("keypress", handleKeyPress);
}, []);
return (
<div>
<p className="phrase">
<span className="phrase--part phrase--part__written">
{phrase.written}
</span>
<span className="phrase--part phrase--part__left">{phrase.left}</span>
</p>
</div>
);
};
Playground:
https://stackblitz.com/edit/react-qgbjaf?file=src/index.js
Note: if you place this handler to any element inside component, it is going to work just as expected

The problem is that the handleKeyPress Method is not updated after the phrase state updated. Therefore, you need to add the handleKeyPress method to the useEffect dependency. Now after each phrase state update, the method gets the correct state of phrases
Stackblitz fork: https://stackblitz.com/edit/react-bx5qp7?file=src/index.js
import React, { useState, useEffect, useCallback } from 'react';
import ReactDOM from 'react-dom';
import './style.css';
const KeyboardTrainer = () => {
const [initialPhrase] = useState('Test Phrase');
const [phrase, setPhrase] = useState({
left: initialPhrase,
written: '',
});
const handleKeyPress = (event) => {
const requiredLetter = phrase.left.charAt(0);
if (requiredLetter === event.key) {
setPhrase({
written: phrase.written + event.key,
left: phrase.left.substring(1, phrase.left.length),
});
}
}
useEffect(() => {
window.addEventListener('keypress', (e) => {
handleKeyPress(e);
});
return () => window.removeEventListener("keypress", handleKeyPress);
}, [handleKeyPress]);
return (
<div>
<p className="phrase">
<span className="phrase--part phrase--part__written">
{phrase.written}
</span>
<span className="phrase--part phrase--part__left">{phrase.left}
</span>
</p>
</div>
)};
ReactDOM.render(<KeyboardTrainer />, document.getElementById('root'));

in your event handler method you cant reach the updated state, because state variables don't update itself inside an EventListener. just use useRef like this:
const phraseRef = useRef({
left: initialPhrase,
written: '',
})
const handleKeyPress = (event) => {
const requiredLetter = phraseRef.current.left.charAt(0);
if (requiredLetter === event.key) {
setPhrase({
written: phraseRef.current.written + event.key,
left: phraseRef.current.left.substring(1, phraseRef.current.left.length),
});
phraseRef.current = {
written: phraseRef.current.written + event.key,
left: phraseRef.current.left.substring(1, phrase.left.length),
}
}
console.debug(phrase, requiredLetter, event.key, phrase);
};

Related

How to correctly use Hooks in React?

I am new to React, and I have to build a timeout mechanism for a page. I used react-idle-timer, with some help found on the Internet. However, when I try to access the page, I get a Minified React error #321, in which it tells me that I used hooks incorrectly.
Can you please take a look on the following code and point me in the right direction? Thanks
import React from "react"
import NavBar from "./Navbar"
import "../styles/Upload.css"
import LinearProgressWithLabel from "./LinearProgressWithLabel"
import axios from "axios"
import Logout from "./Logout"
import { useIdleTimer } from 'react-idle-timer'
import { format } from 'date-fns'
export default function Upload() {
const [selectedFile, setSelectedFile] = React.useState();
const [progress, setProgress] = React.useState(0);
const timeout = 3000;
const [remaining, setRemaining] = React.useState(timeout);
const [elapsed, setElapsed] = React.useState(0);
const [lastActive, setLastActive] = React.useState(+new Date());
const [isIdle, setIsIdle] = React.useState(false);
const handleOnActive = () => setIsIdle(false);
const handleOnIdle = () => setIsIdle(true);
const {
reset,
pause,
resume,
getRemainingTime,
getLastActiveTime,
getElapsedTime
} = useIdleTimer({
timeout,
onActive: handleOnActive,
onIdle: handleOnIdle
});
const handleReset = () => reset();
const handlePause = () => pause();
const handleResume = () => resume();
React.useEffect(() => {
setRemaining(getRemainingTime())
setLastActive(getLastActiveTime())
setElapsed(getElapsedTime())
setInterval(() => {
setRemaining(getRemainingTime())
setLastActive(getLastActiveTime())
setElapsed(getElapsedTime())
}, 1000)
}, []);
function changeHandler(event) {
setSelectedFile(event.target.files[0])
};
function handleSubmission() {
if (selectedFile) {
var reader = new FileReader()
reader.readAsArrayBuffer(selectedFile);
reader.onload = () => {
sendFileData(selectedFile.name, new Uint8Array(reader.result), 4096)
};
}
};
function sendFileData(name, data, chunkSize) {
function sendChunk(offset) {
var chunk = data.subarray(offset, offset + chunkSize) || ''
var opts = { method: 'POST', body: chunk }
var url = '/api/uploaddb?offset=' + offset + '&name=' + encodeURIComponent(name)
setProgress(offset / data.length * 100)
fetch(url, opts).then(() => {
if (chunk.length > 0) {
sendChunk(offset + chunk.length)
}
else {
axios.post('/api/uploaddb/done', { name })
.then(setProgress(100))
.catch(e => console.log(e));
}
})
}
sendChunk(0);
};
return (
<div>
<NavBar />
<div>
<div>
<h1>Timeout: {timeout}ms</h1>
<h1>Time Remaining: {remaining}</h1>
<h1>Time Elapsed: {elapsed}</h1>
<h1>Last Active: {format(lastActive, 'MM-dd-yyyy HH:MM:ss.SSS')}</h1>
<h1>Idle: {isIdle.toString()}</h1>
</div>
<div>
<button onClick={handleReset}>RESET</button>
<button onClick={handlePause}>PAUSE</button>
<button onClick={handleResume}>RESUME</button>
</div>
</div>
<h1>Upload</h1>
<input type="file" name="file" onChange={changeHandler} />
{!selectedFile ? <p className="upload--progressBar">Select a file</p> : <LinearProgressWithLabel className="upload--progressBar" variant="determinate" value={progress} />}
<br />
<div>
<button disabled={!selectedFile} onClick={handleSubmission}>Submit</button>
</div>
</div>
)
}
Well, in this case, you should avoid setting states inside the useEffect function, because this causes an infinite loop. Everytime you set a state value, your component is meant to render again, so if you put states setters inside a useEffect function it will cause an infinite loop, because useEffect function executes once before rendering component.
As an alternative you can set your states values outside your useEffect and then put your states inside the useEffect array param. The states inside this array will be "listened" by useEffect, when these states change, useEffect triggers.
Something like this:
React.useEffect(() => {
}, [state1, state2, state3]);
state anti-pattern
You are using a state anti-pattern. Read about Single Source Of Truth in the React Docs.
react-idle-timer provides getRemainingTime, getLastActiveTime and getElapsedTime
They should not be copied to the state of your component
They are not functions
getRemainingTime(), getLastActiveTime(), or getElapsedTime() are incorrect
To fix each:
getRemainingTime should not be stored in state of its own
Remove const [remaining, setRemaining] = useState(timeout)
Remove setRemaining(getRemainingTime) both places in useEffect
Change <h1>Time Remaining: {remaining}</h1>
To <h1>Time Remaining: {getRemainingTime}</h1>
The same is true for lastActive.
getLastActive should be be stored in state of its own
Remove const [lastActive, setLastActive] = React.useState(+new Date())
Remove setLastActive(getLastActiveTime()) both places in useEffect
Change <h1>Last Active: {format(lastActive, 'MM-dd-yyyy HH:MM:ss.SSS')}</h1>
To <h1>Last Active: {format(getLastActive, 'MM-dd-yyyy HH:MM:ss.SSS')}</h1>
And the same is true for elapsed.
getElapsedTime should be be stored in state of its own
Remove const [elapsed, setElapsed] = React.useState(+new Date())
Remove setElapsed(getElapsedTime()) both places in useEffect
Change <h1>Time Elapsed: {elapsed}</h1>
To <h1>Time Elapsed: {getElapsedTime}</h1>
remove useEffect
Now your useEffect is empty and it can be removed entirely.
unnecessary function wrappers
useIdleTimer provides reset, pause, and resume. You do not need to redefine what is already defined. This is similar to the anti-pattern above.
Remove const handleReset = () => reset()
Change <button onClick={handleReset}>RESET</button>
To <button onClick={reset}>RESET</button>
Remove const handlePause = () => pause()
Change <button onClick={handlePause}>PAUSE</button>
To <button onClick={pause}>PAUSE</button>
Remove const handleResume = () => resume()
Change <button onClick={handleResume}>RESUME</button>
To <button onClick={resume}>RESUME</button>
avoid local state
timeout should be declared as a prop of the Upload component
Remove const timeout = 3000
Change function Upload() ...
To function Upload({ timeout = 3000 }) ...
To change timeout, you can pass a prop to the component
<Upload timeout={5000} />
<Upload timeout={10000} />
use the provided example
Read Hook Usage in the react-idle-timer docs. Start there and work your way up.
import React from 'react'
import { useIdleTimer } from 'react-idle-timer'
import App from './App'
export default function (props) {
const handleOnIdle = event => {
console.log('user is idle', event)
console.log('last active', getLastActiveTime())
}
const handleOnActive = event => {
console.log('user is active', event)
console.log('time remaining', getRemainingTime())
}
const handleOnAction = event => {
console.log('user did something', event)
}
const { getRemainingTime, getLastActiveTime } = useIdleTimer({
timeout: 1000 * 60 * 15,
onIdle: handleOnIdle,
onActive: handleOnActive,
onAction: handleOnAction,
debounce: 500
})
return (
<div>
{/* your app here */}
</div>
)
}

React: scrollIntoView not working following by dependency of useEffect

I have got a dependency imageNo in useEffect() as I want the element to go up when it's being hidden, but scrollIntoView() does not work properly whenever imageNo changes, but it works when clicking a button.
Updated
import React, { useEffect, useRef, useState } from 'react';
const Product = ({ product }) => {
const moveRef = useRef(product.galleryImages.edges.map(() => React.createRef()));
const [imageNo, setImageNo] = useState(0);
useEffect(() => {
const position = moveRef.current[imageNo]?.current.getBoundingClientRect().y;
console.log('imageNo', imageNo); // <<<<----- This is also called whenever scrolling excutes.
if (position > 560) {
moveRef.current[imageNo]?.current.scrollIntoView({
behavior: 'smooth',
block: 'center',
});
}
}, [imageNo]);
const test = () => {
const position = moveRef.current[imageNo]?.current.getBoundingClientRect().y;
if (position > 560) {
moveRef.current[imageNo]?.current.scrollIntoView({
behavior: 'smooth',
block: 'center',
});
}
};
// This changes `imageNo`
const handleScroll = () => {
let id = 0;
console.log('refs.current[id]?.current?.getBoundingClientRect().y', refs.current[id]?.current?.getBoundingClientRect().y);
const temp = imgArr?.find((el, id) => refs.current[id]?.current?.getBoundingClientRect().y >= 78);
if (!temp) id = 0;
else id = temp.id;
if (refs.current[id]?.current?.getBoundingClientRect().y >= 78) {
setImageNo(id);
}
};
useEffect(() => {
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, []);
return (
<div className="flex flex-row layout-width ">
{/* aside */}
<div className="sticky flex self-start top-[76px] overflow-y-auto !min-w-[110px] max-h-[90vh]">
<div className="">
{product.galleryImages.edges.map((image, i) => {
return (
<div ref={moveRef.current[i]} key={image.node.id}>
<Image />
</div>
);
})}
</div>
</div>
<button onClick={test}>btn</button>
</div>
);
};
export default Product;
Any suggestion will be greatly appreciated.
I couldn't see where the imageNo is coming from?
If it is just a normal javascript variable then it wouldn't trigger re-render even after putting it inside the useEffect's dependencies array.
If you want to make the re-render happen based on imageNo then make a useState variable for imageNo and change it using setter function that useState provides.
Helpful note - read about useState and useEffect hooks in React.

ReactJs : useRef current getting null

I am trying to turnoff camera and flashlight when the component gets unmount , I am using react hook I have two function startCamera() and stopCamera() , I am calling startcamera when the component gets mount and stop camera when component gets unmount.
But its showing me error when stopCamera is called while unmounting
i have created an another button to test if StopCamera() working and i found its working , but i want to call the function when component is getting unmounted
my code:
CameraScreen.js
import "./styles.css";
import { useState, useEffect, useRef } from "react";
export default function CameraScreen() {
const videoElement = useRef(null);
const [facingMode, setFacingMode] = useState("environment");
const handleFacingModeToggle = () => {
stopCamera();
facingMode === "environment"
? setFacingMode("user")
: setFacingMode("environment");
};
useEffect(() => {
// const getUserMedia = async () => {
// try {
// const stream = await navigator.mediaDevices.getUserMedia({
// video: true
// });
// videoElement.current.srcObject = stream;
// } catch (err) {
// console.log(err);
// }
// };
// getUserMedia();
startCamera();
return function cleanup() {
stopCamera();
};
}, []);
const stopCamera = () =>
videoElement.current.srcObject &&
videoElement.current.srcObject.getTracks().forEach((t) => t.stop());
function startCamera() {
if (navigator.mediaDevices.getUserMedia) {
navigator.mediaDevices
.getUserMedia({
video: { facingMode: facingMode },
width: { ideal: 1280 },
height: { ideal: 720 }
})
.then(function (stream) {
if (videoElement.current) videoElement.current.srcObject = stream;
const track = stream.getVideoTracks()[0];
//Create image capture object and get camera capabilities
const imageCapture = new ImageCapture(track);
const photoCapabilities = imageCapture
.getPhotoCapabilities()
.then(() => {
//todo: check if camera has a torch
//let there be light!
track.applyConstraints({
advanced: [{ torch: true }]
});
});
})
.catch(function (error) {
alert("Please check your device permissions");
console.log("Something went wrong!");
console.log(error);
});
if (videoElement.current)
videoElement.current.onloadeddata = function () {
if (window.NativeDevice)
window.NativeDevice.htmlCameraReadyToRecord(true);
};
}
}
return (
<>
<video
autoPlay={true}
ref={videoElement}
style={{
minHeight: "67.82vh",
maxHeight: "67.82vh",
maxWidth: "100%",
minWidth: "100%"
}}
className="border-3rem bg-[#666]"
></video>
<button onClick={stopCamera}> stopCamera</button>
</>
);
}
App.js
import "./styles.css";
import { useState } from "react";
import CameraScreen from "./cameraScreen";
export default function App() {
const [switchS, setSwitchS] = useState(false);
return (
<div>
<button className="" onClick={() => setSwitchS(!switchS)} value="switch">
switch
</button>
{switchS && <CameraScreen />}
{!switchS && "Blank Screen"}
</div>
);
}
PS: the above code working at :https://5t2to.csb.app/
codesandbox link : https://codesandbox.io/s/practical-fast-5t2to?file=/src/cameraScreen.js
You can use useLayoutEffect hook. It works just before unmounting, like componentWillUnmount.
Here is an example to that
https://codesandbox.io/s/happy-swartz-ikqdn?file=/src/random.js
You can go to https://ikqdn.csb.app/rand in sandbox browser and check the console on clicking to home button.
You can see the difference in working while unmounting of both useEffect and useLayoutEffect
It preserves ref.current, so what you can do is, you can pass ref.current in the function that you are calling just before unmounting, to prevent ref to the dom elment.
It took a bit of debugging/sleuthing to find the issue. So even though you have a ref attached to the video element, when the component is unmounted the ref is still mutated and becomes undefined. The solution is to save a reference to the current videoElement ref value and use this in a cleanup function.
useEffect(() => {
startCamera();
const ref = videoElement.current;
return () => {
ref.srcObject.getTracks().forEach((t) => t.stop());
};
}, []);
Simply add useLayoutEffect to stop camera
useEffect(() => {
// const getUserMedia = async () => {
// try {
// const stream = await navigator.mediaDevices.getUserMedia({
// video: true
// });
// videoElement.current.srcObject = stream;
// } catch (err) {
// console.log(err);
// }
// };
// getUserMedia();
startCamera();
}, []);
useLayoutEffect(()=>()=>{
stopCamera();
},[]);
Just need to change useEffect() to useLayoutEffect()and it will works like a charm.
useLayoutEffect(() => {
const ref = videoElement;
console.log(ref);
startCamera();
return function cleanup() {
stopCamera();
};
}, []);
sanbox link :- https://codesandbox.io/s/naughty-murdock-by5tc?file=/src/cameraScreen.js:415-731

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

Scroll to element on page load with React Hooks

I'm trying to create a functional component that fetches data from an API and renders it to a list. After the data is fetched and rendered I want to check if the URL id and list item is equal, if they are then the list item should be scrolled into view.
Below is my code:
import React, { Fragment, useState, useEffect, useRef } from "react";
export default function ListComponent(props) {
const scrollTarget = useRef();
const [items, setItems] = useState([]);
const [scrollTargetItemId, setScrollTargetItemId] = useState("");
useEffect(() => {
const fetchData = async () => {
let response = await fetch("someurl").then((res) => res.json());
setItems(response);
};
fetchData();
if (props.targetId) {
setScrollTargetItemId(props.targetId)
}
if (scrollTarget.current) {
window.scrollTo(0, scrollTarget.current.offsetTop)
}
}, [props]);
let itemsToRender = [];
itemsToRender = reports.map((report) => {
return (
<li
key={report._id}
ref={item._id === scrollTargetItemId ? scrollTarget : null}
>
{item.payload}
</li>
);
});
return (
<Fragment>
<ul>{itemsToRender}</ul>
</Fragment>
);
}
My problem here is that scrollTarget.current is always undefined. Please advice what I'm doing wrong. Thanks in advance.
Using useCallback, as #yagiro suggested, did the trick!
My code ended up like this:
const scroll = useCallback(node => {
if (node !== null) {
window.scrollTo({
top: node.getBoundingClientRect().top,
behavior: "smooth"
})
}
}, []);
And then I just conditionally set the ref={scroll} on the node that you want to scroll to.
That is because when a reference is changed, it does not cause a re-render.
From React's docs: https://reactjs.org/docs/hooks-reference.html#useref
Keep in mind that useRef doesn’t notify you when its content changes. Mutating the .current property doesn’t cause a re-render. If you want to run some code when React attaches or detaches a ref to a DOM node, you may want to use a callback ref instead.
constructor(props) {
thi.modal = React.createRef();
}
handleSwitch() {
// debugger
this.setState({ errors: [] }, function () {
this.modal.current.openModal('signup') // it will call function of child component of Modal
});
// debugger
}
return(
<>
<button className="login-button" onClick={this.handleSwitch}>Log in with email</button>
<Modal ref={this.modal} />
</>
)
React Hooks will delay the scrolling until the page is ready:
useEffect(() => {
const element = document.getElementById('id')
if (element)
element.scrollIntoView({ behavior: 'smooth' })
}, [])
If the element is dynamic and based on a variable, add them to the Effect hook:
const [variable, setVariable] = useState()
const id = 'id'
useEffect(() => {
const element = document.getElementById(id)
if (element)
element.scrollIntoView({ behavior: 'smooth' })
}, [variable])

Resources