![](/img/trans.png)
[英]What is the right way to toggle a value inside object state in React hooks?
[英]React hooks - right way to clear timeouts and intervals
我不明白為什么當我使用setTimeout
函數時,我的反應組件開始到無限的 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]
)
定義return () => { /*code/* }
內功能useEffect
運行每次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;
}
問題是您在useEffect
之外調用setTimeout
,因此每次渲染組件時都會設置新的超時時間,最終將再次調用並更改狀態,迫使組件再次重新渲染,這將設置新的超時時間, 哪個...
因此,正如您已經發現的,將setTimeout
或setInterval
與鈎子一起使用的方法是將它們包裝在useEffect
,如下所示:
React.useEffect(() => {
const timeoutID = window.setTimeout(() => {
...
}, 1000);
return () => window.clearTimeout(timeoutID );
}, []);
由於deps = []
, useEffect
的回調只會被調用一次。 然后,您返回的回調將在組件卸載時調用。
無論如何,我鼓勵你創建你自己的useTimeout
鈎子,這樣你就可以通過聲明性地使用setTimeout
來干燥和簡化你的代碼,正如丹·阿布拉莫夫在使用 React Hooks 制作 setInterval Declarative 中建議的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 。
您還可以在https://gist.github.com/Danziger/336e75b6675223ad805a88c2dfdcfd4a 中找到setTimeout
和setInterval
、 useTimeout
和useInterval
聲明版本,以及用 TypeScript 編寫的自定義useThrottledCallback
鈎子。
您的計算機滯后是因為您可能忘記將空數組作為useEffect
的第二個參數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>
我寫了一個反應鈎子,再也不用處理超時了。 就像 React.useState() 一樣工作,但會超時以默認初始值,在這種情況下為 false:
const [showLoading, setShowLoading] = useTimeoutState(false, {timeout: 5000})
您還可以在特定的setStates
上覆蓋此超時:
const [showLoading, setShowLoading] = useTimeoutState(false, {timeout: 5000}) // can also not pass any timeout here
setShowLoading(true, {timeout: 1000}) // timeouts after 1000ms instead of 5000ms
設置多個狀態只會刷新函數,它會在最后一次setState
設置的相同毫秒后超時。
Vanilla js(未測試,打字稿版本是):
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”鈎子可能不合適,因為除了在組件頂部之外,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)
在 React 組件中使用 setTimeout 在一段時間后執行函數或代碼塊。 讓我們探索如何在 React 中使用 setTimeout。 還有一個類似的方法叫做setInterval
useEffect(() => {
const timer = setTimeout(() => {
console.log('This will run after 1 second!')
}, 1000);
return () => clearTimeout(timer);
}, []);
在 Intervals 的情況下,通過使用其他人給出的示例中的useEffect
鈎子來避免將setInterval
方法連續附加(安裝)和分離(卸載)到事件循環,您可能會受益於useReducer
的使用。
想象一個場景,在給定seconds
和minutes
的情況下,您應該倒計時......下面我們有一個執行倒計時邏輯的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.
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.