Unexpected useEffect behaviour - reactjs

I am making hangman game. When letter is clicked it should not be clickable any more.
Here' a simplified version on codesandbox:
https://codesandbox.io/s/intelligent-cori-6b80q
In the sandbox, when letter is clicked it changes to "CLICKED!". Everything seems well here.
But in my project I get strange behavior.
AvailableLetter.js
const AvailableLetter = (props) => {
const [clicked,setClicked]=useState(false);
const setStuff = () => {
setClicked(false); // If I delete this I get Error: Maximum update depth exceeded.
props.setSolved();
};
useEffect( setStuff,[clicked] );
if (clicked) // IF CLICKED REMAINS TRUE, EVERY LETTER GETS PLAYED.
{
if (props.play())
{
props.correct();
}
else
{
props.incorrect();
}
}
const disableLetter = () => {
setClicked(true);
};
let letter=span onClick={disableLetter}>{props.alphabet}</span>;
if(clicked) // CODE NEVER GETS HERE!!!!!!!!!!!!!!
{
letter = <span>{props.alphabet}</span>;
}
return (
<Ax> // Ax is a high order class, just a wrapper
{letter}
</Ax>
);
}
Letter remain clickable even after being clicked. This is not what I want.
Each letter is rendered by Letters.js which feeds in a-z and generates custom AvailableLetter.
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} />
);
}
);
So issues to be solved here are:
- Letters remain clickable even after a click
- If setClicked(false) is removed it causes an infinite loop
const setStuff = () => {
setClicked(false); // if removed causes infinite loop
props.setSolved();
};
All of this is strange because in the codesandbox I don't need to set clicked to false inside setEffect().
You can see all of the code here: https://github.com/gunitinug/hangmanerrors/tree/master/src
Please have a look at project code, it is not long.

You need to understand how useEffect works. Whenever the state changes, useEffect gets called and the handler (in your case setStuff) passed as a callback to useEffect gets executed.
So, setStuff keeps updating the state and useEffect keeps getting called continuously. Hence, you see the error Error: Maximum update depth exceeded.
Change your code as shown below:
const AvailableLetter = (props) => {
const [clicked, setClicked] = useState(false);
const setStuff = () => {
if(clicked) {
if (props.play()) {
props.correct();
}
else {
props.incorrect();
}
setClicked(false);
props.setSolved();
}
};
useEffect( setStuff, [clicked] );
return (
<Ax>
<button onClick={() => setClicked(true)} disabled={clicked}>{props.alphabet}</button>
</Ax>
);
}
As mentioned in the comment posted on your question, this might not be the best approach but my code should solve your problem.

It was a structural problem, I had to delegate handler functions and state to the parent component Letters.js
I got to make it work silky smooth :p
import React, {useState} from 'react';
import AvailableLetter from './AvailableLetter/AvailableLetter';
import DisabledLetter from './DisabledLetter/DisabledLetter';
import classes from './Letters.module.css';
const Letters = (props) => {
const [lettersMap, setLettersMap]=useState(
{
"a":false,"b":false,"c":false,"d":false,"e":false,"f":false,"g":false,"h":false,"i":false,"j":false,"k":false,"l":false,"m":false,"n":false,"o":false,"p":false,"q":false,"r":false,"s":false,"t":false,"u":false,"v":false,"w":false,"x":false,"y":false,"z":false
}
);
const updateClickedHandler = (letter) => {
setLettersMap(
{
...lettersMap,[letter]:true
}
);
};
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 renderedLetters = Object.keys(lettersMap).map(
(letter,i)=>{
if (!lettersMap[letter]) //letter is not yet clicked
{
return (
<AvailableLetter updateClicked={updateClickedHandler} setSolved={props.setSolved} play={()=>playHandler(letter)} correct={()=>props.correct(letter)} incorrect={()=>props.incorrect(letter)} solution={props.solution} key={i} alphabet={letter} />
)
}
else //letter is clicked
{
return (
<DisabledLetter alphabet={letter} />
)
}
}
);
return (
<div className={classes.Letters}>
<p>Solution: {props.solution}</p>
<div className={classes.AvailableLetters}>
{renderedLetters}
</div>
</div>
);
}
export default Letters;
and
import React from 'react';
import classes from './AvailableLetter.module.css';
import Ax from '../../hoc/Ax';
const AvailableLetter = (props) => {
const setStuff = () => {
if (props.play()) {
props.correct();
}
else {
props.incorrect();
}
props.updateClicked(props.alphabet);
props.setSolved();
};
return (
<Ax>
<span className={classes.AvailableLetter} onClick={setStuff}>{props.alphabet}</span>
</Ax>
);
}
export default AvailableLetter;
and finally, I added another component called DisabledLetter.js
import React from 'react';
import classes from './DisabledLetter.module.css';
const DisabledLetter = (props) => {
return (
<span className={classes.DisabledLetter} >{props.alphabet}</span>
);
};
export default DisabledLetter;
You can view entire source here
https://github.com/gunitinug/hangmanclicked/tree/master/src
;)
Discovered an issue though, when game is solved I need to click one more letter for 'YOU WIN' message to display.
When I die 'GAME OVER' is displayed immediately.

Related

How can I detect if two elements are colliding in React?

So, I created a custom hook to detect if my Navbar collides with a specific div in my Portfolio, but I'm a bit insecure if this hook is performance friendly and if it has good practices. Is it okay to use this kind of Event Listener with a useEffect ?
import { useEffect, useState } from "react";
export const useCheckNavCollision = (
navRef: React.RefObject<HTMLElement>,
collisionRef: React.RefObject<HTMLElement>,
scrollListener: React.RefObject<HTMLElement>
) => {
const [detected, setDetected] = useState<boolean>(false);
useEffect(() => {
const scroll = () => {
const greenContainer = collisionRef.current!.getBoundingClientRect().bottom,
navHeight = navRef.current!.getBoundingClientRect().height;
if (navHeight > greenContainer) setDetected(true);
else setDetected(false);
};
if (scrollListener) {
scrollListener.current!.addEventListener("scroll", scroll);
return () => scrollListener.current!.removeEventListener("scroll", scroll);
}
}, [navRef, collisionRef, scrollListener]);
return detected;
};

What's the best way to call a hook function on click in React

I was creating some header for my app, so I tried to place the hooks in some other file for better organization.
But I'm a bit confused, how do I call this function useHeaderNavStatus() when I click the button in the header tag, without creating another useEffect and useState in Header component? Is it possible or I'm too blind?
I appreciate any help! :D
Here is the file structure:
Header Component
const Header = () => {
const headerNav = useHeaderNavStatus();
return (
<header>
<button
ref={headerNav.toggleRef}
onClick={What do I do here?}>
Menu
</button>
</header>
);
}
Hooks file
import { useEffect, useRef, useState } from 'react';
const useHeaderNavStatus = () => {
// Creating ref for toggle button
const toggleRef = useRef<HTMLButtonElement>(null);
// Creating state to know if nav is showing or not
const [isActive, setIsActive] = useState(false);
// This function opens and closes nav
const updateBodyScroll = () => {
setIsActive(!isActive);
const body = document.body.classList;
if (isActive) {
body.remove('no-scroll');
} else {
body.add('no-scroll');
}
};
// This function closes the nav on outside click
const closeNavOnOutsideClick = (event: MouseEvent) => {
if (!toggleRef.current?.contains(event.target as Node))
updateBodyScroll();
};
// Adds and removes event listener to know the target click
useEffect(() => {
if (isActive) {
window.addEventListener('mousedown', closeNavOnOutsideClick);
return () =>
window.removeEventListener('mousedown', closeNavOnOutsideClick);
}
});
return { toggleRef, isActive };
};
Your hook should also return a function that opens the nav:
const openNav = () => setIsActive(true);
return { toggleRef, isActive, openNav };
And then use it:
const Header = () => {
const headerNav = useHeaderNavStatus();
return (
<header>
<button
ref={headerNav.toggleRef}
onClick={headerNav.openNav}>
Menu
</button>
</header>
);
}

What is the good practice way to prevent a useEffect from triggering on initial render? [duplicate]

According to the docs:
componentDidUpdate() is invoked immediately after updating occurs. This method is not called for the initial render.
We can use the new useEffect() hook to simulate componentDidUpdate(), but it seems like useEffect() is being ran after every render, even the first time. How do I get it to not run on initial render?
As you can see in the example below, componentDidUpdateFunction is printed during the initial render but componentDidUpdateClass was not printed during the initial render.
function ComponentDidUpdateFunction() {
const [count, setCount] = React.useState(0);
React.useEffect(() => {
console.log("componentDidUpdateFunction");
});
return (
<div>
<p>componentDidUpdateFunction: {count} times</p>
<button
onClick={() => {
setCount(count + 1);
}}
>
Click Me
</button>
</div>
);
}
class ComponentDidUpdateClass extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0,
};
}
componentDidUpdate() {
console.log("componentDidUpdateClass");
}
render() {
return (
<div>
<p>componentDidUpdateClass: {this.state.count} times</p>
<button
onClick={() => {
this.setState({ count: this.state.count + 1 });
}}
>
Click Me
</button>
</div>
);
}
}
ReactDOM.render(
<div>
<ComponentDidUpdateFunction />
<ComponentDidUpdateClass />
</div>,
document.querySelector("#app")
);
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>
We can use the useRef hook to store any mutable value we like, so we could use that to keep track of if it's the first time the useEffect function is being run.
If we want the effect to run in the same phase that componentDidUpdate does, we can use useLayoutEffect instead.
Example
const { useState, useRef, useLayoutEffect } = React;
function ComponentDidUpdateFunction() {
const [count, setCount] = useState(0);
const firstUpdate = useRef(true);
useLayoutEffect(() => {
if (firstUpdate.current) {
firstUpdate.current = false;
return;
}
console.log("componentDidUpdateFunction");
});
return (
<div>
<p>componentDidUpdateFunction: {count} times</p>
<button
onClick={() => {
setCount(count + 1);
}}
>
Click Me
</button>
</div>
);
}
ReactDOM.render(
<ComponentDidUpdateFunction />,
document.getElementById("app")
);
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>
You can turn it into custom hooks, like so:
import React, { useEffect, useRef } from 'react';
const useDidMountEffect = (func, deps) => {
const didMount = useRef(false);
useEffect(() => {
if (didMount.current) func();
else didMount.current = true;
}, deps);
}
export default useDidMountEffect;
Usage example:
import React, { useState, useEffect } from 'react';
import useDidMountEffect from '../path/to/useDidMountEffect';
const MyComponent = (props) => {
const [state, setState] = useState({
key: false
});
useEffect(() => {
// you know what is this, don't you?
}, []);
useDidMountEffect(() => {
// react please run me if 'key' changes, but not on initial render
}, [state.key]);
return (
<div>
...
</div>
);
}
// ...
I made a simple useFirstRender hook to handle cases like focussing a form input:
import { useRef, useEffect } from 'react';
export function useFirstRender() {
const firstRender = useRef(true);
useEffect(() => {
firstRender.current = false;
}, []);
return firstRender.current;
}
It starts out as true, then switches to false in the useEffect, which only runs once, and never again.
In your component, use it:
const firstRender = useFirstRender();
const phoneNumberRef = useRef(null);
useEffect(() => {
if (firstRender || errors.phoneNumber) {
phoneNumberRef.current.focus();
}
}, [firstRender, errors.phoneNumber]);
For your case, you would just use if (!firstRender) { ....
Same approach as Tholle's answer, but using useState instead of useRef.
const [skipCount, setSkipCount] = useState(true);
...
useEffect(() => {
if (skipCount) setSkipCount(false);
if (!skipCount) runYourFunction();
}, [dependencies])
EDIT
While this also works, it involves updating state which will cause your component to re-render. If all your component's useEffect calls (and also all of its children's) have a dependency array, this doesn't matter. But keep in mind that any useEffect without a dependency array (useEffect(() => {...}) will be run again.
Using and updating useRef will not cause any re-renders.
#ravi, yours doesn't call the passed-in unmount function. Here's a version that's a little more complete:
/**
* Identical to React.useEffect, except that it never runs on mount. This is
* the equivalent of the componentDidUpdate lifecycle function.
*
* #param {function:function} effect - A useEffect effect.
* #param {array} [dependencies] - useEffect dependency list.
*/
export const useEffectExceptOnMount = (effect, dependencies) => {
const mounted = React.useRef(false);
React.useEffect(() => {
if (mounted.current) {
const unmount = effect();
return () => unmount && unmount();
} else {
mounted.current = true;
}
}, dependencies);
// Reset on unmount for the next mount.
React.useEffect(() => {
return () => mounted.current = false;
}, []);
};
a simple way is to create a let, out of your component and set in to true.
then say if its true set it to false then return (stop) the useEffect function
like that:
import { useEffect} from 'react';
//your let must be out of component to avoid re-evaluation
let isFirst = true
function App() {
useEffect(() => {
if(isFirst){
isFirst = false
return
}
//your code that don't want to execute at first time
},[])
return (
<div>
<p>its simple huh...</p>
</div>
);
}
its Similar to #Carmine Tambasciabs solution but without using state :)
‍‍‍‍‍‍
‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍
function useEffectAfterMount(effect, deps) {
const isMounted = useRef(false);
useEffect(() => {
if (isMounted.current) return effect();
else isMounted.current = true;
}, deps);
// reset on unmount; in React 18, components can mount again
useEffect(() => {
isMounted.current = false;
});
}
We need to return what comes back from effect(), because it might be a cleanup function. But we don't need to determine if it is or not. Just pass it on and let useEffect figure it out.
In an earlier version of this post I said resetting the ref (isMounted.current = false) wasn't necessary. But in React 18 it is, because components can remount with their previous state (thanks #Whatabrain).
I thought creating a custom hook would be overkill and I didn't want to muddle my component's readability by using the useLayoutEffect hook for something unrelated to layouts, so, in my case, I simply checked to see if the value of my stateful variable selectedItem that triggers the useEffect callback is its original value in order to determine if it's the initial render:
export default function MyComponent(props) {
const [selectedItem, setSelectedItem] = useState(null);
useEffect(() => {
if(!selectedItem) return; // If selected item is its initial value (null), don't continue
//... This will not happen on initial render
}, [selectedItem]);
// ...
}
This is the best implementation I've created so far using typescript. Basically, the idea is the same, using the Ref but I'm also considering the callback returned by useEffect to perform cleanup on component unmount.
import {
useRef,
EffectCallback,
DependencyList,
useEffect
} from 'react';
/**
* #param effect
* #param dependencies
*
*/
export default function useNoInitialEffect(
effect: EffectCallback,
dependencies?: DependencyList
) {
//Preserving the true by default as initial render cycle
const initialRender = useRef(true);
useEffect(() => {
let effectReturns: void | (() => void) = () => {};
// Updating the ref to false on the first render, causing
// subsequent render to execute the effect
if (initialRender.current) {
initialRender.current = false;
} else {
effectReturns = effect();
}
// Preserving and allowing the Destructor returned by the effect
// to execute on component unmount and perform cleanup if
// required.
if (effectReturns && typeof effectReturns === 'function') {
return effectReturns;
}
return undefined;
}, dependencies);
}
You can simply use it, as usual as you use the useEffect hook but this time, it won't run on the initial render. Here is how you can use this hook.
useNoInitialEffect(() => {
// perform something, returning callback is supported
}, [a, b]);
If you use ESLint and want to use the react-hooks/exhaustive-deps rule for this custom hook:
{
"rules": {
// ...
"react-hooks/exhaustive-deps": ["warn", {
"additionalHooks": "useNoInitialEffect"
}]
}
}
#MehdiDehghani, your solution work perfectly fine, one addition you have to do is on unmount, reset the didMount.current value to false. When to try to use this custom hook somewhere else, you don't get cache value.
import React, { useEffect, useRef } from 'react';
const useDidMountEffect = (func, deps) => {
const didMount = useRef(false);
useEffect(() => {
let unmount;
if (didMount.current) unmount = func();
else didMount.current = true;
return () => {
didMount.current = false;
unmount && unmount();
}
}, deps);
}
export default useDidMountEffect;
Simplified implementation
import { useRef, useEffect } from 'react';
function MyComp(props) {
const firstRender = useRef(true);
useEffect(() => {
if (firstRender.current) {
firstRender.current = false;
} else {
myProp = 'some val';
};
}, [props.myProp])
return (
<div>
...
</div>
)
}
You can use custom hook to run use effect after mount.
const useEffectAfterMount = (cb, dependencies) => {
const mounted = useRef(true);
useEffect(() => {
if (!mounted.current) {
return cb();
}
mounted.current = false;
}, dependencies); // eslint-disable-line react-hooks/exhaustive-deps
};
Here is the typescript version:
const useEffectAfterMount = (cb: EffectCallback, dependencies: DependencyList | undefined) => {
const mounted = useRef(true);
useEffect(() => {
if (!mounted.current) {
return cb();
}
mounted.current = false;
}, dependencies); // eslint-disable-line react-hooks/exhaustive-deps
};
For people who are having trouble with React 18 strict mode calling the useeffect on the initial render twice, try this:
// The init variable is necessary if your state is an object/array, because the == operator compares the references, not the actual values.
const init = [];
const [state, setState] = useState(init);
const dummyState = useRef(init);
useEffect(() => {
// Compare the old state with the new state
if (dummyState.current == state) {
// This means that the component is mounting
} else {
// This means that the component updated.
dummyState.current = state;
}
}, [state]);
Works in development mode...
function App() {
const init = [];
const [state, setState] = React.useState(init);
const dummyState = React.useRef(init);
React.useEffect(() => {
if (dummyState.current == state) {
console.log('mount');
} else {
console.log('update');
dummyState.current = state;
}
}, [state]);
return (
<button onClick={() => setState([...state, Math.random()])}>Update state </button>
);
}
ReactDOM.createRoot(document.getElementById("app")).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<div id="app"></div>
And in production.
function App() {
const init = [];
const [state, setState] = React.useState(init);
const dummyState = React.useRef(init);
React.useEffect(() => {
if (dummyState.current == state) {
console.log('mount');
} else {
console.log('update');
dummyState.current = state;
}
}, [state]);
return (
<button onClick={() => setState([...state, Math.random()])}>Update state </button>
);
}
ReactDOM.createRoot(document.getElementById("app")).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
<script crossorigin src="https://unpkg.com/react#18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.production.min.js"></script>
<div id="app"></div>
If you want to skip the first render, you can create a state "firstRenderDone" and set it to true in the useEffect with empty dependecy list (that works like a didMount). Then, in your other useEffect, you can check if the first render was already done before doing something.
const [firstRenderDone, setFirstRenderDone] = useState(false);
//useEffect with empty dependecy list (that works like a componentDidMount)
useEffect(() => {
setFirstRenderDone(true);
}, []);
// your other useEffect (that works as componetDidUpdate)
useEffect(() => {
if(firstRenderDone){
console.log("componentDidUpdateFunction");
}
}, [firstRenderDone]);
All previous are good, but this can be achieved in a simplier way considering that the action in useEffect can be "skipped" placing an if condition(or any other ) that is basically not run first time, and still with the dependency.
For example I had the case of :
Load data from an API but my title has to be "Loading" till the date were not there, so I have an array, tours that is empty at beginning and show the text "Showing"
Have a component rendered with different information from those API.
The user can delete one by one those info, even all making the tour array empty again as the beginning but this time the API fetch is been already done
Once the tour list is empty by deleting then show another title.
so my "solution" was to create another useState to create a boolean value that change only after the data fetch making another condition in useEffect true in order to run another function that also depend on the tour length.
useEffect(() => {
if (isTitle) {
changeTitle(newTitle)
}else{
isSetTitle(true)
}
}, [tours])
here my App.js
import React, { useState, useEffect } from 'react'
import Loading from './Loading'
import Tours from './Tours'
const url = 'API url'
let newTours
function App() {
const [loading, setLoading ] = useState(true)
const [tours, setTours] = useState([])
const [isTitle, isSetTitle] = useState(false)
const [title, setTitle] = useState("Our Tours")
const newTitle = "Tours are empty"
const removeTours = (id) => {
newTours = tours.filter(tour => ( tour.id !== id))
return setTours(newTours)
}
const changeTitle = (title) =>{
if(tours.length === 0 && loading === false){
setTitle(title)
}
}
const fetchTours = async () => {
setLoading(true)
try {
const response = await fetch(url)
const tours = await response.json()
setLoading(false)
setTours(tours)
}catch(error) {
setLoading(false)
console.log(error)
}
}
useEffect(()=>{
fetchTours()
},[])
useEffect(() => {
if (isTitle) {
changeTitle(newTitle)
}else{
isSetTitle(true)
}
}, [tours])
if(loading){
return (
<main>
<Loading />
</main>
)
}else{
return (
<main>
<Tours tours={tours} title={title} changeTitle={changeTitle}
removeTours={removeTours} />
</main>
)
}
}
export default App
const [dojob, setDojob] = useState(false);
yourfunction(){
setDojob(true);
}
useEffect(()=>{
if(dojob){
yourfunction();
setDojob(false);
}
},[dojob]);

react 25+5 clock for freecodecamp test fail

I’m making the 25 + 5 clock for the freecodecamp certification but 2 test failled.
The test 10 and 11 for the #Timer are wrong.
" 25 + 5 clock has paused but time continued elapsing: expected ‘58’ to equal ‘59’ "
On my side, it’s working and you can test it yourself link to the deployed project here 1.
You can click the play and pause button as fast as you can, it work.
But the test not.
It’s for 2 days that I’m checking on stackoverflow, freecodecamp forum, google about this issue.
I did a lot of change but not possible to find the issue.
body component
import React from 'react';
import Compteur from './Compteur';
import Config from './Config';
import { useState, useEffect } from 'react';
const Body = () => {
const [sessionCounter, setSessionCounter] = useState(1500);
const [breakCounter, setBreakCounter] = useState(300);
const [counterScreenSession, setCounterScreenSession] = useState(sessionCounter);
const [play, setPlay] = useState(false);
const [session, setSession] = useState(true);
const handleSessionCounter = (e) => {
let number = e.currentTarget.dataset.session
if(number === "up"){
if(sessionCounter<3600){
return setSessionCounter(sessionCounter+60);
}else{
return sessionCounter;
}
}
else{
if(sessionCounter >= 120){
return setSessionCounter(sessionCounter-60);
}else{
return sessionCounter;
}
}
}
const handleBreakCounter = (e) => {
let number = e.currentTarget.dataset.breaker
if(number === "up"){
if(breakCounter<3600){
return setBreakCounter(breakCounter+60);
}else{
return breakCounter;
}
}
else{
if(breakCounter >= 120){
return setBreakCounter(breakCounter-60);
}else{
return breakCounter;
}
}
}
const handleClear = () => {
setPlay(false);
setSession(true);
setBreakCounter(300);
setSessionCounter(1500)
document.getElementById("beep").pause();
document.getElementById("beep").currentTime=0;
return setCounterScreenSession(1500);
}
const handleCounterScreen = () => {
setPlay(play=>!play);
}
useEffect(() => {
if(play && counterScreenSession>0){
const timer = window.setInterval(()=>{
setCounterScreenSession(counterScreenSession => counterScreenSession-1);
}, 1000);
return ()=>{
clearInterval(timer)
}
}
}, [play, counterScreenSession])
useEffect(() => {
if(counterScreenSession===0 && session){
document.getElementById("beep").play();
setCounterScreenSession(breakCounter);
setSession(!session);
}
if(counterScreenSession===0 && !session){
setCounterScreenSession(sessionCounter);
setSession(!session);
}
}, [counterScreenSession, session, breakCounter, sessionCounter])
useEffect(()=>{
return setCounterScreenSession(sessionCounter);
}, [sessionCounter, breakCounter])
const timeCounter = () =>{
let minutes = Math.floor(counterScreenSession/60);
let seconds = counterScreenSession%60;
if(minutes<10){
minutes = "0"+minutes;
}
if(seconds<10){
seconds = "0"+seconds;
}
return `${minutes}:${seconds}`;
}
return (
<div className="body">
<Config handleBreakCounter={handleBreakCounter} handleSessionCounter={handleSessionCounter}
sessionCounter={sessionCounter} breakCounter={breakCounter}/>
<Compteur counterScreenSession={counterScreenSession} play={play} handleCounterScreen=
{handleCounterScreen} handleClear={handleClear} session={session} sessionCounter={sessionCounter}
timeCounter={timeCounter} breakCounter={breakCounter}/>
</div>
);
};
export default Body;
Compteur component
import React from 'react';
import { AiFillPauseCircle, AiFillPlayCircle } from "react-icons/ai";
import {VscDebugRestart} from "react-icons/vsc";
import { CircularProgressbar } from 'react-circular-progressbar';
import 'react-circular-progressbar/dist/styles.css';
const Compteur = ({counterScreenSession, play, handleCounterScreen, handleClear, session,
timeCounter,breakCounter,sessionCounter}) => {
return (
<div className={"compteur"} >
<div className="compteur__name" id="timer-label">{session? "Session" : "Break"}</div>
<CircularProgressbar
className="compteur__animation"
value={counterScreenSession}
minValue={0}
maxValue={session? sessionCounter:breakCounter }
counterClockwise="true"
styles={{
path:{
stroke: "#005479"
},
trail:{
stroke:"#A8223A"
}}
}
/>
<div className="compteur__time"
className={counterScreenSession<600 && counterScreenSession%60<5?"compteur__time
compteur__name--red" : "compteur__time" }id="time-left">
{
/*
counterScreenSession<600 && counterScreenSession%60<10 ?
"0"+Math.floor(counterScreenSession/60)+":0"+counterScreenSession%60:
counterScreenSession>599 && counterScreenSession%60<10 ?
Math.floor(counterScreenSession/60)+":0"+counterScreenSession%60:
counterScreenSession<600 && counterScreenSession%60>10 ?
"0"+Math.floor(counterScreenSession/60)+":"+counterScreenSession%60:
Math.floor(counterScreenSession/60)+":"+counterScreenSession%60
*/
timeCounter()
}
</div>
<audio id="beep" src="./sound/duke-game-over.mp3"></audio>
<div className="compteur__controler">
{
play === false ?<button className="compteur__controler__play" id="start_stop" onClick=
{handleCounterScreen} ><AiFillPlayCircle/></button>:<button className="compteur__controler__break"
onClick={handleCounterScreen}><AiFillPauseCircle/></button>
}
<button className="compteur__controler__clear" id="reset" onClick={handleClear}>
<VscDebugRestart/></button>
</div>
</div>
);
};
export default Compteur;
Link to the repo on github here.
I had the same problem and solved it using a class-based solution.
What I had was 53 - 55. It means there's a 2000ms (in your case is 1000ms) difference between the time the test case was paused and the time the test case replayed. The problem was because I chained the beep sound play, then setting the state (switching session-break), and clearing the interval (so it was serially firing three different functions). It was solved when I moved the beep sound play and switching session-break, all together within the function that invokes clearInterval (so they're all "fired together" in that function).
That chaining might've happened here:
if(counterScreenSession===0 && session){
document.getElementById("beep").play();
setCounterScreenSession(breakCounter);
setSession(!session);
}
if(counterScreenSession===0 && !session){
setCounterScreenSession(sessionCounter);
setSession(!session);
}
So if I were you, I probably would try to incorporate those beep play and setsession within the return function in the else statement here, together with the clearInterval. Not sure if it'll work out though, I'm not even sure it could be played that way, but it might be worth it to toy around with that idea.
if(play && counterScreenSession>0){
const timer = window.setInterval(()=>{
setCounterScreenSession(counterScreenSession => counterScreenSession-1);
}, 1000);
return ()=>{
clearInterval(timer)
}
}

React hangman game: clicked state not managed correctly producing unexpected behavior

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.

Resources