1
votes

Réaction setInterval in useEffect avec délai setTimeout

Je souhaite exécuter un intervalle avec un délai pour la première fois qu'il se déclenche. Comment puis-je faire cela avec useEffect? En raison de la syntaxe, j'ai eu du mal à réaliser ce que je voulais faire

La fonction d'intervalle

useEffect(() => {
    setTimeout(() => {
//I want to run the interval here, but it will only run once 
//because of no dependencies. If i populate the dependencies, 
//setTimeout will run more than once.
}, Math.random() * 1000);
  }, []);

La fonction de retard

  useEffect(()=>{
    const timer = setInterval(() => {
      //do something here
      return ()=> clearInterval(timer)
    }, 1000);
  },[/*dependency*/])


2 commentaires

setInterval a déjà un délai à la première fois, avez-vous besoin d'un délai d'expiration différent de la période d'intervalle?


ionush, ce n'est pas un problème trivial. voir ma réponse pour une explication


4 Réponses :


-1
votes

Est-ce ce que vous voulez réaliser? le tableau vide sur useeffect indique qu'il exécutera ce code une fois l'élément rendu

<div id="react"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script></script>
const {useState, useEffect} = React;
// Example stateless functional component
const SFC = props => {
  
  const [value,setvalue] = useState('initial')
  const [counter,setcounter] = useState(0)
  

  useEffect(() => {
    const timer = setInterval(() => {
    setvalue('delayed value')
    setcounter(counter+1)
    clearInterval(timer)
    }, 2000);
  }, []);
  
  return(<div>
          Value:{value} | counter:{counter}
         </div>)
};

// Render it
ReactDOM.render(
  <SFC/>,
  document.getElementById("react")
);


2 commentaires

Non, je veux exécuter un setInterval


J'ai édité l'extrait, vérifiez si c'est ce que vous voulez, j'ai ajouté un compteur pour vérifier si l'intervalle est arrêté après l'utilisation de clearInterval



1
votes

Je pense que ce que vous essayez de faire est le suivant:

<div id="react"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script></script>
const DelayTimer = props => {
  const [value, setvalue] = React.useState("initial");
  const [counter, setcounter] = React.useState(0);

  React.useEffect(() => {
    let timer;
    setTimeout(() => {
      setvalue("delayed value");
      timer = setInterval(() => {
        setcounter(c => c + 1);
      }, 1000);
    }, 2000);
    return () => clearInterval(timer);
  }, []);

  return (
    <div>
      Value:{value} | counter:{counter}
    </div>
  );
};

// Render it
ReactDOM.render(<DelayTimer />, document.getElementById("react"));


1 commentaires

Merci. Un des problèmes que j'utilisais avec les valeurs useState dans la fermeture setInterval et les valeurs étaient périmées, j'ai donc dû utiliser la valeur setState (latestState => {}) latestState // par exemple. [state, setState] = useState ("") pour récupérer le dernier état



2
votes

pour commencer

Pensez à démêler les préoccupations de votre composant et à écrire de petits morceaux. Ici, nous avons un hook personnalisé useInterval qui définit strictement la partie setInterval du programme. J'ai ajouté quelques lignes console.log afin que nous puissions observer les effets -

<div id="react"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script></script>

Maintenant, quand nous écrivons MyComp , nous pouvons gérer la partie setTimeout du programme -

const { useState, useEffect, useRef, useCallback } = React

const append = (a = [], x = null) =>
  [ ...a, x ]
  
const remove = (a = [], x = null) =>
{ const pos = a.findIndex(q => q === x)
  if (pos < 0) return a
  return [ ...a.slice(0, pos), ...a.slice(pos + 1) ]
}

function useInterval (f, delay = 1000)
{ const interval = useRef(f)
  const [busy, setBusy] = useState(0)
  
  useEffect(() => {
    interval.current = f
  }, [f])
  
  useEffect(() => {
    // start
    if (!busy) return
    setBusy(true)
    const t =
      setInterval(_ => interval.current(), delay)
      
    // stop
    return () => {
      setBusy(false)
      clearInterval(t)
    }
  }, [busy, delay])
  
  return [
    _ => setBusy(true),  // start
    _ => setBusy(false), // stop
    busy                 // isBusy
  ]
}

function MyTimer ({ delay = 1000, ... props })
{ const [counter, setCounter] =
    useState(0)
  
  const [doubler, setDoubler] = useState(false)
  const [turbo, setTurbo] = useState(false)
  
  const [start, stop, busy] =
    useInterval
      ( doubler
          ? _ => setCounter(x => x * 2)
          : _ => setCounter(x => x + 1)
      , turbo
          ? Math.floor(delay / 2)
          : delay
      )
      
  const toggleTurbo = () =>
    setTurbo(t => !t)
    
  const toggleDoubler = () =>
    setDoubler(t => !t)
  
  return <span>
    {counter}
    <button
      onClick={start}
      disabled={busy}
      children="Start"
    />
    <button
      onClick={toggleDoubler}
      disabled={!busy}
      children={`Doubler: ${doubler ? "ON" : "OFF"}`}
    />
    <button
      onClick={toggleTurbo}
      disabled={!busy}
      children={`Turbo: ${turbo ? "ON" : "OFF"}`}
    />
    <button
      onClick={stop}
      disabled={!busy}
      children="Stop"
    />
  </span>
}

function Main ()
{ const [timers, setTimers] = useState([])
  
  const addTimer = () =>
    setTimers(r => append(r, <MyTimer />))
    
  const destroyTimer = c => () =>
    setTimers(r => remove(r, c))
  
  return <main>
    <p>Run in expanded mode. Open your developer console</p>
    <button
      onClick={addTimer}
      children="Add Timer"
    />
    { timers.map((c, key) =>
      <div key={key}>
        {c}
        <button
          onClick={destroyTimer(c)} 
          children="Destroy"
        />
      </div>
    )}
  </main>
}

ReactDOM.render
  ( <Main/>
  , document.getElementById("react")
  )

Nous pouvons maintenant useInterval dans diverses parties de notre programme, et chacune on peut être utilisé différemment. Toute la logique du démarrage, de l'arrêt et du nettoyage est bien encapsulée dans le hook.

Voici une démo que vous pouvez exécuter pour la voir fonctionner -

  // ...
  const toggleTurbo = () =>
    setTurbo(t => !t)

  const toggleDoubler = () =>
    setDoubler(t => !t)

  return <span>
    {counter}
    {/* start button ... */}
    <button
      onClick={toggleDoubler}  // <--
      disabled={!busy}
      children={`Doubler: ${doubler ? "ON" : "OFF"}`}
    />
    <button
      onClick={toggleTurbo}    // <--
      disabled={!busy}
      children={`Turbo: ${turbo ? "ON" : "OFF"}`}
    />
    {/* stop button ... */}
  </span>
}
function MyTimer ({ delay = 1000, auto = true, ... props })
{ const [counter, setCounter] = useState(0)

  const [doubler, setDoubler] = useState(false) // <--
  const [turbo, setTurbo] = useState(false)     // <--

  const [start, stop, busy] =
    useInterval
      ( doubler   // <-- doubler changes which f is run
          ? _ => setCounter(x => x * 2)
          : _ => setCounter(x => x + 1)
      , turbo     // <-- turbo changes delay
          ? Math.floor(delay / 2)
          : delay
      )

  // ...


faire les choses correctement

Nous voulons nous assurer que notre hook useInterval ne laisse aucune fonction chronométrée en cours d'exécution si notre minuterie est arrêtée ou après le retrait de nos composants. Testons-les dans un exemple plus rigoureux où nous pouvons ajouter / supprimer de nombreux minuteries et les démarrer / les arrêter à tout moment -

 ajouter / supprimer des minuteries

Quelques modifications fondamentales ont été nécessaires à apporter à useInterval -

function useInterval (f, delay = 1000)
{ const [busy, setBusy] = // ...
  const interval = useRef(f)

  useEffect(() => {
    interval.current = f
  }, [f])

  useEffect(() => {
    // start
    // ...
    const t =
      setInterval(_ => interval.current(), delay)

    // stop
    // ...
  }, [busy, delay])

  return // ...
}

Utilisation de useInterval dans MyTimer est intuitif. MyTimer n'est pas nécessaire pour effectuer un quelconque nettoyage de l'intervalle. Le nettoyage est automatiquement géré par useInterval -

<div id="react"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script></script>

Le composant Main ne fait rien de spécial. Il gère simplement un état de tableau de composants MyTimer . Aucun code ou nettoyage spécifique à la minuterie n'est requis -

const { useState, useEffect } = React

const append = (a = [], x = null) =>
  [ ...a, x ]
  
const remove = (a = [], x = null) =>
{ const pos = a.findIndex(q => q === x)
  if (pos < 0) return a
  return [ ...a.slice(0, pos), ...a.slice(pos + 1) ]
}

function useInterval (f, delay = 1000)
{ const [busy, setBusy] = useState(0)
  
  useEffect(() => {
    // start
    if (!busy) return
    setBusy(true)
    const t = setInterval(f, delay)
    // stop
    return () => {
      setBusy(false)
      clearInterval(t)
    }
  }, [busy, delay])
  
  return [
    _ => setBusy(true),  // start
    _ => setBusy(false), // stop
    busy                 // isBusy
  ]
}

function MyTimer ({ delay = 1000, auto = true, ... props })
{ const [counter, setCounter] =
    useState(0)
    
  const [start, stop, busy] =
    useInterval(_ => {
      console.log("tick", Date.now())
      setCounter(x => x + 1)
    }, delay)

  useEffect(() => {
    console.log("delaying...")
    setTimeout(() => {
      console.log("starting...")
      auto && start()
    }, 2000)
  }, [])
  
  return <span>
    {counter}
    <button
      onClick={start}
      disabled={busy}
      children="Start"
    />
    <button
      onClick={stop}
      disabled={!busy}
      children="Stop"
    />
  </span>
}

function Main ()
{ const [timers, setTimers] = useState([])
  
  const addTimer = () =>
    setTimers(r => append(r, <MyTimer />))
    
  const destroyTimer = c => () =>
    setTimers(r => remove(r, c))
  
  return <main>
    <p>Run in expanded mode. Open your developer console</p>
    <button
      onClick={addTimer}
      children="Add Timer"
    />
    { timers.map((c, key) =>
      <div key={key}>
        {c}
        <button
          onClick={destroyTimer(c)} 
          children="Destroy"
        />
      </div>
    )}
  </main>
}

ReactDOM.render
  ( <Main/>
  , document.getElementById("react")
  )

Développez l'extrait ci-dessous pour voir useInterval fonctionner dans votre propre navigateur. Le mode plein écran est recommandé pour cette démo -

const append = (a = [], x = null) =>
  [ ...a, x ]

const remove = (a = [], x = null) =>
{ const pos = a.findIndex(q => q === x)
  if (pos < 0) return a
  return [ ...a.slice(0, pos), ...a.slice(pos + 1) ]
}

function Main ()
{ const [timers, setTimers] = useState([])

  const addTimer = () =>
    setTimers(r => append(r, <MyTimer />))

  const destroyTimer = c => () =>
    setTimers(r => remove(r, c))

  return <main>
    <button
      onClick={addTimer}
      children="Add Timer"
    />
    { timers.map((c, key) =>
      <div key={key}>
        {c}
        <button
          onClick={destroyTimer(c)} 
          children="Destroy"
        />
      </div>
    )}
  </main>
}
function MyTimer ({ delay = 1000, auto = true, ... props })
{ const [counter, setCounter] =
    useState(0)

  const [start, stop, busy] =
    useInterval(_ => {
      console.log("tick", Date.now()) // <-- for demo
      setCounter(x => x + 1)
    }, delay)

  useEffect(() => {
    console.log("delaying...") // <-- for demo
    setTimeout(() => {
      console.log("starting...") // <-- for demo
      auto && start()
    }, 2000)
  }, [])

  return <span>
    {counter}
    <button onClick={start} disabled={busy} children="Start" />
    <button onClick={stop} disabled={!busy} children="Stop" />
  </span>
}

avancer

Imaginons un scénario useInterval encore plus complexe où la fonction chronométrée, f , et le délai peut changer -

 timers avancés

function useInterval (f, delay = 1000)
{ const [busy, setBusy] = useState(0)

  useEffect(() => {
    // start
    if (!busy) return
    setBusy(true)
    const t = setInterval(f, delay)
    // stop
    return () => {
      setBusy(false)
      clearInterval(t)
    }
  }, [busy, delay])

  return [
    _ => setBusy(true),  // start
    _ => setBusy(false), // stop
    busy                 // isBusy
  ]
}

Maintenant, nous pouvons modifier MyTimer pour ajouter l'état doubler et turbo -

<div id="react"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script></script>

Ensuite nous ajoutons un bouton double et turbo -

const { useState, useEffect } = React

const useInterval = (f, delay) =>
{ const [timer, setTimer] = useState(undefined)
  
  const start = () =>
    { if (timer) return
      console.log("started")
      setTimer(setInterval(f, delay))
    }
  
  const stop = () =>
    { if (!timer) return
      console.log("stopped", timer)
      setTimer(clearInterval(timer))
    }
    
  useEffect(() => stop, [])
  
  return [start, stop, !!timer]
}
  
const MyComp = props =>
{ const [counter, setCounter] =
    useState(0)
    
  const [start, stop, running] =
    useInterval(_ => setCounter(x => x + 1), 1000)

  useEffect(() => {
    console.log("delaying...")
    setTimeout(() => {
      console.log("starting...")
      !running && start()
    }, 2000)
  }, [])
  
  return <div>
    {counter}
    <button
      onClick={start}
      disabled={running}
      children="Start"
    />
    <button
      onClick={stop}
      disabled={!running}
      children="Stop"
    />
  </div>
};


ReactDOM.render
  ( <MyComp/>
  , document.getElementById("react")
  )

Développez l'extrait ci-dessous pour exécuter la démo avancée de la minuterie navigateur -

function MyComp (props)
{ const [counter, setCounter] =
    useState(0)

  const [start, stop, running] =
    useInterval(_ => setCounter(x => x + 1), 1000) // first try at useInterval

  useEffect(() => {
    console.log("delaying...")
    setTimeout(() => {
      console.log("starting...")
      !running && start()
    }, 2000)
  }, [])

  return <div>
    {counter}
    <button
      onClick={start}
      disabled={running}
      children="Start"
    />
    <button
      onClick={stop}
      disabled={!running}
      children="Stop"
    />
  </div>
}
// rough draft
// read on to make sure we get all the parts right
function useInterval (f, delay)
{ const [timer, setTimer] =
    useState(null)

  const start = () =>
    { if (timer) return
      console.log("started")
      setTimer(setInterval(f, delay))
    }

  const stop = () =>
    { if (!timer) return
      console.log("stopped", timer)
      setTimer(clearInterval(timer))
    }

  useEffect(() => stop, [])

  return [start, stop, timer != null]
}


0 commentaires

0
votes

Si vous essayez d'utiliser un setInterval à l'intérieur de useEffect , je pense que vous avez un peu changé l'ordre, ça devrait être comme ça

const INITIAL_DELAY = 10000
const INTERVAL_DELAY = 5000

useEffect(() => {
  let interval
  setTimeout(() => {
    const interval = setInterval(() => {
    /* do repeated stuff */
    }, INTERVAL_DELAY)
  }, INITIAL_DELAY - INTERVAL_DELAY)
  return () => clearInterval(interval)
})


0 commentaires