簡體   English   中英

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 ,因此每次渲染組件時都會設置新的超時時間,最終將再次調用並更改狀態,迫使組件再次重​​新渲染,這將設置新的超時時間, 哪個...

因此,正如您已經發現的,將setTimeoutsetInterval與鈎子一起使用的方法是將它們包裝在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 中找到setTimeoutsetIntervaluseTimeoutuseInterval聲明版本,以及用 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的使用。

想象一個場景,在給定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.

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM