反应钩子-正确的方式清除超时和间隔

我不明白为什么当我使用setTimeout函数时,我的react组件开始到infinite console.log。一切都在工作,但PC开始滞后的地狱。 有人说这个函数在超时时改变我的状态还有那个重新渲染组件,设置新定时器等等。现在我需要了解如何清除它是正确的。

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
}

明确在不同版本的代码中没有帮助:

const [showLoading, setShowLoading] = useState(true)
let timer1 = setTimeout(() => setShowLoading(true), 1000)
useEffect(
() => {
return () => {
clearTimeout(timer1)
}
},
[showLoading]
)
311947 次浏览

useEffect中定义的return () => { /*code/* }函数在每次useEffect运行时运行(除了第一次在组件挂载时呈现)和在组件卸载时运行(如果你不再显示组件)。

这是一种使用和清除超时或间隔的工作方式:

沙箱的例子

import { useState, useEffect } from "react";


const delay = 5;


export default function App() {
const [show, setShow] = useState(false);


useEffect(
() => {
let timer1 = setTimeout(() => setShow(true), delay * 1000);


// this will clear Timeout
// when component unmount like in willComponentUnmount
// and show will not change to true
return () => {
clearTimeout(timer1);
};
},
// useEffect will run only one time with empty []
// if you pass a value to array,
// like this - [data]
// than clearTimeout will run every time
// this value changes (useEffect re-run)
[]
);


return show ? (
<div>show is true, {delay}seconds passed</div>
) : (
<div>show is false, wait {delay}seconds</div>
);
}

如果您需要清除其他组件中的超时或间隔:

沙盒示例

import { useState, useEffect, useRef } from "react";


const delay = 1;


export default function App() {
const [counter, setCounter] = useState(0);
const timer = useRef(null); // we can save timer in useRef and pass it to child


useEffect(() => {
// useRef value stored in .current property
timer.current = setInterval(() => setCounter((v) => v + 1), delay * 1000);


// clear on component unmount
return () => {
clearInterval(timer.current);
};
}, []);


return (
<div>
<div>Interval is working, counter is: {counter}</div>
<Child counter={counter} currentTimer={timer.current} />
</div>
);
}


function Child({ counter, currentTimer }) {
// this will clearInterval in parent component after counter gets to 5
useEffect(() => {
if (counter < 5) return;


clearInterval(currentTimer);
}, [counter, currentTimer]);


return null;
}

文章来自Dan Abramov

您的计算机滞后,因为您可能忘记将空数组作为useEffect的第二个参数传入,并在回调中触发setState。这将导致一个无限循环,因为useEffect在呈现时被触发。

下面是一个在挂载时设置定时器并在卸载时清空定时器的工作方法:

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>

问题是你在useEffect外面调用了setTimeout,所以你在每次组件被呈现时都设置了一个新的超时,这个超时最终会被再次调用并改变状态,迫使组件再次重新呈现,这将设置一个新的超时,这…

因此,正如你已经发现的那样,使用setTimeoutsetInterval与钩子一起使用的方法是将它们包装在useEffect中,如下所示:

React.useEffect(() => {
const timeoutID = window.setTimeout(() => {
...
}, 1000);


return () => window.clearTimeout(timeoutID );
}, []);

作为deps = []useEffect的回调只会被调用一次。然后,卸载组件时将调用您返回的回调。

无论如何,我鼓励你创建自己的useTimeout钩子,这样你就可以通过使用setTimeout 以声明的方式来DRY和简化你的代码,就像Dan Abramov在使用React钩子使setInterval声明性中为setInterval建议的那样,两者非常相似:

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>

除了产生更简单、更清晰的代码外,这允许你通过传递delay = null来自动清除超时,并返回超时ID,以防你想自己手动取消它(这在Dan的帖子中没有涉及)。

如果你正在寻找setInterval而不是setTimeout的类似答案,请查看这个:https://stackoverflow.com/a/59274004/3723993

你还可以找到setTimeoutsetIntervaluseTimeoutuseInterval的声明式版本,还有一些用TypeScript在https://www.npmjs.com/package/@swyg/corre中编写的附加钩子。

我写了一个react钩子,再也不用处理超时。 就像React.useState():

新回答

const [showLoading, setShowLoading] = useTimeoutState(false)


// sets loading to true for 1000ms, then back to false
setShowLoading(true, { timeout: 1000})
export const useTimeoutState = <T>(
defaultState: T
): [T, (action: SetStateAction<T>, opts?: { timeout: number }) => void] => {
const [state, _setState] = useState<T>(defaultState);
const [currentTimeoutId, setCurrentTimeoutId] = useState<
NodeJS.Timeout | undefined
>();


const setState = useCallback(
(action: SetStateAction<T>, opts?: { timeout: number }) => {
if (currentTimeoutId != null) {
clearTimeout(currentTimeoutId);
}


_setState(action);


const id = setTimeout(() => _setState(defaultState), opts?.timeout);
setCurrentTimeoutId(id);
},
[currentTimeoutId, defaultState]
);
return [state, setState];
};

旧的答案

const [showLoading, setShowLoading] = useTimeoutState(false, {timeout: 5000})


// will set show loading after 5000ms
setShowLoading(true)
// overriding and timeouts after 1000ms
setShowLoading(true, { timeout: 1000})

设置多个状态将刷新超时,并且它将在上次setState设置的相同毫秒后超时。

香草js(未测试,typescript版本是):

import React from "react"


// sets itself automatically to default state after timeout MS. good for setting timeouted states for risky requests etc.
export const useTimeoutState = (defaultState, opts) => {
const [state, _setState] = React.useState(defaultState)
const [currentTimeoutId, setCurrentTimeoutId] = React.useState()


const setState = React.useCallback(
(newState: React.SetStateAction, setStateOpts) => {
clearTimeout(currentTimeoutId) // removes old timeouts
newState !== state && _setState(newState)
if (newState === defaultState) return // if already default state, no need to set timeout to set state to default
const id = setTimeout(
() => _setState(defaultState),
setStateOpts?.timeout || opts?.timeout
)
setCurrentTimeoutId(id)
},
[currentTimeoutId, state, opts, defaultState]
)
return [state, setState]
}

打字稿:

import React from "react"
interface IUseTimeoutStateOptions {
timeout?: number
}
// sets itself automatically to default state after timeout MS. good for setting timeouted states for risky requests etc.
export const useTimeoutState = <T>(defaultState: T, opts?: IUseTimeoutStateOptions) => {
const [state, _setState] = React.useState<T>(defaultState)
const [currentTimeoutId, setCurrentTimeoutId] = React.useState<number | undefined>()
// todo: change any to React.setStateAction with T
const setState = React.useCallback(
(newState: React.SetStateAction<any>, setStateOpts?: { timeout?: number }) => {
clearTimeout(currentTimeoutId) // removes old timeouts
newState !== state && _setState(newState)
if (newState === defaultState) return // if already default state, no need to set timeout to set state to default
const id = setTimeout(
() => _setState(defaultState),
setStateOpts?.timeout || opts?.timeout
) as number
setCurrentTimeoutId(id)
},
[currentTimeoutId, state, opts, defaultState]
)
return [state, setState] as [
T,
(newState: React.SetStateAction<T>, setStateOpts?: { timeout?: number }) => void
]
}```
const[seconds, setSeconds] = useState(300);


function TimeOut() {
useEffect(() => {
let interval = setInterval(() => {
setSeconds(seconds => seconds -1);
}, 1000);


return() => clearInterval(interval);
}, [])


function reset() {
setSeconds(300);
}


return (
<div>
Count Down: {seconds} left
<button className="button" onClick={reset}>
Reset
</button>
</div>
)
}

确保导入useState和useEffect。另外,添加在0处停止计时器的逻辑。

如果你想创建一个像“开始”这样的按钮;然后使用“;useInterval"Hook可能不适合,因为react不允许你调用组件顶部以外的钩子。

export default function Loading() {
// if data fetching is slow, after 1 sec i will show some loading animation
const [showLoading, setShowLoading] = useState(true)
const interval = useRef();


useEffect(() => {
interval.current = () => setShowLoading(true);
}, [showLoading]);


// make a function like "Start"
// const start = setInterval(interval.current(), 1000)


setInterval(() => interval.current(), 1000);


console.log('this message will render  every second')
return 1
}


如果你的超时在if结构中。试试这个:

useEffect(() => {
let timeout;


if (yourCondition) {
timeout = setTimeout(() => {
// your code
}, 1000);
} else {
// your code
}


return () => {
clearTimeout(timeout);
};
}, [yourDeps]);
export const useTimeout = () => {
const timeout = useRef();
useEffect(
() => () => {
if (timeout.current) {
clearTimeout(timeout.current);
timeout.current = null;
}
},
[],
);
return timeout;
};

您可以使用简单的钩子来共享超时逻辑。

const timeout = useTimeout();
timeout.current = setTimeout(your conditions)

如果在其他人给出的例子中使用useEffect钩子来避免连续地将setInterval方法附加(挂载)和分离(解挂载)到事件循环,则可以使用useReducer

想象这样一个场景,给定secondsminutes,你将倒数时间… 下面我们得到了一个reducer函数,它执行倒数逻辑
const reducer = (state, action) => {
switch (action.type) {
case "cycle":
if (state.seconds > 0) {
return { ...state, seconds: state.seconds - 1 };
}
if (state.minutes > 0) {
return { ...state, minutes: state.minutes - 1, seconds: 60 };
}
case "newState":
return action.payload;
default:
throw new Error();
}
}

现在我们要做的就是在每个间隔中分派cycle动作:

  const [time, dispatch] = useReducer(reducer, { minutes: 0, seconds: 0 });
const { minutes, seconds } = time;


const interval = useRef(null);
  

//Notice the [] provided, we are setting the interval only once (during mount) here.
useEffect(() => {
interval.current = setInterval(() => {
dispatch({ type: "cycle" });
}, 1000);
// Just in case, clear interval on component un-mount, to be safe.
return () => clearInterval(interval.current);
}, []);


//Now as soon as the time in given two states is zero, remove the interval.
useEffect(() => {
if (!minutes && !seconds) {
clearInterval(interval.current);
}
}, [minutes, seconds]);
// We could have avoided the above state check too, providing the `clearInterval()`
// inside our reducer function, but that would delay it until the next interval.

每10秒触发一次api:

useEffect(() => {
const timer = window.setInterval(() => {
// function of api call
}, 1000);


return () => {
window.clearInterval(timer);
}
}, [])

如有状态变化:

useEffect(() => {
// add condition to state if needed
const timer = window.setInterval(() => {
// function of api call
}, 1000);


return () => {
window.clearInterval(timer);
}
}, [state])