I'm trying to execute a function that is using react state but when state changes the function doesn't updates with the state value.
const {useState, useEffect} = React;
function Example() {
const [count, setCount] = useState(0);
const testFunction = function(){
setInterval(() => {
console.log(count)
}, 3000)
}
useEffect(() => {
let fncs = [testFunction];
fncs.forEach(fnc => fnc.apply(this));
}, [])
// Similar to componentDidMount and componentDidUpdate:
useEffect(() => {
// Update the document title using the browser API
document.getElementById('other-div').innerHTML = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
ReactDOM.render( <Example />, document.getElementById('root') );
Exmaple:
https://jsfiddle.net/bzyxqkwt/4/
just so you understand, im passing functions to another component and he execute those functions on some event like this
fncs.forEach(fnc => fnc.apply(this, someEventParams));
I'm just guessing that the setInterval is just for demo purposes and needs to be stopped/removed, but basically useEffect captures the initial state. So if you want to catch inside the inner function updated state, then you should pass all the values that you need (in this case count parameter). Something like:
const testFunction = function(){
// setInterval(() => {
console.log(count); // it should log the updated value
// }, 3000)
}
useEffect(() => {
let fncs = [testFunction];
fncs.forEach(fnc => fnc.apply(this));
}, [count]); // <-- this makes the trick
Related
I am trying to create a stop watch using react and set interval but do not understand why count variable is always 0 is it being reset to default state. I thought since state takes some time to update hence count was always 0 but even if i increase the set interval timer it shows the same value.
Through i am trying to understand how react hooks work if someone can shed some light on functioning of hooks or redirect me to necessary links please do so
code is working in case i replace setCount(count+1) to setCount(prevCount=>prevCount+1) also you need to declare intervalId outside of app function
import "./styles.css";
import { useState } from "react";
export default function App() {
const [count, setCount] = useState(0);
let intervalId = -1;
const increment = () => {
console.log(count);
setCount(count + 1);
};
const handleStart = () => {
if (intervalId === -1)
intervalId = setInterval(() => {
// console.log("called");
increment();
}, 1000);
};
const handleStop = () => {
clearInterval(intervalId);
setCount(0);
};
const handlePause = () => {
clearInterval(intervalId);
};
const handleResume = () => {
handleStart();
};
return (
<div className="App">
<div className="counter">{count}</div>
<button onClick={handleStart} className="counter">
start
</button>
<button onClick={handleStop} className="counter">
stop
</button>
<button onClick={handlePause} className="counter">
pause
</button>
<button onClick={handleResume} className="counter">
resume
</button>
</div>
);
}
I'm by no means a React expert, but I watched this talk before and found it very enlightening, and I recommend you watch it too.
I also recommend you watch the first part, but the timestamp I linked is more relevant to your question.
Now in the official react docs here, they say:
If the new state is computed using the previous state, you can pass a function to setState. The function will receive the previous value, and return an updated value.
So, as you noticed with prevCount, it works when you do it that.
In this blog post, the writer explains that:
if you increment the count value as follows setCount(count + 1)
The the count will stuck at 0 + 1 = 1 because the variable count value when setInterval() is called is 0.
So we know that we need the previous state. If you follow the blog post, he does set up a working counter.
The way the code is right now will create an interval for each click since the intervalID gets set to -1 with each update. So, similar to the blogpost, we can have the interval ID as a state. Here's your stopwatch using that approach:
CodeSandbox
import "./styles.css";
import { useState } from "react";
export default function App() {
const [count, setCount] = useState(0);
const [intervalID, setIntervalID] = useState(0);
const handleStart = () => {
if (!intervalID) {
let interval = setInterval(() => setCount(prevCount => prevCount + 1), 1000);
setIntervalID(interval)
}
};
const handlePause = () => {
if (intervalID){
clearInterval(intervalID)
setIntervalID(0)
}
};
const handleStop = () => {
handlePause();
setCount(0)
};
const handleResume = () => {
handleStart();
};
return (
<div className="App">
<div className="counter">{count}</div>
<button onClick={handleStart} className="counter">
start
</button>
<button onClick={handleStop} className="counter">
stop
</button>
<button onClick={handlePause} className="counter">
pause
</button>
<button onClick={handleResume} className="counter">
resume
</button>
</div>
);
}
But this way, we have no way to clear the interval after unmounting the component. So we'll need useEffect(). And since we're using useEffect(), (check why here). And now, instead of saving our intervalID as the state, we can instead have an isRunning state and have useEffect create or clear the interval every time isRunning changes. So now all our handlers have to do is setIsRunning and useEffect will handle the rest.
So the code will look like this:
CodeSandbox
import "./styles.css";
import { useEffect, useState } from "react";
export default function App() {
const [count, setCount] = useState(0);
const [isRunning, setIsRunning] = useState(0);
useEffect(()=> {
let intervalId;
if (isRunning) {
intervalId = setInterval(() => setCount(prevCount => prevCount + 1), 1000);
} else {
clearInterval(intervalId)
}
return () => clearInterval(intervalId) // Clear after unmounting
}, [isRunning])
const handleStart = () => {
setIsRunning(true);
};
const handleStop = () => {
setIsRunning(false);
setCount(0)
};
const handlePause = () => {
setIsRunning(false);
};
const handleResume = () => {
handleStart();
};
return (
<div className="App">
<div className="counter">{count}</div>
<button onClick={handleStart} className="counter">
start
</button>
<button onClick={handleStop} className="counter">
stop
</button>
<button onClick={handlePause} className="counter">
pause
</button>
<button onClick={handleResume} className="counter">
resume
</button>
</div>
);
}
I really recommend you watch the talk I linked in the beginning. I'm sure you'll find it very helpful in terms of using hooks and some of the issues you can face and how to fix them.
In the example bellow, Child component calls onFinish callback 5 seconds after clicking on button. The problem is that onFinish callback can change in those 5 seconds, but the it will call the last caught one.
import React, { useState } from "react";
const Child = ({ onFinish }) => {
const [finished, setFinished] = useState(false);
const finish = async () => {
setFinished(true);
setTimeout(() => onFinish(), 5000);
};
return finished ? (
<p>Wait 5 seconds and increment while waiting.</p>
) : (
<button onClick={finish}>Click here to finish</button>
);
};
export default function App() {
const [count, setCount] = useState(0);
return (
<>
<p>Count: {count}</p>
<button onClick={() => setCount((c) => c + 1)}>Increment</button>
<Child onFinish={() => alert(`Finished on count: ${count}`)} />
</>
);
}
The workaroud for this one is to replace finish with the following:
const cb = useRef();
cb.current = onFinish;
const finish = async () => {
setFinished(true);
setTimeout(() => cb.current(), 5000);
};
Is there a better approach to update the callback to the latest one?
Yes, you can check the current state and compare it with prev state like
setFinished((prevState) => newState)
I have an 'Accept' button which I would like to be automatically clicked after 5 seconds. I'm using React with Next.js. The button code is:
<button name="accept" className="alertButtonPrimary" onClick={()=>acceptCall()}>Accept</button>
If I can't do this, I would like to understand why, so I can improve my React and Next.js skills.
I'm guessing you want this activated 5 seconds after render, in that case, put a setTimeout inside of the useEffect hook, like so. this will call whatever is in the hook after the render is complete.
Although this isn't technically activating the button click event.
useEffect(() => {
setTimeout(() => {
acceptCall()
}, timeout);
}, [])
in that case you should use a ref like so,
const App = () => {
const ref = useRef(null);
const myfunc = () => {
console.log("I was activated 5 seconds later");
};
useEffect(() => {
setTimeout(() => {
ref.current.click();
}, 5000); //miliseconds
}, []);
return (
<button ref={ref} onClick={myfunc}>
TEST
</button>
);
};
Hopefully, this is what you are looking for.
https://codesandbox.io/s/use-ref-forked-bl7i0?file=/src/index.js
You could create a ref for the <button> and set a timeout inside of an effect hook to call the button click event after 5 seconds.
You could throw in a state hook to limit the prompt.
import React, { useEffect, useRef, useState } from "react";
const App = () => {
const buttonRef = useRef("accept-button");
const [accepted, setAccepted] = useState(false);
const acceptCall = (e) => {
alert("Accepted");
};
const fireEvent = (el, eventName) => {
const event = new Event(eventName, { bubbles: true });
el.dispatchEvent(event);
};
useEffect(() => {
if (!accepted) {
setTimeout(() => {
if (buttonRef.current instanceof Element) {
setAccepted(true);
fireEvent(buttonRef.current, "click");
}
}, 5000);
}
}, [accepted]);
return (
<div className="App">
<button
name="accept"
className="alertButtonPrimary"
ref={buttonRef}
onClick={acceptCall}
>
Accept
</button>
</div>
);
};
export default App;
There is counter on page. To avoid re-rendering the entire Parent component every second, the counter is placed in a separate Child component.
From time to time there is need to take current time from counter Child (in this example by clicking Button).
I found solution with execution useEffect by passing empty object as dependency.
Even though it works, I don't feel this solution is correct one.
Do you have suggestion how this code could be improved?
Parent component:
const Parent = () => {
const [getChildValue, setGetChildValue] = useState(0);
const [triggerChild, setTriggerChild] = useState(0); // set just to force triggering in Child
const fooJustToTriggerChildAction = () => {
setTriggerChild({}); // set new empty object to force useEffect in child
};
const handleValueFromChild = (timeFromChild) => {
console.log('Current time from child:', timeFromChild);
};
return (
<>
<Child
handleValueFromChild={handleValueFromChild}
triggerChild={triggerChild}
/>
<Button onPress={fooJustToTriggerChildAction} >
Click to take time
</Button>
</>
);
};
Child component
const Child = ({
triggerChild,
handleValueFromChild,
}) => {
const [totalTime, setTotalTime] = useState(0);
const totalTimeRef = useRef(totalTime); // useRef to handle totalTime inside useEffect
const counter = () => {
totalTimeRef.current = totalTimeRef.current + 1;
setTotalTime(totalTimeRef.current);
setTimeout(counter, 1000);
};
useEffect(() => {
counter();
}, []); // Run time counter at first render
useEffect(() => {
const valueForParent = totalTimeRef.current;
handleValueFromChild(valueForParent); // use Parent's function to pass new time
}, [triggerChild]); // Force triggering with empty object
return (
<>
<div>Total time: {totalTime}</div>
</>
);
};
Given your set of requirements, I would do something similar, albeit with one small change.
Instead of passing an empty object (which obviously works, as {} !== {}), I would pass a boolean flag to my child, requesting to pass back the current timer value. As soon as the value is passed, I would then reset the flag to false, pass the value and wait for the next request by the parent.
Parent component:
const Parent = () => {
const [timerNeeded, setTimerNeeded] = useState(false);
const fooJustToTriggerChildAction = () => {
setTimerNeeded(true);
};
const handleValueFromChild = (timeFromChild) => {
console.log('Current time from child:', timeFromChild);
setTimerNeeded(false);
};
return (
<>
<Child
handleValueFromChild={handleValueFromChild}
timerNeeded={timerNeeded}
/>
<Button onPress={fooJustToTriggerChildAction} >
Click to take time
</Button>
</>
);
};
Child component
const Child = ({
timerNeeded,
handleValueFromChild,
}) => {
const [totalTime, setTotalTime] = useState(0);
const totalTimeRef = useRef(totalTime); // useRef to handle totalTime inside useEffect
const counter = () => {
totalTimeRef.current = totalTimeRef.current + 1;
setTotalTime(totalTimeRef.current);
setTimeout(counter, 1000);
};
useEffect(() => {
counter();
}, []); // Run time counter at first render
useEffect(() => {
if (timerNeeded) {
handleValueFromChild(totalTimeRef.current);
}
}, [timerNeeded]);
return (
<>
<div>Total time: {totalTime}</div>
</>
);
};
Some React Hooks API like useEffect, useMemo, useCallback have a second parameter: an array of inputs:
useEffect(didUpdate, inputs);
As the official document said:
#see Conditionally firing an effect
That way an effect is always recreated if one of its inputs changes.
every value referenced inside the effect function should also appear in the inputs array.
So we can see, the inputs array takes two responsibilities.
In most situations, they are working properly. But sometimes they conflict.
For example, I have a little counting program, it does two things:
Click button and the count plus 1.
Send the count to server every 5 seconds.
Codesandbox:
https://codesandbox.io/s/k0m1mq9v
Or see the code here:
import { useState, useEffect, useCallback } from 'react';
function xhr(count) {
console.log(`Sending "${count}" to my server.`);
// TODO send count to my server by XMLHttpRequest
}
function add1(n) {
return n + 1;
}
function Example() {
// Declare a new state variable, which we'll call "count"
const [count, setCount] = useState(0);
// Handle click to increase count by 1
const handleClick = useCallback(
() => setCount(add1),
[],
);
// Send count to server every 5 seconds
useEffect(() => {
const intervalId = setInterval(() => xhr(count), 5000);
return () => clearInterval(intervalId);
}, []);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={handleClick}>
Click me
</button>
</div>
);
}
export default Example;
When I run this code, I'll always send count = 0 to my server, because I haven't passed the count to useEffect.
But if I pass count to useEffect, my setInterval will be cleared and the whole callback will be recreated each time when I click the button.
I think maybe there's another paradigm to achieve my goal which I haven't think of. If not, that is a conflict of the inputs array.
Reply from React:
React discussion Github
A better solution may be implemented, but not now.
But life will continue, so a workaround pattern like this may help:
const [count, setCount] = useState(0);
const countRef = useRef(count);
useEffect(() => {
countRef.current = count
}, [count]);
useRef() can solve your problem. I think this is an elegant solution: code in sandbox
function App() {
const [count, setCount] = useState(0);
// ***** Initialize countRef.current with count
const countRef = useRef(count);
const handleClick = useCallback(() => setCount(add1), []);
// ***** Set countRef.current to current count
// after comment https://github.com/facebook/react/issues/14543#issuecomment-452996829
useEffect(() => (countRef.current = count));
useEffect(() => {
// ***** countRef.current is xhr function argument
const intervalId = setInterval(() => xhr(countRef.current), 5000);
return () => clearInterval(intervalId);
}, []);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={handleClick}>Click me</button>
</div>
);
}
EDIT
After comment: https://github.com/facebook/react/issues/14543#issuecomment-452996829