React Hook Implementation causes slower count sidebyside - javascript

Why is the first functional component slower than the second functional component when they are composed side by side in a React application and you switch tabs and then go back to it after a few seconds?
Here is a sandbox so you can see it in action.
https://codesandbox.io/s/useeffect-87pm7
function SlowerCounter() {
const [count, setCount] = useState(0);
useEffect(() => {
const intervalId = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(intervalId);
}, [count]);
return <div>The count is: {count}</div>;
}
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const intervalId = setInterval(() => {
setCount(count => ++count);
}, 1000);
return () => clearInterval(intervalId);
}, []);
return <div>The count is: {count}</div>;
}

The problem is that in the second useEffect you set and clear a new interval on every render, while others keep running on the same instance.
It causes a different effect on the interval when you switching tabs, therefore, the useEffect logic and the understanding of how browser tabs work causes the "bug".
Try adding logging for every clearing function in useEffect:
function SuggestedWayToUseEffectOneButItsActuallyNotWorkingCorrectly() {
const [count, setCount] = useState(0);
useEffect(() => {
const intervalId = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => {
console.log('cleared 2');
clearInterval(intervalId);
};
}, [count]);
return <div>The count is: {count}</div>;
}

When you pass second parameter with an empty array in the useEffect it doesn't need to check on every render. It will continue updating from previous value. But when you pass the parameter with a lookup value in an array, then it will need to check on every render and app will continue updating from previous value.
So, when switching to different tabs back and forth the app will unmount and re-mount and the calculation between them you find a little bit slower as it checks for the value after rendering but with an empty array will continue updating without any check. So, the time gap between useEffect cached value check is what you see is slower.

Related

React setInterval and useState

I have 2 question. First, why this code does not work. Second, why this code slow when it comes 2^n -1 for example 1-3-7-15
let time = 0
function App() {
const [mytime, setMytime] = useState(time)
setInterval(() => {
time += 1
setMytime(time)
}, 1000)
return <div> {mytime} </div>
Issue
The setInterval gets called each time when mytime changes for rerendering (when you call the setMytime). And the number of setInterval calls grows exponentially. This would lead to a memory leak as well.
Solution
You should run it only once. You should use useEffect hook with an empty dependency array.
Try like this.
import { useEffect, useState } from "react";
function App() {
const [mytime, setMytime] = useState(0);
useEffect(() => {
// create a interval and get the id
const myInterval = setInterval(() => {
setMytime((prevTime) => prevTime + 1);
}, 1000);
// clear out the interval using the id when unmounting the component
return () => clearInterval(myInterval);
}, []);
return <div> {mytime} </div>;
}
export default App;

useEffect with react-hooks/exhaustive-deps where callbacks depend on state

I've got a hooks problem I've been unable to find an answer for. The closest article being this other post.
Essentially I want to have a function that is invoked once per component lifecycle with hooks. I'd normally use useEffect as such:
useEffect(() => {
doSomethingOnce()
setInterval(doSomethingOften, 1000)
}, [])
I'm trying not to disable eslint rules and get a react-hooks/exhaustive-deps warning.
Changing this code to the following creates an issue of my functions being invoked every time the component is rendered... I don't want this.
useEffect(() => {
doSomethingOnce()
setInterval(doSomethingOften, 1000)
}, [doSomethingOnce])
The suggested solution is to wrap my functions in useCallback but what I can't find the answer to is what if doSomethingOnce depends on the state.
Below is the minimal code example of what I am trying to achieve:
import "./styles.css";
import { useEffect, useState, useCallback } from "react";
export default function App() {
const [count, setCount] = useState(0);
const getRandomNumber = useCallback(
() => Math.floor(Math.random() * 1000),
[]
);
const startCounterWithARandomNumber = useCallback(() => {
setCount(getRandomNumber());
}, [setCount, getRandomNumber]);
const incrementCounter = useCallback(() => {
setCount(count + 1);
}, [count, setCount]);
useEffect(() => {
startCounterWithARandomNumber();
setInterval(incrementCounter, 1000);
}, [startCounterWithARandomNumber, incrementCounter]);
return (
<div className="App">
<h1>Counter {count}</h1>
</div>
);
}
As you can see from this demo since incrementCounter depends on count it gets recreated. Which in turn re-invokes my useEffect callback that I only wanted to be called the once. The result is that the startCounterWithARandomNumber and incrementCounter get called many more times than I would expect.
Any help would be much appreciated.
Update
I should have pointed out, this is a minimal example of a real-use case where the events are aysncronous.
In my real code the context is a live transcription app. I initially make a fetch call to GET /api/all.json to get the entire transcription then poll GET /api/latest.json every second merging the very latest speech to text from with the current state. I tried to emulate this in my minimal example with a seed to start followed by a polled method call that has a dependency on the current state.
I think you have overcomplicated your code considerably.
Solution
If you want the startCounterWithARandomNumber function to run once to set initial state, then just use a state initialization function.
const getRandomNumber = () => Math.floor(Math.random() * 1000);
const [count, setCount] = useState(getRandomNumber);
As for the effect setting up the interval, you will also want to only run this once when mounting. Move the interval callback that "ticks" and increments the count state into the effect callback so it is no longer a dependency. Use a functional state update to correctly update from the previous state and not the initial state. Don't forget to return a cleanup function from the useEffect hook to clear any running interval timers.
useEffect(() => {
const incrementCounter = () => setCount((c) => c + 1);
const timer = setInterval(incrementCounter, 1000);
return () => clearInterval(timer);
}, []);
Demo
Full code:
import { useEffect, useState } from "react";
export default function App() {
const getRandomNumber = () => Math.floor(Math.random() * 1000);
const [count, setCount] = useState(getRandomNumber);
useEffect(() => {
const incrementCounter = () => setCount((c) => c + 1);
const timer = setInterval(incrementCounter, 1000);
return () => clearInterval(timer);
}, []);
return (
<div className="App">
<h1>Counter {count}</h1>
</div>
);
}
Update
I guess it's still a bit unclear what your real code and use case is doing. It seems you've written your doSomethingOnce and doSomethingOften functions in such a way so as to still have some outer dependency that when used inside an useEffect hook is flagged by the linter.
Based on your counting example here an example that doesn't warn about dependencies.
const getRandomNumber = () => Math.floor(Math.random() * 1000);
const fetch = () =>
new Promise((resolve) => {
setTimeout(() => {
if (Math.random() < 0.1) { // 10% to return new value
resolve(getRandomNumber());
}
}, 3000);
});
function App() {
const [count, setCount] = React.useState(0);
const doSomethingOnce = () => {
console.log('doSomethingOnce');
fetch().then((val) => setCount(val));
};
const doSomethingOften = () => {
console.log('doSomethingOften');
fetch().then((val) => {
console.log('new value, update state')
setCount(val);
});
};
React.useEffect(() => {
doSomethingOnce();
const timer = setInterval(doSomethingOften, 1000);
return () => clearInterval(timer);
}, []);
// This effect is only to update state independently of any state updates from "polling"
React.useEffect(() => {
const tick = () => setCount((c) => c + 1);
const timer = setInterval(tick, 100);
return () => clearInterval(timer);
}, []);
return (
<div className="App">
<h1>Counter {count}</h1>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(
<App />,
rootElement
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
This is sort of a contrived and "tuned" solution though since the "fetch" only resolves when it's returning a new value. In reality if you are likely polling and completely replacing a chunk of state that the component isn't regularly modifying (at least I hope it isn't as this makes merging/synchronizing more difficult). I can improve this answer if there were a better/clearer view of what your code is actually doing.
I dont know the usecase you want to solve so keeping minimal code changes to your code.
A very simple solution would be to increment counter as
const incrementCounter = useCallback(() => {
setCount(prevCount => prevCount+1);
}, [setCount]);
This way the increment counter does not rely on count.
Forked from your sandbox.
https://codesandbox.io/s/fragrant-architecture-t58bs

change the value of useState with setInterval

I have a simple component with useState that increase a counter in each click -
function Counter() {
let [counter, setCounter] = useState(0);
const incCounter = () => {
setCounter(counter + 1);
};
return (
<div className="App">
<h1>{counter}</h1>
<button onClick={incCounter}>Inc</button>
</div>
);
}
Here is its demo
and now I want to call the increase function each 1 second , so I added this piece of code into the component function -
useEffect(() => {
setInterval(() => {
incCounter();
}, 1000);
}, []);
Here is its demo
but I don't see the counter increased in the component.
How should I write it correctly and see the counter increased in each 1 second as expected ?
Issue
You've a stale enclosure of the state value from the render cycle the timer callback was set from.
Solution
Use a functional state update so the state is updated from the previous state, not the state from the render cycle the update was enqueued in. This allows the interval callback and click handler to seamlessly update state, independently from each other.
setCounter(counter => counter + 1);
In fact, you should use a functional state update anytime the next state value depends on any previous state value. Incrementing a counter is the classic example of using functional updates.
Don't forget to return a cleanup function to clear the interval for when the component umnounts.
Full code
function Counter() {
let [counter, setCounter] = useState(0);
const incCounter = () => {
setCounter((counter) => counter + 1);
};
useEffect(() => {
const timerId = setInterval(() => {
incCounter();
}, 1000);
return () => clearInterval(timerId);
}, []);
return (
<div className="App">
<h1>{counter}</h1>
<button onClick={incCounter}>Inc</button>
</div>
);
}
Demo

React interval using old state inside of useEffect

I ran into a situation where I set an interval timer from inside useEffect. I can access component variables and state inside the useEffect, and the interval timer runs as expected. However, the timer callback doesn't have access to the component variables / state. Normally, I would expect this to be an issue with "this". However, I do not believe "this" is the the case here. No puns were intended. I have included a simple example below:
import React, { useEffect, useState } from 'react';
const App = () => {
const [count, setCount] = useState(0);
const [intervalSet, setIntervalSet] = useState(false);
useEffect(() => {
if (!intervalSet) {
setInterval(() => {
console.log(`count=${count}`);
setCount(count + 1);
}, 1000);
setIntervalSet(true);
}
}, [count, intervalSet]);
return <div></div>;
};
export default App;
The console outputs only count=0 each second. I know that there's a way to pass a function to the setCount which updates current state and that works in this trivial example. However, that was not the point I was trying to make. The real code is much more complex than what I showed here. My real code looks at current state objects that are being managed by async thunk actions. Also, I am aware that I didn't include the cleanup function for when the component dismounts. I didn't need that for this simple example.
The first time you run the useEffect the intervalSet variable is set to true and your interval function is created using the current value (0).
On subsequent runs of the useEffect it does not recreate the interval due to the intervalSet check and continues to run the existing interval where count is the original value (0).
You are making this more complicated than it needs to be.
The useState set function can take a function which is passed the current value of the state and returns the new value, i.e. setCount(currentValue => newValue);
An interval should always be cleared when the component is unmounted otherwise you will get issues when it attempts to set the state and the state no longer exists.
import React, { useEffect, useState } from 'react';
const App = () => {
// State to hold count.
const [count, setCount] = useState(0);
// Use effect to create and clean up the interval
// (should only run once with current dependencies)
useEffect(() => {
// Create interval get the interval ID so it can be cleared later.
const intervalId = setInterval(() => {
// use the function based set state to avoid needing count as a dependency in the useEffect.
// this stops the need to code logic around stoping and recreating the interval.
setCount(currentCount => {
console.log(`count=${currentCount}`);
return currentCount + 1;
});
}, 1000);
// Create function to clean up the interval when the component unmounts.
return () => {
if (intervalId) {
clearInterval(intervalId);
}
}
}, [setCount]);
return <div></div>;
};
export default App;
You can run the code and see this working below.
const App = () => {
// State to hold count.
const [count, setCount] = React.useState(0);
// Use effect to create and clean up the interval
// (should only run once with current dependencies)
React.useEffect(() => {
// Create interval get the interval ID so it can be cleared later.
const intervalId = setInterval(() => {
// use the function based set state to avoid needing count as a dependency in the useEffect.
// this stops the need to code logic around stoping and recreating the interval.
setCount(currentCount => {
console.log(`count=${currentCount}`);
return currentCount + 1;
});
}, 1000);
// Create function to clean up the interval when the component unmounts.
return () => {
if (intervalId) {
clearInterval(intervalId);
}
}
}, [setCount]);
return <div></div>;
};
ReactDOM.render(<App />, document.getElementById('app'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="app"></div>
If you need a more complex implementation as mention in your comment on another answer, you should try using a ref perhaps. For example, this is a custom interval hook I use in my projects. You can see there is an effect that updates callback if it changes.
This ensures you always have the most recent state values and you don't need to use the custom updater function syntax like setCount(count => count + 1).
const useInterval = (callback, delay) => {
const savedCallback = useRef()
useEffect(() => {
savedCallback.current = callback
}, [callback])
useEffect(() => {
if (delay !== null) {
const id = setInterval(() => savedCallback.current(), delay)
return () => clearInterval(id)
}
}, [delay])
}
// Usage
const App = () => {
useInterval(() => {
// do something every second
}, 1000)
return (...)
}
This is a very flexible option you could use. However, this hook assumes you want to start your interval when the component mounts. Your code example leads me to believe you want this to start based on the state change of the intervalSet boolean. You could update the custom interval hook, or implement this in your component.
It would look like this in your example:
const useInterval = (callback, delay, initialStart = true) => {
const [start, setStart] = React.useState(initialStart)
const savedCallback = React.useRef()
React.useEffect(() => {
savedCallback.current = callback
}, [callback])
React.useEffect(() => {
if (start && delay !== null) {
const id = setInterval(() => savedCallback.current(), delay)
return () => clearInterval(id)
}
}, [delay, start])
// this function ensures our state is read-only
const startInterval = () => {
setStart(true)
}
return [start, startInterval]
}
const App = () => {
const [countOne, setCountOne] = React.useState(0);
const [countTwo, setCountTwo] = React.useState(0);
const incrementCountOne = () => {
setCountOne(countOne + 1)
}
const incrementCountTwo = () => {
setCountTwo(countTwo + 1)
}
// Starts on component mount by default
useInterval(incrementCountOne, 1000)
// Starts when you call `startIntervalTwo(true)`
const [intervalTwoStarted, startIntervalTwo] = useInterval(incrementCountTwo, 1000, false)
return (
<div>
<p>started: {countOne}</p>
<p>{intervalTwoStarted ? 'started' : <button onClick={startIntervalTwo}>start</button>}: {countTwo}</p>
</div>
);
};
ReactDOM.render(<App />, document.getElementById('app'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="app"></div>
The problem is the interval is created only once and keeps pointing to the same state value. What I would suggest - move firing the interval to separate useEffect, so it starts when the component mounts. Store interval in a variable so you are able to restart it or clear. Lastly - clear it with every unmount.
const App = () => {
const [count, setCount] = React.useState(0);
const [intervalSet, setIntervalSet] = React.useState(false);
React.useEffect(() => {
setIntervalSet(true);
}, []);
React.useEffect(() => {
const interval = intervalSet ? setInterval(() => {
setCount((c) => {
console.log(c);
return c + 1;
});
}, 1000) : null;
return () => clearInterval(interval);
}, [intervalSet]);
return null;
};
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="root"></div>

SetInterval is not showing updated state

I have set the state to true before calling the setInterval function. But even though the useEffect hook is being triggered with the new value of the state, it's not being reflected in the setInterval function.
Code sandbox here: https://jsfiddle.net/6e05tc2L/3/
let interval;
const Component = () => {
React.useEffect(() => {
console.log('State updated to', state);
});
const [state, setState] = React.useState(false);
const on = () => {
setState(true);
interval = setInterval(() => {
console.log(state);
}, 1000);
}
const off = () => {
setState(false);
clearInterval(interval);
}
const toggle = () => state ? off() : on()
return (<div>
<button onClick={toggle}>Toggle State</button>
</div>);
}
ReactDOM.render(
<Component />,
document.getElementById('container')
);
Shouldn't it be using the newer value of state once it's updated?
The values inside the function which you pass to useEffect are refreshed on every render, because useEffect uses a new definition of the function you pass to it.
But the function passed to setInterval is defined once and it closes over the old stale value of state. Which has not yet updated.
Closures are tricky with hooks, but the thing to realize is that useEffect creates a new function for each render and hence each time the function closes over a fresh state value.
The trick then is to call your setInterval related code inside a useEffect itself, which itself depends on the changing value of state
React.useEffect(() => {
if(state) {
interval = setInterval(() => {
console.log(state);
}, 1000);
} else {
clearInterval(interval);
}
}, [state]);
Or, better, use a useInterval hook which takes care of these details for you.
setInterval always has access to the value of your component's first render because the function passed to setInterval closes around that value and is never redeclared. You can use a custom hook to fix this:
function useInterval(callback, delay) {
const savedCallback = useRef();
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
function tick() {
savedCallback.current();
}
let id = setInterval(tick, delay);
return () => clearInterval(id);
}, [delay]);
}
That implementation and a thorough explanation of the mismatch between React Hooks and setInterval is from Making setInterval Declarative with React Hooks by Dan Abramov, one of the React contributors.
I'm not a ReactJS expert, but I guess the state you are logging is not refreshed since it is declared once and never refreshed. If React.useState(false) is the method that is giving you your state, you should use it in your interval function.
Here is an example of what I'm trying to explain:
const object = { value: false }
const notRefreshed = object.value // here we are copying the value
const interval = setInterval(() => {
const refreshed = object.value // here we are using the reference to copy the latest value
console.log("refreshed", refreshed)
console.log("notRefreshed", notRefreshed)
}, 500)
setTimeout(() => object.value = true, 1600)
setTimeout(() => clearInterval(interval), 2600)
If you want to reaload your component whenever your state change you should create your useEffect like this.
React.useEffect(() => {
console.log('State updated to', state);
}, [state]);
The way you created is the same as componentDidMount() with an array as the second parameter it's like componentDidUpdate() with it's dependencies. So, your component will re-render whenever your state change.
To solve the infinity call of setTimeout you can do this where you create the function
React.useCallback(() => {
setInterval(() => {
console.log(state);
}, 1000);
})
with this React will know that you want to create this function just once.

Categories

Resources