问题
I don't understand why is when I use setTimeout
function my react component start to infinite console.log. Everything is working, but PC start to lag as hell.
Some people saying that function in timeout changing my state and that rerender component, that sets new timer and so on. Now I need to understand how to clear it's right.
export default function Loading() {
// if data fetching is slow, after 1 sec i will show some loading animation
const [showLoading, setShowLoading] = useState(true)
let timer1 = setTimeout(() => setShowLoading(true), 1000)
console.log('this message will render every second')
return 1
}
Clear in different version of code not helping to:
const [showLoading, setShowLoading] = useState(true)
let timer1 = setTimeout(() => setShowLoading(true), 1000)
useEffect(
() => {
return () => {
clearTimeout(timer1)
}
},
[showLoading]
)
回答1:
Return function in useEffect
runs every time useEffect
runs (except first run on component mount). Think about it as every time there is new useEffect
execution, the old one get deleted.
This is a working way to use and clear timeouts or intervals:
export default function Loading() {
const [showLoading, setShowLoading] = useState(false)
useEffect(
() => {
let timer1 = setTimeout(() => setShowLoading(true), 1000)
// this will clear Timeout when component unmont like in willComponentUnmount
return () => {
clearTimeout(timer1)
}
},
[] //useEffect will run only one time
//if you pass a value to array, like this [data] than clearTimeout will run every time this value changes (useEffect re-run)
)
return showLoading && <div>I will be visible after ~1000ms</div>
}
If you need to clear timeouts or intervals somewhere outside:
export default function Loading() {
const [showLoading, setShowLoading] = useState(false)
const timerToClearSomewhere = useRef(false) //now you can pass timer to another component
useEffect(
() => {
timerToClearSomewhere.current = setInterval(() => setShowLoading(true), 1000)
return () => {
clearInterval(timerToClearSomewhere.current)
}
},
[]
)
//here we can imitate clear from somewhere else place
useEffect(() => {
setTimeout(() => clearInterval(timerToClearSomewhere.current), 15000)
}, [])
return showLoading && <div>I will be visible after ~1000ms</div>
}
If you need to manage queue (change state inside timer/interval), look for my answer here.
回答2:
Your computer was lagging because you probably forgot to pass in the empty array as the second argument of useEffect
and was triggering a setState
within the callback. That causes an infinite loop because useEffect
is triggered on renders.
Here's a working way to set a timer on mount and clearing it on unmount:
function App() {
React.useEffect(() => {
const timer = window.setInterval(() => {
console.log('1 second has passed');
}, 1000);
return () => { // Return callback to run on unmount.
window.clearInterval(timer);
};
}, []); // Pass in empty array to run useEffect only on mount.
return (
<div>
Timer Example
</div>
);
}
ReactDOM.render(
<div>
<App />
</div>,
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>
回答3:
The problem is you are calling setTimeout
outside useEffect
, so you are setting a new timeout every time the component is rendered, which will eventually be invoked again and change the state, forcing the component to re-render again, which will set a new time, which...
So, as you have already found out, the way to use setTimeout
or setInterval
with hooks is to wrap them in useEffect
, like so:
React.useEffect(() => {
const timeoutID = window.setTimeout(() => {
...
}, 1000);
return () => window.clearInterval(timeoutID );
}, []);
As deps = []
, useEffect
's callback will only be called once. Then, the callback you return will be called when the component is unmounted.
Anyway, I would encourage you to create your own useTimeout
hook so that you can DRY and simplify your code by using setTimeout
declaratively, as Dan Abramov suggests for setInterval
in Making setInterval Declarative with React Hooks, which is quite similar:
function useTimeout(callback, delay) {
const timeoutRef = React.useRef();
const callbackRef = React.useRef(callback);
// Remember the latest callback:
//
// Without this, if you change the callback, when setTimeout kicks in, it
// will still call your old callback.
//
// If you add `callback` to useEffect's deps, it will work fine but the
// timeout will be reset.
React.useEffect(() => {
callbackRef.current = callback;
}, [callback]);
// Set up the timeout:
React.useEffect(() => {
if (typeof delay === 'number') {
timeoutRef.current = window.setTimeout(() => callbackRef.current(), delay);
// Clear timeout if the components is unmounted or the delay changes:
return () => window.clearTimeout(timeoutRef.current);
}
}, [delay]);
// In case you want to manually clear the timeout from the consuming component...:
return timeoutRef;
}
const App = () => {
const [isLoading, setLoading] = React.useState(true);
const [showLoader, setShowLoader] = React.useState(false);
// Simulate loading some data:
const fakeNetworkRequest = React.useCallback(() => {
setLoading(true);
setShowLoader(false);
// 50% of the time it will display the loder, and 50% of the time it won't:
window.setTimeout(() => setLoading(false), Math.random() * 4000);
}, []);
// Initial data load:
React.useEffect(fakeNetworkRequest, []);
// After 2 second, we want to show a loader:
useTimeout(() => setShowLoader(true), isLoading ? 2000 : null);
return (<React.Fragment>
<button onClick={ fakeNetworkRequest } disabled={ isLoading }>
{ isLoading ? 'LOADING... 📀' : 'LOAD MORE 🚀' }
</button>
{ isLoading && showLoader ? <div className="loader"><span className="loaderIcon">📀</span></div> : null }
{ isLoading ? null : <p>Loaded! ✨</p> }
</React.Fragment>);
}
ReactDOM.render(<App />, document.querySelector('#app'));
body,
button {
font-family: monospace;
}
body, p {
margin: 0;
}
#app {
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
}
button {
margin: 32px 0;
padding: 8px;
border: 2px solid black;
background: transparent;
cursor: pointer;
border-radius: 2px;
}
.loader {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
font-size: 128px;
background: white;
}
.loaderIcon {
animation: spin linear infinite .25s;
}
@keyframes spin {
from { transform:rotate(0deg) }
to { transform:rotate(360deg) }
}
<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>
Apart from producing simpler and cleaner code, this allows you to automatically clear the timeout by passing delay = null
and also returns the timeout ID, in case you want to cancel it yourself manually (that's not covered in Dan's posts).
If you are looking for a similar answer for setInterval
rather than setTimeout
, check this out: https://stackoverflow.com/a/59274004/3723993.
You can also find declarative version of setTimeout
and setInterval
, useTimeout
and useInterval
, plus a custom useThrottledCallback
hook written in TypeScript in https://gist.github.com/Danziger/336e75b6675223ad805a88c2dfdcfd4a.
来源:https://stackoverflow.com/questions/53090432/react-hooks-right-way-to-clear-timeouts-and-intervals