React setInterval in useEffect with setTimeout delay

北城以北 提交于 2021-02-08 07:32:19

问题


I want to run an interval with a delay for the first time it fires. How can I do this with useEffect? Because of the syntax I've found it difficult to achieve what I want to do

The interval function

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

The delay function

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);
  }, []);

Sure it is achievable somehow...


回答1:


getting started

Consider detangling the concerns of your component and writing small pieces. Here we have a useInterval custom hook which strictly defines the setInterval portion of the program. I added some console.log lines so we can observe the effects -

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

Now when we write MyComp we can handle the setTimeout portion of the program -

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

Now we can useInterval in various parts of our program, and each one can be used differently. All the logic for the start, stop and cleanup is nicely encapsulated in the hook.

Here's a demo you can run to see it working -

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")
  )
<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>

getting it right

We want to make sure our useInterval hook doesn't leave any timed functions running if our timer are stopped or after our components are removed. Let's test them out in a more rigorous example where we can add/remove many timers and start/stop them at any time -

A few fundamental changes were necessary to make to useInterval -

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

Using useInterval in MyTimer component is intuitive. MyTimer is not required to do any sort of cleanup of the interval. Cleanup is automatically handled by useInterval -

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

The Main component doesn't do anything special. It just manages an array state of MyTimer components. No timer-specific code or clean up is required -

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

Expand the snippet below to see useInterval working in your own browser. Fullscreen mode is recommended for this demo -

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")
  )
<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>

getting advanced

Let's imagine an even more complex useInterval scenario where the timed function, f, and the delay can change -

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 // ...
}

Now we can edit MyTimer to add the doubler and turbo state -

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
      )

  // ...

Then we add a double and turbo button -

  // ...
  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>
}

Expand the snippet below to run the advanced timer demo in your own browser -

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")
  )
<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>



回答2:


I think What you're trying to do is this:

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



回答3:


If you are trying to use a setInterval inside useEffect, I think you switched up the order a bit, it should be like this

const INTERVAL_DELAY = 1000
useEffect(() => {
  const interval = setInterval(() => {
  /* do repeated stuff */
  }, INTERVAL_DELAY)
  return () => clearInterval(interval)
})

The interval will start after a delay, so if you want an interval delay of X seconds to start after Y seconds, you have to actually use a delay in setTimeout as Y - X

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




回答4:


Is this what you want to achieve? the empty array on useeffect tells it will run this code once the element is rendered

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


来源:https://stackoverflow.com/questions/61971791/react-setinterval-in-useeffect-with-settimeout-delay

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!