Edit:
I have an example on codepen showing the effects I want to implement. It is working, but I'm not sure if it is cleanly done to satisfy React 18's StrictMode.
https://codepen.io/yochess/pen/NWMOvrv?editors=0110
Question:
I have code where I have 2 div elements representing a left arrow and a right arrow.
When I hover over and click the arrow div element, I have css code that changes their styling using hover and active.
//file.css
.left-arrow-wrapper:active {
color: black;
}
.left-arrow-wrapper:hover{
background: rgb(254 226 226) !important;
}
.right-arrow-wrapper:active {
color: black;
}
.right-arrow-wrapper:hover{
background: rgb(254 226 226) !important;
}
I want to sort of emulate this effect with onkeydown.
For example, if the left arrow is clicked then setState is invoked on the left arrow's stylings. 0.1 seconds later using setTimeout, a 2nd setState is invoked on the left arrow and its styling would revert back to its original state.
As a result, useEffect is visited a few times. I want to make sure if a user is spamming the left arrow, then while its styling is changed, I want no effects to take place.
I am new to React and Hooks and the code above works on React 17, but when I change to React 18, the code is bugged. I'm assuming I am incorrectly implementing this effect. Is there a more proper way to accomplish this?
A side question is how do I properly clean up the setTimeouts on unmount? In the code below, I push all the setTimeouts into an array, and then set them to null once they are called. Then on unmount, I would return a function that clears the setTimeouts that are not null.
Here is the code:
import React, { useState, useEffect, useRef } from 'react'
import {
FaAngleLeft,
FaAngleRight,
} from "react-icons/fa"
const setStyles = {
defaultArrowStyle:{
backgroundColor: "lightgray",
borderStyle: "ridge"
},
clickedArrowStyle: {
backgroundColor: "rgb(254 226 226)",
borderStyle: "ridge"
},
}
const ArrowButtons = () => {
const [leftArrowStyle, setLeftArrowStyle] = useState(setStyles.defaultArrowStyle)
const [rightArrowStyle, setRightArrowStyle] = useState(setStyles.defaultArrowStyle)
const timeRef = useRef([])
const counterRef = useRef(0)
useEffect(() => {
window.addEventListener("keydown", handleKey)
return () => {
window.removeEventListener("keydown", handleKey);
}
}, [handleKey])
useEffect(() => {
if (rightArrowStyle.backgroundColor !== setStyles.clickedArrowStyle.backgroundColor) return
const counter = counterRef.current
const timer = setTimeout(() => cacheAndSetArrowStyle(setRightArrowStyle, counter), 100)
timeRef.current[counterRef.current++] = timer
}, [rightArrowStyle])
useEffect(() => {
if (leftArrowStyle.backgroundColor !== setStyles.clickedArrowStyle.backgroundColor) return
const counter = counterRef.current
const timer = setTimeout(() => cacheAndSetArrowStyle(setLeftArrowStyle, counter), 100)
timeRef.current[counterRef.current++] = timer
}, [leftArrowStyle])
useEffect(() => {
return () => {
// eslint-disable-next-line
timeRef.current.filter(timer => timer).forEach(timer => clearTimeout(timer))
}
}, [])
return (
<div className="row">
<div className="col-2 hand-icon text-center left-arrow-wrapper" style={leftArrowStyle} onClick={handleLeftClick}>
<FaAngleLeft className="left-arrow" />
</div>
<div className="col-2 hand-icon text-center right-arrow-wrapper" style={rightArrowStyle} onClick={handleRightClick}>
<FaAngleRight className="double-right-arrow" />
</div>
</div>
)
function handleKey(event) {
if (event.key === "ArrowRight") {
setRightArrowStyle(setStyles.clickedArrowStyle)
}
if (event.key === "ArrowLeft") {
setLeftArrowStyle(setStyles.clickedArrowStyle)
}
}
function cacheAndSetArrowStyle(setArrowStyle, counter) {
timeRef.current[counter] = null
setArrowStyle(setStyles.defaultArrowStyle)
}
}
export default React.memo(ArrowButtons)
Related
This ugly code works. Every second viewportHeight is set to the value of window.visualViewport.height
const [viewportHeight, setViewportHeight] = React.useState(0);
React.useEffect(() => {
setInterval(() => {
setViewportHeight(window.visualViewport.height);
}, 1000);
}, []);
However this doesn't work. viewportHeight is set on page load but not when the height changes.
React.useEffect(() => {
setViewportHeight(window.visualViewport.height);
}, [window.visualViewport.height]);
Additional context: I need the page's height in state and I need the virtual keyboard's height to be subtracted from this on Mobile iOS.
You can only use state variables managed by React as dependencies - so a change in window.visualViewport.height will not trigger your effect.
You can instead create a div that spans the whole screen space and use a resize observer to trigger effects when its size changes:
import React from "react";
import useResizeObserver from "use-resize-observer";
const App = () => {
const { ref, width = 0, height = 0 } = useResizeObserver();
const [viewportHeight, setViewportHeight] = React.useState(height);
React.useEffect(() => {
setViewportHeight(window.visualViewport.height);
}, [height]);
return (
<div ref={ref} style={{ width: "100vw", height: "100vh" }}>
// ...
</div>
);
};
This custom hook works:
function useVisualViewportHeight() {
const [viewportHeight, setViewportHeight] = useState(undefined);
useEffect(() => {
function handleResize() {
setViewportHeight(window.visualViewport.height);
}
window.visualViewport.addEventListener('resize', handleResize);
handleResize();
return () => window.visualViewport.removeEventListener('resize', handleResize);
}, []);
return viewportHeight;
}
I wrote a program that when we click a button until we release the key, the function runs continuously
In each run, one unit is added up each time and one unit is reduced from the bottom, and each time the result is printed on the console.
I want this operation to be done continuously until the key is released
But I do not know where the problem is that it does not work properly
Please help me, thank you
const [step, setStep] = useState({ top: 0, bottom: 0 });
let timer;
function stop() {
clearInterval(timer);
console.log(step)
}
function handleHello() {
repeat(() => setStep({ top: step.top + 1, bottom: step.bottom - 1 }));
}
function repeat(what) {
timer = setInterval(what, 100);
what();
}
return (
<div style={{textAlign:"center"}}>
<h1>Hello</h1>
<h3>The output is on the console</h3>
<button onMouseDown={handleHello} onMouseUp={stop}>
please press
</button>
</div>
);
}
I think you're making it mostly correct, but you have a small problem with the timer and setState. You should use useEffect to track mouse behaviour instead of calling setState.
If you're still doubting about it, you can try it out in this sandbox
import { useState, useEffect } from 'react'
let timer; //you need to have a global variable for your `timer`
const YourComponent = () => {
const [step, setStep] = useState({ top: 0, bottom: 0 });
const [isMouseHolding, setIsMouseHolding] = useState(false)
useEffect(() => {
if(isMouseHolding) { //if holding mouse, we trigger `repeat`
repeat(() => setStep(prevState => ({ top: prevState.top + 1, bottom: prevState.bottom - 1 })))
} else {
stop() //stop it if releasing mouse holding
}
}, [isMouseHolding]) //to track mouse holding behaviour changes
const stop = () => {
clearInterval(timer);
setIsMouseHolding(false);
}
const handleHello = () => {
setIsMouseHolding(true)
}
const repeat = (what) => {
timer = setInterval(what, 100)
}
return (
<div style={{textAlign:"center"}}>
<h1>Hello</h1>
<h3>The output is on the console</h3>
<button onMouseDown={handleHello} onMouseUp={stop}>
please press
</button>
<div>Top {step.top} and down {step.bottom}</div>
</div>
);
}
export default YourComponent
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.
Hi I am new developer at ReactJs world. I have a question. I have value variable with initial value as 1. But I have a problem while increasing it. In JavaScript I can incarease an any value one by one but I did not make same thing using Hooks. The thing which I want to do is changing background image with time. Could you help me at this issue? How can I change my background image with time ?
my example tsx.part:
import React, { useEffect, useState } from 'react';
const LeftPart = (props: any) => {
let imgNumber : number = 1;
const [value, setValue] = useState(1);
useEffect(() => {
const interval = setInterval(() => {
imgNumber = imgNumber + 1;
setValue(value+1);
console.log(imgNumber)
console.log(value)
}, 3000);
return () => clearInterval(interval);
}, []);
return (
<div className="col-xl-7 col-lg-7 col-md-7 col-sm col-12">
<img id="image" src={"../../../assets/images/bg"+{value}+".jpg"} style={{ width: "100%", height: "99vh" }} alt="Login Images"></img>
</div >
)
}
export default LeftPart;
Your issue is the useEffect block dependency list (that empty array). When you explicitly set no dependencies, React will call the callback on first render and never again. If you want it to continuously change, just remove that second parameter entirely. If you implicitly leave no useEffect dependencies, it is called on every render.
Fixed:
useEffect(() => {
const interval = setInterval(() => {
imgNumber = imgNumber + 1;
setValue(value+1);
console.log(imgNumber)
console.log(value)
}, 3000);
return () => clearInterval(interval);
});
Hello I have Letters.js which generates AvailableLetter for a-z.
import React, {useState} from 'react';
import AvailableLetter from './AvailableLetter/AvailableLetter';
import classes from './Letters.module.css';
const Letters = (props) => {
const [allLetters]=useState(
['a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z']
);
const playHandler = (alphabet) => {
const solution = props.solution.split('');
console.log(solution);
if (solution.indexOf(alphabet)<0)
{
console.log('incorrect');
return false;
}
else
{
console.log('correct');
return true;
}
}
const availableLetters = [ ...allLetters ].map(
(alphabet,i) => {
return (
<AvailableLetter setSolved={props.setSolved} play={()=>playHandler(alphabet)} correct={()=>props.correct(alphabet)} incorrect={()=>props.incorrect(alphabet)} solution={props.solution} key={i} alphabet={alphabet} />
);
}
);
return (
<div className={classes.Letters}>
<p>Solution: {props.solution}</p>
<div className={classes.AvailableLetters}>
{availableLetters}
</div>
</div>
);
}
export default Letters;
I have AvailableLetter.js here and I want it to be unclickable after first time clicked.
import React, {useState, useEffect} from 'react';
import classes from './AvailableLetter.module.css';
import Ax from '../../hoc/Ax';
const AvailableLetter = (props) => {
// const [show,setShow]=useState(true);
// const [clicked, setClicked]=useState(false);
// const [outcome,setOutcome]=useState(false);
const [clicked,setClicked]=useState(false);
// if (show)
// {
// setClicked(true);
// }
// const play = (alphabet) => {
// const solution = props.solution.split('');
// if (solution.indexOf(alphabet)<0)
// {
// return false;
// }
// else
// {
// return true;
// }
// }
const setStuff = () => {
// setShow(true);
setClicked(false);
props.setSolved();
};
useEffect( setStuff,[clicked] );
// useEffect( ()=>setShow(true),[show] );
// useEffect( ()=>props.setSolved(),[show] );
if (clicked) // STRANGELY THIS PART WORKS!!!
{
if (props.play())
{
props.correct();
// alert('correct');
}
else
{
props.incorrect();
// alert('wrong');
}
}
const attachedClasses = [classes.AvailableLetter];
const disableLetter = () => {
attachedClasses.push(classes.Disabled);
setClicked(true);
};
// const letter = <span onClick={disableLetter} className={attachedClasses.join(' ')} >{props.alphabet}</span>;
let letter=null;
if (!clicked)
{
letter = <span onClick={disableLetter} className={attachedClasses.join(' ')} >{props.alphabet}</span>;
}
else if(clicked) // CODE NEVER GETS HERE!!!!!!!!!!!!!!
{
letter = <span className={attachedClasses.join(' ')} >{props.alphabet}</span>;
}
return (
<Ax>
{letter}
</Ax>
);
}
export default AvailableLetter;
The CSS file for it is AvailableLetter.module.css:
.AvailableLetter
{
border: 1px solid black;
padding: 10px;
margin: 3px;
}
.AvailableLetter.Disabled
{
pointer-events: none;
background: #aaa;
}
It seems my logic inside AvailableLetter is correct, but it never reaches the else if (clicked) part and letters remain always clickable.
Inside AvailableLetter.js: If I use button instead:
<button disable={clicked} onClick={()=>setClicked(true)}>props.alphabet</button>
Strangely disable doesn't work even when setClicked(true).
But if I do
<button disable>props.alphabet</button>
Now it disables.
I appreciate your help!
Update:
Removing setClicked(false) from setStuff() gets error:
Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
Your combination of effects and hooks have created a feedback loop.
This button:
<span onClick={disableLetter} className={attachedClasses.join(' ')} >{props.alphabet}</span>
calls this function:
const disableLetter = () => {
attachedClasses.push(classes.Disabled);
setClicked(true);
};
which sets clicked to true. Once that happens, this effect runs:
const setStuff = () => {
// setShow(true);
setClicked(false);
props.setSolved();
};
useEffect( setStuff,[clicked] );
which immediately makes clicked == false again. Also worth noting that setStuff gets called a second time because clicked changed values, triggering the effect again. What is setStuff supposed to do in this context? Remove the call to setClicked(false) in that function and clicked should remain as true.
I'd highly recommend cleaning up your code so it's easier to follow. Logic like this:
if (!clicked) {
// not clicked
} else if (clicked) {
// clicked
}
could easily be described like this:
if (clicked) {
// clicked
} else {
// not clicked
}
By doing this, you'll save yourself a lot of headaches when debugging problems like the one you're having.
Maximum Update Depth Error
Based on your stack trace and code in the question/pastebin, you have another loop caused by this part:
if (clicked)
{
if (props.play())
{
props.correct();
// alert('correct');
}
else
{
props.incorrect(); // <- this is your trigger
// alert('wrong');
}
}
You should move this code into your setStuff function so it's called only once by the effect.
I'd also suggest re-thinking your structure here, so you (and others) can follow what you're doing better. Stack traces will help you with any further errors you get so you can follow the source of more loops you might encounter.