SetInterval içinde React durum kancası kullanılırken durum güncellenmiyor


110

Yeni React Hook'larını deniyorum ve her saniye artması gereken bir sayacı olan bir Clock bileşenim var. Ancak değer birin ötesine geçmez.

function Clock() {
  const [time, setTime] = React.useState(0);
  React.useEffect(() => {
    const timer = window.setInterval(() => {
      setTime(time + 1);
    }, 1000);
    return () => {
      window.clearInterval(timer);
    };
  }, []);

  return (
    <div>Seconds: {time}</div>
  );
}

ReactDOM.render(<Clock />, 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>

Yanıtlar:


159

Bunun nedeni, setIntervalkapanışına aktarılan geri çağrının yalnızca timeilk işlemedeki değişkene erişmesi time, sonraki işlemede yeni değere erişimi olmaması, çünkü useEffect()ikinci seferde çağrılmamasıdır.

timesetIntervalgeri arama içinde her zaman 0 değerine sahiptir .

Gibi setStatesen aşina, devlet kancalar için iki alternatif: güncellenmesi durumunda birdir ve mevcut durum içinde geçirilen geri arama formunu İkinci formu kullanın ve içinde en son durum değeri okumalısınız. setStateGeri arama Arttırmadan önce en son durum değerine sahip olduğunuzdan emin olun.

Bonus: Alternatif Yaklaşımlar

Dan Abramov, blog gönderisindesetInterval kancalarla kullanım konusunu derinlemesine inceliyor ve bu konuyla ilgili alternatif yollar sunuyor. Okumanızı şiddetle tavsiye ederiz!

function Clock() {
  const [time, setTime] = React.useState(0);
  React.useEffect(() => {
    const timer = window.setInterval(() => {
      setTime(prevTime => prevTime + 1); // <-- Change this line!
    }, 1000);
    return () => {
      window.clearInterval(timer);
    };
  }, []);

  return (
    <div>Seconds: {time}</div>
  );
}

ReactDOM.render(<Clock />, 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>


3
Bu çok faydalı bir bilgidir. Teşekkürler Yangshun!
Tholle

8
Hoş geldiniz, sadece çok basit (ama muhtemelen açık olmayan?) Bir hata
yaptığımı

3
SO en iyi haliyle 👍
Patrick Hund

42
Haha buraya yazımı takmaya geldim ve zaten cevapta!
Dan Abramov

2
Harika bir blog yazısı. Göz açma.
benjaminadk

19

useEffect işlev, boş giriş listesi sağlandığında bileşen bağında yalnızca bir kez değerlendirilir.

Bunun bir alternatifi , durum her güncellendiğinde setIntervalyeni aralık ayarlamaktır setTimeout:

  const [time, setTime] = React.useState(0);
  React.useEffect(() => {
    const timer = setTimeout(() => {
      setTime(time + 1);
    }, 1000);
    return () => {
      clearTimeout(timer);
    };
  }, [time]);

Performans etkisi setTimeoutönemsizdir ve genellikle göz ardı edilebilir. Bileşen sürece zamana duyarlı yeni ayarlanmış zaman aşımı istenmeyen etkilere neden noktasına etmekte, burada iki setIntervalve setTimeoutyaklaşım kabul edilebilir.


13

Diğerlerinin de belirttiği gibi, sorun useStateyalnızca bir kez çağrılmasıdır (çünkü deps = []aralığı ayarlamak için:

React.useEffect(() => {
    const timer = window.setInterval(() => {
        setTime(time + 1);
    }, 1000);

    return () => window.clearInterval(timer);
}, []);

Daha sonra, her setIntervaltıklandığında, aslında çağıracak setTime(time + 1), ancak timeher zaman setIntervalgeri arama (kapanış) tanımlandığında başlangıçta sahip olduğu değeri tutacaktır .

useStateAyarlayıcısının alternatif biçimini kullanabilir ve ayarlamak istediğiniz gerçek değer yerine bir geri arama sağlayabilirsiniz (aynen olduğu gibi setState):

setTime(prevTime => prevTime + 1);

Ancak Dan Abramov'un burada React Hooks ile setInterval Bildirimi Yapma bölümünde önerdiği gibi, useIntervalkodunuzu setInterval bildirime dayalı olarak KURU ve basitleştirmek için kendi kancanızı oluşturmanızı tavsiye ederim :

function useInterval(callback, delay) {
  const intervalRef = React.useRef();
  const callbackRef = React.useRef(callback);

  // Remember the latest callback:
  //
  // Without this, if you change the callback, when setInterval ticks again, it
  // will still call your old callback.
  //
  // If you add `callback` to useEffect's deps, it will work fine but the
  // interval will be reset.

  React.useEffect(() => {
    callbackRef.current = callback;
  }, [callback]);

  // Set up the interval:

  React.useEffect(() => {
    if (typeof delay === 'number') {
      intervalRef.current = window.setInterval(() => callbackRef.current(), delay);

      // Clear interval if the components is unmounted or the delay changes:
      return () => window.clearInterval(intervalRef.current);
    }
  }, [delay]);
  
  // Returns a ref to the interval ID in case you want to clear it manually:
  return intervalRef;
}


const Clock = () => {
  const [time, setTime] = React.useState(0);
  const [isPaused, setPaused] = React.useState(false);
        
  const intervalRef = useInterval(() => {
    if (time < 10) {
      setTime(time + 1);
    } else {
      window.clearInterval(intervalRef.current);
    }
  }, isPaused ? null : 1000);

  return (<React.Fragment>
    <button onClick={ () => setPaused(prevIsPaused => !prevIsPaused) } disabled={ time === 10 }>
        { isPaused ? 'RESUME ⏳' : 'PAUSE 🚧' }
    </button>

    <p>{ time.toString().padStart(2, '0') }/10 sec.</p>
    <p>setInterval { time === 10 ? 'stopped.' : 'running...' }</p>
  </React.Fragment>);
}

ReactDOM.render(<Clock />, document.querySelector('#app'));
body,
button {
  font-family: monospace;
}

body, p {
  margin: 0;
}

p + p {
  margin-top: 8px;
}

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

Daha basit ve daha temiz bir kod üretmenin yanı sıra, bu, aralığı otomatik olarak geçerek duraklatmanıza (ve temizlemenize) olanak tanır delay = nullve ayrıca kendiniz manuel olarak iptal etmek istemeniz durumunda aralık kimliğini de döndürür (bu, Dan'in yayınlarında ele alınmamıştır).

Aslında, bu delay, duraklatılmadığında yeniden başlamaması için de geliştirilebilir, ancak çoğu kullanım durumu için bu yeterince iyi sanırım.

Eğer için de benzer bir cevap arıyorsanız setTimeoutziyade setInterval: Bu out https://stackoverflow.com/a/59274757/3723993 .

Ayrıca bildirimsel sürümünü bulabilirsiniz setTimeoutve setInterval, useTimeoutve useInterval, ve bir de özel useThrottledCallbackde daktilo yazılı kanca https://gist.github.com/Danziger/336e75b6675223ad805a88c2dfdcfd4a .


5

Her useReducerzaman mevcut durumdan geçeceği için alternatif bir çözüm kullanmak olacaktır.

function Clock() {
  const [time, dispatch] = React.useReducer((state = 0, action) => {
    if (action.type === 'add') return state + 1
    return state
  });
  React.useEffect(() => {
    const timer = window.setInterval(() => {
      dispatch({ type: 'add' });
    }, 1000);
    return () => {
      window.clearInterval(timer);
    };
  }, []);

  return (
    <div>Seconds: {time}</div>
  );
}

ReactDOM.render(<Clock />, 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>


useEffectBağımlılıklar dizisi boşken burada neden birden çok kez çağrılıyor, bu da useEffectyalnızca bileşenin / uygulamanın oluşturduğu ilk sefer çağrılması gerektiği anlamına geliyor ?
BlackMath

1
@BlackMath İçerideki işlev useEffect, bileşen gerçekten ilk oluşturduğunda yalnızca bir kez çağrılır. Ama onun içinde setInterval, zamanı düzenli olarak değiştirmekten sorumlu bir kişi var. Biraz okumanızı öneririm setInterval, bundan sonra işler daha net olmalı! developer.mozilla.org/en-US/docs/Web/API/…
Bear-Foot

3

Aşağıdaki gibi yapın, iyi çalışıyor.

const [count , setCount] = useState(0);

async function increment(count,value) {
    await setCount(count => count + 1);
  }

//call increment function
increment(count);

Geri count => count + 1arama benim için işe yarayan şeydi, teşekkürler!
Nickofthyme

1

Bu çözümler benim için işe yaramıyor çünkü değişkeni almam ve sadece güncellemem gereken bazı şeyler yapmam gerekiyor.

Bir sözle kancanın güncellenmiş değerini elde etmek için bir geçici çözüm buldum

Örneğin:

async function getCurrentHookValue(setHookFunction) {
  return new Promise((resolve) => {
    setHookFunction(prev => {
      resolve(prev)
      return prev;
    })
  })
}

Bununla setInterval işlevi içindeki değeri şu şekilde elde edebilirim

let dateFrom = await getCurrentHackValue(setSelectedDateFrom);

0

React'e zaman değiştiğinde yeniden render almasını söyleyin. vazgeçmek

function Clock() {
  const [time, setTime] = React.useState(0);
  React.useEffect(() => {
    const timer = window.setInterval(() => {
      setTime(time + 1);
    }, 1000);
    return () => {
      window.clearInterval(timer);
    };
  }, [time]);

  return (
    <div>Seconds: {time}</div>
  );
}

ReactDOM.render(<Clock />, 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>


2
Bununla ilgili sorun, zamanlayıcının her countdeğişiklikten sonra temizlenip sıfırlanacak olmasıdır .
2019, 06:51

Ve setTimeout()Estus tarafından belirtildiği gibi tercih edildiği için
Friedman
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.