225 votes

Crochets React - bonne façon d'effacer les délais et les intervalles

Je ne comprends pas pourquoi, lorsque j'utilise setTimeout fonction mon composant react commence à être infini console.log. Tout fonctionne, mais le PC se met à lagger comme l'enfer. Certaines personnes disent que la fonction en timeout changer mon état et que le composant rerender, qui définit un nouveau timer et ainsi de suite. Maintenant, j'ai besoin de comprendre comment effacer c'est juste.

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
}

L'effacement dans une version différente du code n'aide pas :

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

363voto

RTW Points 3991

Défini return () => { /*code/* } fonction à l'intérieur useEffect fonctionne à chaque fois useEffect s'exécute (sauf le premier rendu au montage du composant) et au démontage du composant (si vous n'affichez plus le composant).

Il s'agit d'un moyen efficace d'utiliser et d'effacer les délais ou les intervalles :

Exemple de bac à sable .

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>
  );
}

Si vous devez effacer les délais ou les intervalles dans un autre composant :

Exemple de bac à sable.

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;
}

Article de Dan Abramov .

65voto

Danziger Points 5234

Le problème est que vous appelez setTimeout à l'extérieur de useEffect Vous définissez donc un nouveau délai d'attente à chaque fois que le composant est rendu, ce qui finira par être invoqué à nouveau et modifiera l'état, obligeant le composant à être rendu à nouveau, ce qui définira un nouveau délai d'attente, qui...

Donc, comme vous l'avez déjà découvert, la façon d'utiliser setTimeout ou setInterval avec des crochets est de les envelopper dans useEffect comme ça :

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

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

Comme deps = [] , useEffect Le rappel de l'utilisateur ne sera appelé qu'une seule fois. Ensuite, le callback que vous renvoyez sera appelé lorsque le composant sera démonté.

Quoi qu'il en soit, je vous encourage à créer votre propre useTimeout de sorte que vous puissiez DRY et simplifier votre code en utilisant setTimeout de manière déclarative comme le suggère Dan Abramov pour setInterval sur Rendre setInterval déclaratif avec React Hooks qui est assez similaire :

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>

En plus de produire un code plus simple et plus propre, cela vous permet d'effacer automatiquement le délai d'attente en passant la commande delay = null et renvoie également l'ID du délai d'attente, au cas où vous voudriez l'annuler vous-même manuellement (ce n'est pas couvert dans les posts de Dan).

Si vous cherchez une réponse similaire pour setInterval plutôt que setTimeout regarde ça : https://stackoverflow.com/a/59274004/3723993 .

Vous pouvez également trouver une version déclarative de setTimeout et setInterval , useTimeout et useInterval ainsi qu'un useThrottledCallback écrite en TypeScript dans https://gist.github.com/Danziger/336e75b6675223ad805a88c2dfdcfd4a .

10voto

yangshun Points 273

Votre ordinateur était en panne parce que vous avez probablement oublié de passer le tableau vide comme deuxième argument de la fonction useEffect et déclenchait une setState dans le callback. Cela provoque une boucle infinie car useEffect est déclenché lors des rendus.

Voici un moyen efficace de définir une minuterie au montage et de l'effacer au démontage :

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>

10voto

Gabriel Petersson Points 2009

J'ai écrit un hook react pour ne plus jamais avoir à gérer les timeouts. Il fonctionne comme React.useState(), mais le délai d'attente est fixé par défaut à la valeur initiale, dans ce cas false :

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

Vous pouvez également modifier ce délai pour certaines applications spécifiques. 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

La définition de plusieurs états ne fera que rafraîchir la fonction et elle expirera au bout de la même ms que la dernière fonction. setState set.

Vanilla js (non testé, la version typescript l'est) :

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]
}

Dactylographié :

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
  ]
}```

1voto

Kabiraj Kharel Points 46
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>
)
}

Assurez-vous d'importer useState et useEffect. Ajoutez également la logique permettant d'arrêter le minuteur à 0.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X