I am playing around with React Hooks. When the button is clicked, I want to increment a counter. After the counter was incremented, the application should not allow further increments until clicked is reset to false.
I came up with this:
function App() {
const [counter, setCounter] = useState(0);
const [clicked, setClicked] = useState(false);
useEffect(() => {
if (clicked) {
setCounter(counter + 1);
setTimeout(() => {
setClicked(false);
}, 2000);
}
}, [clicked]);
return (
<div className="App">
<p>Clicked: {String(clicked)}</p>
<p>Counter: {counter}</p>
<button type="button" onClick={() => setClicked(true)}>
Click me
</button>
</div>
);
}
It actually works. However React is complaining with following warning:
React Hook useEffect has a missing dependency: 'counter'. Either
include it or remove the dependency array. You can also do a
functional update 'setCounter(c => ...)' if you only need 'counter' in
the 'setCounter' call. (react-hooks/exhaustive-deps)
When I add the counter to the dependencies, useEffect will get into an infinite loop, because clicked is true and setCounter was called from within useEffect.
I want the counter only to be incremented, when clicked changed from false to true. That works if the dependency list only contains clicked, but React complains about that.
Try out for yourself: https://codesandbox.io/s/dreamy-shadow-7xesm
Try replacing setCounter(counter + 1) with this:
setCounter(counter => counter + 1)
Like the warning says. Should solve it.
Your problem with infinite loop will be gone if you remove the timeout. (btw what is it for? Are you trying to implement a debounce or throttle?)
import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
function App() {
const [counter, setCounter] = useState(0);
const [clicked, setClicked] = useState(false);
useEffect(() => {
if (clicked) {
setClicked(false);
setCounter(counter + 1);
}
}, [clicked, counter]);
return (
<div className="App">
<p>Clicked: {String(clicked)}</p>
<p>Counter: {counter}</p>
<button type="button" onClick={() => setClicked(true)}>
Click me
</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Related
I'm using useCallback to memoize a function, I wrote 2 functions, formatCounter and formatCounter2 and added useCallback to the functions with dependencies of counter and counter2.
When onClick function is called, the counter variable has changed but formatCounter2 called, but why? As I understand it needs to be called only when the dependency changes (counter2) and it does not change.
function App() {
const [counter, setCounter] = React.useState(0);
const [counter2, setCounter2] = React.useState(0);
const onClick = React.useCallback(()=>{
setCounter(prevState => ++prevState)
},[]);
const onClickSecond = React.useCallback(()=>{
setCounter2(prevState => ++prevState)
},[]);
const formatCounter = React.useCallback((counterVal)=> {
console.log('formatCounter Called')
return `The counter value is ${counterVal}`;
},[counter])
const formatCounter2 = React.useCallback((counterVal2)=> {
console.log('formatCounterTwo Called')
return `The counter value2 is ${counterVal2}`;
},[counter2])
const objMemo = React.useMemo(()=> {
console.log('obj memo is')
return {
a:counter>2?'counter is bigger than 2': 'counter is less than 2'
}
},[counter])
return (
<div className="App">
<div>{formatCounter(counter)}</div>
<div>{formatCounter2(counter2)}</div>
<button onClick={onClick}>
Increment
</button>
<button onClick={onClickSecond}>
Increment2
</button>
<p>{objMemo.a}</p>
</div>
);
}
link to code
useCallback memoizes the function reference. But you're calling the format functions (both) in the render method. So what you're actually seeing is the inner function being called.
Maybe out of curiosity you can check the reference to the function you're memoizing. There you will see that the reference is not changing.
To check this I usually do:
useEffect(() => {
console.log('formatCounter reference changed!', formatCounter);
}, [formatCounter]);
You will see that it logs for when counter changes but not for when counter2 changes.
This is my code:
function AddPost() {
const [file, setFile] = useState({})
const handleChange = (event) => {
setFile(event.target.files[0]);
console.log(file);
}
return (
<div>
<TextField type='file' onChange={handleChange} label='image' variant='outlined' />
</div>
)
}
I am not getting file info. on console, while i am selecting a file . Instead of that I am getting empty object why ?
You need to use a useEffect to besure you are doing your action after a state is updated. Here is an example :
import React, { Component } from "react";
import { render } from "react-dom";
const App = () => {
const [num, setNum] = React.useState(0);
const handleClick = () => {
setNum(1);
console.log('num =', num);
}
// Note the dependency array below
React.useEffect(() => console.log('num (useEffect) = ', num), [num]);
return (
<div>
<button onClick={handleClick}>Click</button>
</div>
);
};
render(<App />, document.getElementById("root"));
and here is repro on Stackblitz.
Here, on click, num will be 0 in the function, but it will be set to 1 in the useEffect.
Just the file is not updated yet (whitin onChange) , so you see the initial state
The state update is performed asynchronously and only visible in the next render call.
In the next render call, file will be updated to the new value and then your (also new) function handleChange will use that value.
This means in your current code the log will always be off by one (empty object, file 1, file 2 etc.) when uploading one file after each other.
The behavior is described in the documentation of the useState hook.
To fix the log, simply write
const newFile = event.target.files[0];
setFile(newFile);
console.log(newFile);
I'm a bit new to ReactJS and still trying to wrap my head around things. I'm currently having problems using an EventEmitter when also using a useState at the same time.
Here is my sandbox:
https://codesandbox.io/s/vibrant-germain-q4933
If you watch the console and click on either the "emit" or "child emit" buttons, it works fine. The signal is being emitted and received. However, as soon as you click the "child count" button a few times, and then go back and click the "child emit", you will see things starting to repeat.
This problem seems to only happen when you use a useState and emitter.on in the same child. It probably has do with passing an emitter as a prop. Not sure if that is a good idea or not.
What am I missing conceptually here?
Thanks
You should not define your emitter instances directly within the function render cycle. Instead you must create and add listner events only once.
To create a listner just once, use useRef and to add a listner just once, use useEffect hook
Sample code
const App = props => {
const myEmitter = useRef(new EventEmitter());
const [counter, setCounter] = useState(0);
useEffect(() => {
myEmitter.current.on("event", msg => {
console.log("Parent received: " + msg);
});
}, []);
const Send = () => {
myEmitter.current.emit("event", "parent sending this message.");
};
return (
<Content>
<button onClick={Send}>emit</button>
<button onClick={() => setCounter(counter + 1)}>count {counter}</button>
<Child myEmitter={myEmitter.current} />
</Content>
);
};
import React, { useState, useEffect } from "react";
const Child = props => {
const [counter, setCounter] = useState(0);
useEffect(() => {
props.myEmitter.on("event", msg => {
console.log("Child received: " + msg);
});
}, [props.myEmitter]);
const Emit = () => {
props.myEmitter.emit("event", "child sending this message");
};
return (
<>
<button onClick={Emit}>child emit</button>
<button onClick={() => setCounter(counter + 1)}>
child count {counter}
</button>
</>
);
};
export default Child;
Demo
I have the following component defined in my app scaffolded using create-react:
import React, { useState } from 'react';
const Play = props => {
const [currentSecond, setCurrentSecond] = useState(1);
let timer;
const setTimer = () => {
timer = setInterval(() => {
if (currentSecond < props.secondsPerRep) {
setCurrentSecond(() => currentSecond + 1);
}
}, 1000);
}
setTimer();
return (
<div>
<div>
<p>{currentSecond}</p>
</div>
</div>
);
}
export default Play;
And currentSecond is updated every second until it hits the props.secondsPerRep however if I try to start the setInterval from a click handler:
import React, { useState } from 'react';
const Play = props => {
const [currentSecond, setCurrentSecond] = useState(1);
let timer;
const setTimer = () => {
timer = setInterval(() => {
if (currentSecond < props.secondsPerRep) {
setCurrentSecond(() => currentSecond + 1);
}
}, 1000);
}
return (
<div>
<div>
<button onClick={setTimer}>Start</button>
<p>{currentSecond}</p>
</div>
</div>
);
}
export default Play;
Then currentSecond within the setInterval callback always returns to the initial value, i.e. 1.
Any help greeeeeeatly appreciated!
Your problem is this line setCurrentSecond(() => currentSecond + 1); because you are only calling setTimer once, your interval will always be closed over the initial state where currentSecond is 1.
Luckily, you can easily remedy this by accessing the actual current state via the args in the function you pass to setCurrentSecond like setCurrentSecond(actualCurrentSecond => actualCurrentSecond + 1)
Also, you want to be very careful arbitrarily defining intervals in the body of functional components like that because they won't be cleared properly, like if you were to click the button again, it would start another interval and not clear up the previous one.
I'd recommend checking out this blog post because it would answer any questions you have about intervals + hooks: https://overreacted.io/making-setinterval-declarative-with-react-hooks/
https://overreacted.io/making-setinterval-declarative-with-react-hooks/ is a great post to look at and learn more about what's going on. The React useState hook doesn't play nice with setInterval because it only gets the value of the hook in the first render, then keeps reusing that value rather than the updated value from future renders.
In that post, Dan Abramov gives an example custom hook to make intervals work in React that you could use. That would make your code look more like this. Note that we have to change how we trigger the timer to start with another state variable.
const Play = props => {
const [currentSecond, setCurrentSecond] = React.useState(1);
const [isRunning, setIsRunning] = React.useState(false);
useInterval(() => {
if (currentSecond < props.secondsPerRep) {
setCurrentSecond(currentSecond + 1);
}
}, isRunning ? 1000 : null);
return (
<div>
<div>
<button onClick={() => setIsRunning(true)}>Start</button>
<p>{currentSecond}</p>
</div>
</div>
);
}
I went ahead and put an example codepen together for your use case if you want to play around with it and see how it works.
https://codepen.io/BastionTheDev/pen/XWbvboX
That is because you're code is closing over the currentSecond value from the render before you clicked on the button. That is javascript does not know about re-renders and hooks. You do want to set this up slightly differently.
import React, { useState, useRef, useEffect } from 'react';
const Play = ({ secondsPerRep }) => {
const secondsPassed = useRef(1)
const [currentSecond, setCurrentSecond] = useState(1);
const [timerStarted, setTimerStarted] = useState(false)
useEffect(() => {
let timer;
if(timerStarted) {
timer = setInterval(() => {
if (secondsPassed.current < secondsPerRep) {
secondsPassed.current =+ 1
setCurrentSecond(secondsPassed.current)
}
}, 1000);
}
return () => void clearInterval(timer)
}, [timerStarted])
return (
<div>
<div>
<button onClick={() => setTimerStarted(!timerStarted)}>
{timerStarted ? Stop : Start}
</button>
<p>{currentSecond}</p>
</div>
</div>
);
}
export default Play;
Why do you need a ref and the state? If you would only have the state the cleanup method of the effect would run every time you update your state. Therefore, you don't want your state to influence your effect. You can achieve this by using the ref to count the seconds. Changes to the ref won't run the effect or clean it up.
However, you also need the state because you want your component to re-render once your condition is met. But since the updater methods for the state (i.e. setCurrentSecond) are constant they also don't influence the effect.
Last but not least I've decoupled setting up the interval from your counting logic. I've done this with an extra state that switches between true and false. So when you click your button the state switches to true, the effect is run and everything is set up. If you're components unmounts, or you stop the timer, or the secondsPerRep prop changes the old interval is cleared and a new one is set up.
Hope that helps!
Try that. The problem was that you're not using the state that is received by the setCurrentSecond function and the function setInterval don't see the state changing.
const Play = props => {
const [currentSecond, setCurrentSecond] = useState(1);
const [timer, setTimer] = useState();
const onClick = () => {
setTimer(setInterval(() => {
setCurrentSecond((state) => {
if (state < props.secondsPerRep) {
return state + 1;
}
return state;
});
}, 1000));
}
return (
<div>
<div>
<button onClick={onClick} disabled={timer}>Start</button>
<p>{currentSecond}</p>
</div>
</div>
);
}
Consider the following component:
function SomeComponent(props){
const [isMouseOver, setIsMouseOver] = useState(false);
return (
<div onMouseOver={_ => setIsMouseOver(true)}
onMouseOut={_ => setIsMouseOver(false)}>
<img src={isMouseOver ? EditIconHover : EditIcon} alt="icon"/>
</div>
);
}
New instance of arrow function is created on every render. It creates a closure over setIsMouseOver function, though this function never changes.
Sure, it does not drastically affect performance in this case, but I'd like to know how to avoid these unnecessary memory allocations.
Do I have to attach all dependencies required for event handler to DOM element
<div data-deps={setIsMouseOver} onMouseOver={onMouseOverHandler} onMouseOut={onMouseOutHandler}></div>
and then access deps property inside onMouseOverHandler and onMouseOutHandler functions?
You might want to use memoization, although in this particular example its an overhead.
Refer to useCallback
import React, { useState, useCallback, useRef, useEffect } from 'react';
import ReactDOM from 'react-dom';
const App = () => {
const [isEnabled, setIsEnabled] = useState(false);
const toggle = useCallback(() => setIsEnabled(v => !v), []);
const toggleRef = useRef();
const setterRef = useRef();
useEffect(() => {
toggleRef.current = toggle;
setterRef.current = setIsEnabled;
}, [toggle]);
useEffect(() => {
console.log(toggle === toggleRef.current);
console.log(setIsEnabled === setterRef.current);
});
return <button onClick={toggle}>{isEnabled ? 'Enabled' : 'Disabled'}</button>;
};
ReactDOM.render(<App />, document.getElementById('root'));