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.
Related
I am writing a React hook that allows me to use setInterval in my components. In order to do so I need to keep the latest version of the callback in a ref so that it can be accessed from the scope of the interval later on.
This is my code so far:
import { useRef, useEffect } from 'react'
export default function useInterval(
callback: () => void,
delay: number | null,
) {
const callbackRef = useRef(callback)
// Remember the latest callback.
useEffect(() => {
callbackRef.current = callback
})
useEffect(() => {
// Don't schedule if no delay is specified.
if (delay === null) {
return
}
const id = setInterval(() => callbackRef.current(), delay)
return () => clearInterval(id)
}, [delay])
}
My question is about the first instance of useEffect where the latest value is passed to the ref. According the the React documentation this code will execute after my component has rendered.
I can imagine this is useful when you are passing a ref to an element so you can be sure that it has a value after it has rendered. But if my code doesn't care about when the component renders, does it still make sense to keep this in a useEffect?
Would it make sense that I rewrite the code as follows:
import { useRef, useEffect } from 'react'
export default function useInterval(
callback: () => void,
delay: number | null,
) {
const callbackRef = useRef(callback)
// Remember the latest callback.
callbackRef.current = callback
useEffect(() => {
// Don't schedule if no delay is specified.
if (delay === null) {
return
}
const id = setInterval(() => callbackRef.current(), delay)
return () => clearInterval(id)
}, [delay])
}
When to use useEffect without dependencies vs. direct assignment?
From docs:
The Effect Hook lets you perform side effects in function components
A useEffect without dependencies (or undefined as dependencies) will be run at first render and every subsequent re-render, But always as a side-effect i.e. after the component has rendered.
A direct assignment (a sync operation) will be run at first render and every subsequent re-render, But always as in the render cycle. It may affect performance or delay the rendering.
So, when to use which one? It depends on your use case.
Which one to use in this (in question) use case?
I would say neither
useEffect(() => {
callbackRef.current = callback
})
nor
callbackRef.current = callback
seems correct in this use case.
Because we don't want to do the assignment - callbackRef.current = callback at every re-render. But we want to do it when there is a change in callback. So, the below one seems better:
useEffect(() => {
callbackRef.current = callback
}, [callback])
You may see this blog and this related post.
A demo which shows that an effect runs after as a side-effect (Log inside effect is always the last):
function useInterval(callback, delay) {
const callbackRef = React.useRef(callback)
React.useEffect(() => {
callbackRef.current = callback
}, [callback])
React.useEffect(() => {
if (delay !== null) {
const id = setInterval(() => callbackRef.current(), delay)
return () => clearInterval(id)
}
}, [delay])
}
function Demo() {
const [count, setCount] = React.useState(0)
function doThis() {
setCount(count + 1)
}
useInterval(doThis, 1000)
console.log('log - before effect')
React.useEffect(() => {
console.log('log inside effect')
})
console.log('log - after effect')
return <h1>{count}</h1>
}
ReactDOM.render(<Demo />, document.getElementById('root'))
<script crossorigin src="https://unpkg.com/react#17/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.production.min.js"></script>
<body>
<div id="root"></div>
</body>
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>
const [invalidateCacheKey, setInvalidateCacheKey] = useState(0);
const onChangeAssignee = () => {
setInvalidateCacheKey(invalidateCacheKey + 1);
mutate();
};
const selectOrder = () => {
dispatch(
showModal('SHOOTING_OPERATIONAL_VIEW', {
modalType: 'OPERATIONAL_VIEW',
modalProps: {
content: <ShootingActionsView updateOrders={mutate} onChangeAssignee={onChangeAssignee} />,
},
})
);
};
I have a functional component, I'm using useState to update the state of my invalidateCacheKey counter.
Then I have a dispatch method (react-redux) that displays a modal, I pass to the modal the callback (onChangeAssignee).
The problem is that: when the callback is fired the state (invalidateCacheKey) doesn't change inside the onChangeAssignee method (it is 0 after and before run the callback logging state inside the onChangeAssignee method), while inside the functional component (logging the state after useState declaration) the state (invalidateCacheKey) is 0 before the callback and is 1 after the callback.
I think that problem is dispatch method, it "stores" my state and it doesn't update it.
How to fix that?
Ciao, unfortunately hooks in react are async so if you try to write something like:
const onChangeAssignee = () => {
setInvalidateCacheKey(invalidateCacheKey + 1);
console.log(invalidateCacheKey)
...
};
you will log an old value of invalidateCacheKey, because setInvalidateCacheKey is async as I told you. To get updated value in react hooks you could use useEffect hook like:
useEffect(() => { // this will be triggered every time invalidateCacheKey changes
console.log(invalidateCacheKey) // this shows the la st value of invalidateCacheKey
}, [invalidateCacheKey])
As an alternative, you could use a use-state-with-callback library. With this library you could write something like:
import useStateWithCallback from 'use-state-with-callback';
...
const [invalidateCacheKey, setInvalidateCacheKey] = useStateWithCallback(0, invalidateCacheKey => {
console.log(invalidateCacheKey) // here you have the last value of invalidateCacheKey
});
Note: set state reading state itself is always discouraged in react hooks. I suggest you to use this way:
const onChangeAssignee = () => {
setInvalidateCacheKey(invalidateCacheKey => invalidateCacheKey + 1);
...
};
or
const onChangeAssignee = () => {
let appo = invalidateCacheKey;
setInvalidateCacheKey(appo + 1);
...
};
EDIT
Now lets say you need to use invalidateCacheKey in onChangeAssignee function. Lets suppose you worte code like this:
const onChangeAssignee = () => {
setInvalidateCacheKey(invalidateCacheKey + 1);
dostuff(invalidateCacheKey) // dostuff takes an old value of invalidateCacheKey so it doesn't work
};
You can solve this by moving dostuff into useEffect hook like:
useEffect(() => {
dostuff(invalidateCacheKey) // here dostuff works because it takes last value of invalidateCacheKey
}, [invalidateCacheKey])
const onChangeAssignee = () => {
setInvalidateCacheKey(invalidateCacheKey => invalidateCacheKey + 1);
};
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.
This question already has answers here:
State not updating when using React state hook within setInterval
(14 answers)
Closed 4 years ago.
The code is here: https://codesandbox.io/s/nw4jym4n0
export default ({ name }: Props) => {
const [counter, setCounter] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCounter(counter + 1);
}, 1000);
return () => {
clearInterval(interval);
};
});
return <h1>{counter}</h1>;
};
The problem is each setCounter trigger re-rendering so the interval got reset and re-created. This might looks fine since the state(counter) keeps incrementing, however it could freeze when combining with other hooks.
What's the correct way to do this? In class component it's simple with a instance variable holding the interval.
You could give an empty array as second argument to useEffect so that the function is only run once after the initial render. Because of how closures work, this will make the counter variable always reference the initial value. You could then use the function version of setCounter instead to always get the correct value.
Example
const { useState, useEffect } = React;
function App() {
const [counter, setCounter] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCounter(counter => counter + 1);
}, 1000);
return () => {
clearInterval(interval);
};
}, []);
return <h1>{counter}</h1>;
};
ReactDOM.render(
<App />,
document.getElementById('root')
);
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="root"></div>
A more versatile approach would be to create a new custom hook that stores the function in a ref and only creates a new interval if the delay should change, like Dan Abramov does in his great blog post "Making setInterval Declarative with React Hooks".
Example
const { useState, useEffect, useRef } = React;
function useInterval(callback, delay) {
const savedCallback = useRef();
// Remember the latest callback.
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
// Set up the interval.
useEffect(() => {
let id = setInterval(() => {
savedCallback.current();
}, delay);
return () => clearInterval(id);
}, [delay]);
}
function App() {
const [counter, setCounter] = useState(0);
useInterval(() => {
setCounter(counter + 1);
}, 1000);
return <h1>{counter}</h1>;
};
ReactDOM.render(
<App />,
document.getElementById('root')
);
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="root"></div>
As another answer by #YangshunTay already shows, it's possible to make it useEffect callback run only once and work similarly to componentDidMount. In this case it's necessary to use state updater function due to the limitations imposed by function scopes, otherwise updated counter won't be available inside setInterval callback.
The alternative is to make useEffect callback run on each counter update. In this case setInterval should be replaced with setTimeout, and updates should be limited to counter updates:
export default ({ name }: Props) => {
const [counter, setCounter] = useState(0);
useEffect(() => {
const timeout = setTimeout(() => {
setCounter(counter + 1);
}, 1000);
return () => {
clearTimeout(timeout);
};
}, [counter]);
return <h1>{counter}</h1>;
};
The correct way to do this would be to run the effect only once. Since you only need to run the effect once because during mounting, you can pass in an empty array as a second argument to achieve.
However, you will need to change setCounter to use the previous value of counter. The reason is because the callback passed into setInterval's closure only accesses the counter variable in the first render, it doesn't have access to the new counter value in the subsequent render because the useEffect() is not invoked the second time; counter always has the value of 0 within the setInterval callback.
Like the setState you are familiar with, state hooks have two forms: one where it takes in the updated state, and the callback form which the current state is passed in. You should use the second form and read the latest state value within the setState callback to ensure that you have the latest state value before incrementing it.
function Counter() {
const [counter, setCounter] = React.useState(0);
React.useEffect(() => {
const timer = setInterval(() => {
setCounter(prevCount => prevCount + 1); // <-- Change this line!
}, 1000);
return () => {
clearInterval(timer);
};
}, []); // Pass in empty array to run effect only once!
return (
<div>Count: {counter}</div>
);
}
ReactDOM.render(<Counter />, document.querySelector('#app'));
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>