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
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.
I wanted to build a timer application in React using functional component and below are the requirements.
The component will display a number initialized to 0 know as counter.
The component will display a Start button below the counter number.
On clicking the Start button the counter will start running. This means the counter number will start incrementing by 1 for every one second.
When the counter is running(incrementing), the Start button will become the Pause button.
On clicking the Pause button, the counter will preserve its value (number) but stops running(incrementing).
The component will also display a Reset button.
On clicking the Reset button, the counter will go to its initial value(which is 0 in our case) and stops running(incrementing).
Below is the code that I have implemented, but clearInterval doesn't seems to be working, Also how do i implement Reset Button?
Code:
import React, { useState, useEffect } from "react";
export default function Counter() {
const [counter, setCounter] = useState(0);
const [flag, setFlag] = useState(false);
const [isClicked, setClicked] = useState(false);
var myInterval;
function incrementCounter() {
setClicked(!isClicked);
if (flag) {
myInterval = setInterval(
() => setCounter((counter) => counter + 1),
1000
);
setFlag(false);
} else {
console.log("sasdsad");
clearInterval(myInterval);
}
}
function resetCounter() {
clearInterval(myInterval);
setCounter(0);
}
useEffect(() => {
setFlag(true);
}, []);
return (
<div>
<p>{counter}</p>
<button onClick={incrementCounter}>
{isClicked ? "Pause" : "Start"}
</button>
<button onClick={resetCounter}>Reset</button>
</div>
);
}
Codesandbox link:
CodeSandbox
I did a slightly different version that use an extra useEffect that runs on isRunning (changed name from flag) change:
import React, { useState, useEffect, useRef } from "react";
export default function Counter() {
const [counter, setCounter] = useState(0);
// Change initial value to `false` if you don't want
// to have timer running on load
// Changed `flag` name to more significant name
const [isRunning, setIsRunning] = useState(false);
// You don't need 2 variable for this
//const [isClicked, setClicked] = useState(false);
// Using `useRef` to store a reference to the interval
const myInterval = useRef();
useEffect(() => {
// You had this line to start timer on load
// but you can just set the initial state to `true`
//setFlag(true);
// Clear time on component dismount
return () => clearInterval(myInterval.current);
}, []);
// useEffect that start/stop interval on flag change
useEffect(() => {
if (isRunning) {
myInterval.current = setInterval(
() => setCounter((counter) => counter + 1),
1000
);
} else {
clearInterval(myInterval.current);
myInterval.current = null;
}
}, [isRunning]);
// Now on click you only change the flag
function toggleTimer() {
setIsRunning((isRunning) => !isRunning);
}
function resetCounter() {
clearInterval(myInterval.current);
myInterval.current = null;
setCounter(0);
setIsRunning(false);
}
return (
<div>
<p>{counter}</p>
<button onClick={toggleTimer}>{isRunning ? "Pause" : "Start"}</button>
<button onClick={resetCounter}>Reset</button>
</div>
);
}
Demo: https://codesandbox.io/s/dank-night-wwxqz3?file=/src/Counter.js
As a little extra i've made a version that uses a custom hook useTimer. In this way the component code is way cleaner:
https://codesandbox.io/s/agitated-curie-nkjf62?file=/src/Counter.js
Use useRef to make the interval as a ref. Then use resetCounter() to clean the interval ref.
const intervalRef = useRef(null)
const incrementCounter = () => {
intervalRef.current = setInterval(() => {
setCounter(prevState => prevState + 1)
}, 1000);
};
const resetCounter = () => {
clearInterval(intervalRef.current);
intervalRef.current = null;
};
Between each rendering your variable myInterval value doesn't survive. That's why you need to use the [useRef][1] hook that save the reference of this variable across each rendering.
Besides, you don't need an flag function, as you have all information with the myClicked variable
Here is a modification of your code with those modifications. Don't hesitate if you have any question.
import React, { useState, useEffect, useRef } from "react";
export default function Counter() {
const [counter, setCounter] = useState(0);
const [isStarted, setIsStarted] = useState(false);
const myInterval = useRef();
function start() {
setIsStarted(true);
myInterval.current = setInterval(() => setCounter((counter) => counter + 1), 100);
100;
}
function pause() {
setIsStarted(false);
clearInterval(myInterval.current);
}
function resetCounter() {
clearInterval(myInterval.current);
setCounter(0);
}
return (
<div>
<p>{counter}</p>
{!isStarted ?
<button onClick={start}>
Start
</button>
:
<button onClick={pause}>
Pause
</button>
}
<button onClick={resetCounter}>Reset</button>
</div>
);
}
\\\
[1]: https://reactjs.org/docs/hooks-reference.html#useref
I'll just leave this here for anyone having the same problem.
in my case, the issue was node setInterval was used instead of window.setInterval.
this is a problem since this returns a type of Node.Timer which is an object instead of number (setInterval ID) for the clearInterval() to work as it needs an argument type of number. so to fix this,
React.useEffect(() => {
let timeoutId;
timeoutId = window.setInterval(callback, 100);
return = () => {
if(timeoutId) clearInterval(timeoutId)
}
}, [])
or in class components use componentWillMount()
You have to store myInterval in state. After that when button is clicked and flag is false, you can clear interval (myInterval in state).
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
When I run the code below, I expected the counter to be set to zero and start again like 1..2..3 and so on.
But when I click reset (on 5) it goes like 0..6..1..7..2.. and so on.
Why does this happen? What am I missing here? Thanks.
const App = (props) => {
const [counter, setCounter] = useState(0)
setTimeout(
() => setCounter(counter + 1),
1000
)
return (
<div>
<h1>{counter}</h1>
<button onClick={()=>setCounter(0)}>Reset</button>
</div>
)
}
It's best to run this kind of effect in a useEffect hook. That way, you can run the effect whenever counter is updated and provide a mechanism to cancel an existing timeout.
When the timeout hits 1 second, the counter stateful variable will be incremented. Since counter is specified in the useEffect dependency array, the effect will run again, queuing up another timeout.
We return a cleanup function from our userEffect hook. This is important because, if our counter is changed by some other mechanism (e.g., the Reset button), we'll want to cancel the in-progress timeout to start over!
import React, { useState, useEffect } from "react";
const App = (props) => {
const [counter, setCounter] = useState(0)
useEffect(() => {
const timeout = setTimeout(
() => setCounter(counter + 1),
1000
)
return () => {
clearTimeout(timeout);
}
}, [counter])
return (
<div>
<h1>{counter}</h1>
<button onClick={()=>setCounter(0)}>Reset</button>
</div>
)
}
When state is in a hook it can become stale and leak memory:
function App() {
const [greeting, setGreeting] = useState("hello");
const cb = useCallback(() => {
alert("greeting is " + greeting);
}, []);
return (
<div className="App">
<button onClick={() => cb()}>Click me</button>
<p>
Click the button above, and now update the greeting by clicking the one
below:
</p>
<button onClick={() => setGreeting("bye")}>
Update greeting
</button>
<p>Greeting is: {greeting}</p>
<p>
Now click the first button again and see that the callback still has the
old state.
</p>
</div>
);
}
Demo: https://codesandbox.io/s/react-hook-stale-datamem-leak-demo-9pchk
The problem with that is that we will run into infinite loops in a typical scenario to fetch some data if we follow Facebook's advice to list all dependencies always, as well as ensure we don't have stale data or memory leaks (as the example showed above):
const [state, setState] = useState({
number: 0
});
const fetchRandomNumber = useCallback(async () => {
if (state.number !== 5) {
const res = await fetch('randomNumber');
setState(v => ({ ...v, number: res.number }));
}
}, [setState, state.number]);
useEffect(() => {
fetchRandomNumber();
}, [fetchRandomNumber]);
Since Facebook say we should list fetchRandomNumber as a dependency (react-hooks/exhaustive-deps ESLint rule) we have to use useCallback to maintain a reference, but it regenerates on every call since it both depends on state.number and also updates it.
This is a contrived example but I've run into this many times when fetching data. Is there a workaround for this or is Facebook wrong in this situation?
Use the functional form of the state setter:
const fetchData = useCallback(async () => {
const res = await fetch(`url?page=${page}`);
setData((data) => ([...data, ...res.data]));
setPage((page) => page + 1);
}, [setData, setPage]);
Now you don't need data and page as your deps
You can also use a ref to run the effect only on mount :
const mounted = useRef(false);
useEffect(() => {
if(!mounted.current) {
fetchSomething();
mounted.current = true;
}
return () => { mounted.current = false }
}, [fetchSomething]);
And
const fetchSomething = useCallback(async () => {
...
}, [setData, setPage, data, page]);
fetchSomething is not a dependency here. You don't want to retrigger the effect, you only cause it once when the component mounts. Thats what useEffect(() => ..., []) is for.