import React from 'react';
const Comp2 = () => {
const [count, setCount] = React.useState(0);
const handleIncrease = () => {
setCount((x) => x + 1);
};
const checkCurrentCount = () => {
console.log('checking...');
setTimeout(() => {
alert(`Ok the current count is: ${count}`);
}, 2000);
};
return (
<div>
<p>{count}</p>
<button onClick={handleIncrease}>+</button>
<button onClick={checkCurrentCount}>check count</button>
</div>
);
};
Problem
If the count number already change, but the alert shows the past number. How to encounter this problem? the setTimeout just simulation of the problem..
You can fix this issue with help of useRef hook.
useRef returns a mutable ref object whose .current property is initialized to the passed argument (initialValue). The returned object will persist for the full lifetime of the component.
View the solution on code sandbox
https://codesandbox.io/s/priceless-newton-l66q6?file=/src/App.js
You need clearTimeout and setTimeout again
CodeSandBox
Related
I want to change State with child elements in React. However, when I click once, it is not immediately updated. Click twice, it shows the correct answer.
How to update async?
export default function Example() {
const onClick = async () => {
console.log('a', test)
// should be 'b', but console log 'a'
}
const [test, setTest] = useState('a')
return (
<ClickExample setTest={setTest} onClick={onClick} />
)
}
export default function ClickExample() {
const next = useCallback(
(alphabet: string) => {
setTest(alphabet)
onClick()
},
[onClick, setTest],
)
return <SelectButton onClick={() => next('b')} />
}
You can receive the value to be updated as an argument from the onClick callback. It'll be something like this:
export default function Example() {
const [test, setTest] = useState('a')
const handleClick = (newValue) => {
setTest(newValue);
}
return (
<ClickExample onClick={handleClick} />
)
}
export default function ClickExample({ onClick }) {
return <SelectButton onClick={() => onClick('b')} />
}
NOTE: You should avoid using useCallback() when it is not necessary. Read more over the web but this article from Kent C. Dodds is a good start. As a rule of thumb: Never use useCallback()/useMemo() unless you REALLY want to improve performance after needing that improvement.
In the first render, the value of test is equal to'a'. So when the console.log is executed, it has already captured 'a' as the value of test state. (See closures and stale closures).
One way to fix this would be to create a handleClick function in the parent component which receives the new value of test as its input and set the state and log the new value(which will be updated in the next render) using its argument.
// ClickExample
const handleClick = (alphabet) => {
setTest(alphabet);
console.log('a', alphabet);
};
codesandbox
In a custom alert system I have created in react I have 2 main functions, a custom hook:
function useAlerts(){
const [alerts, setAlerts] = useState([]);
const [count, setCount] = useState(0);
let add = function(content){
let remove = function(){
console.log(`alerts was set to ${alerts} before remove`);
setAlerts(alerts.slice(1));
};
let newAlert =
<div className = 'alert' onAnimationEnd = {remove} key = {count}>
<Warning/>
<span>
{content}
</span>
</div>
setAlerts([...alerts, newAlert]);
setCount(count + 1);
}
return [alerts,add];
}
and another element to display the data within the custom hook.
function Alerts(){
let [alerts,add] = useAlerts();
useEffect(() => {
let handler = function(){
add('test');
};
window.addEventListener('keyup',handler);
return function(){
window.removeEventListener('keyup',handler);
}
});
return (
<div className = 'alerts'>
{alerts}
</div>
)
}
the current issue I have is with the remove callback function, the console will look something like this.
let remove = function(){
console.log(`alerts was set to ${alerts} before remove`);
/// expected output: alerts was set to [<div>,<div>,<div>,<div>] before remove
/// given output: alerts was set to [] before remove
setAlerts(alerts.slice(1));
};
I understand this is because the remove function is taking the initial value of the alert state, but how do I make sure it keeps an up to date value? React doesn't seem to allow useEffect in a callback so I seem to be a bit stuck. Is passing the value in an object the way to go or something?
The issue I believe I see here is that of stale enclosures over the local React state. Use functional state updates to correctly update from the previous state instead of whatever if closed over in callback scope. In fact, use functional state updates any time the next state depends on the previous state's value. Incrementing counts and mutating arrays are two prime examples for needing functional state updates.
function useAlerts() {
const [alerts, setAlerts] = useState([]);
const [count, setCount] = useState(0);
const add = (content) => {
const remove = () => {
console.log(`alerts was set to ${alerts} before remove`);
setAlerts(alerts => alerts.slice(1));
};
const newAlert = (
<div className='alert' onAnimationEnd={remove} key={count}>
<Warning/>
<span>
{content}
</span>
</div>
);
setAlerts(alerts => [...alerts, newAlert]);
setCount(count => count + 1);
}
return [alerts, add];
}
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 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>
);
}