Status wird nicht aktualisiert, wenn der React-Status-Hook in setInterval verwendet wird

95

Ich probiere die neuen React Hooks aus und habe eine Clock-Komponente mit einem Zähler, der sich jede Sekunde erhöhen soll. Der Wert steigt jedoch nicht über eins an.

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/[email protected]/umd/react.development.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>

<div id="app"></div>

Yangshun Tay
quelle

Antworten:

141

Der Grund dafür ist, dass der in setInterval'Closure übergebene Rückruf nur auf die timeVariable im ersten Rendering zugreift und keinen Zugriff auf den neuen timeWert im nachfolgenden Rendering hat, da der useEffect()beim zweiten Rendering nicht aufgerufen wird.

timehat im setIntervalRückruf immer den Wert 0 .

Wie setStateSie es kennen, haben Status-Hooks zwei Formen: eine, in der der aktualisierte Status angezeigt wird, und die Rückrufform, in der der aktuelle Status übergeben wird. Sie sollten die zweite Form verwenden und den neuesten Statuswert innerhalb des setStateRückrufs an lesen Stellen Sie sicher, dass Sie den neuesten Statuswert haben, bevor Sie ihn erhöhen.

Bonus: Alternative Ansätze

Dan Abramov geht in setIntervalseinem Blog-Beitrag ausführlich auf das Thema der Verwendung mit Hooks ein und bietet alternative Möglichkeiten, um dieses Problem zu umgehen. Sehr zu empfehlen, es zu lesen!

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/[email protected]/umd/react.development.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>

<div id="app"></div>

Yangshun Tay
quelle
3
Dies sind sehr nützliche Informationen. Vielen Dank, Yangshun!
Tholle
8
Gern geschehen
3
SO von seiner besten Seite
Patrick Hund
38
Haha, ich bin hierher gekommen, um meinen Beitrag einzustecken und er ist bereits in der Antwort!
Dan Abramov
2
Toller Blog-Beitrag. Augenöffnung.
benjaminadk
19

useEffect Die Funktion wird beim Einhängen von Komponenten nur einmal ausgewertet, wenn eine leere Eingabeliste bereitgestellt wird.

Eine Alternative dazu setIntervalbesteht darin, bei setTimeoutjeder Aktualisierung des Status ein neues Intervall festzulegen:

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

Die Auswirkungen auf die Leistung von setTimeoutsind unbedeutend und können im Allgemeinen ignoriert werden. Sofern die Komponente nicht zeitempfindlich gegenüber dem Punkt ist, an dem neu eingestellte Zeitüberschreitungen unerwünschte Auswirkungen haben, sind sowohl setIntervalals auch setTimeoutAnsätze akzeptabel.

Estus Flask
quelle
10

Wie andere bereits betont haben, besteht das Problem darin, dass useStatenur einmal aufgerufen wird ( deps = []um das Intervall festzulegen:

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

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

Jedes Mal, wenn ein setIntervalHäkchen gesetzt ist, wird tatsächlich aufgerufen setTime(time + 1), es timewird jedoch immer der Wert beibehalten, den es ursprünglich hatte, als der setIntervalRückruf (Abschluss) definiert wurde.

Sie können die alternative Form des useStateSetters verwenden und einen Rückruf anstelle des tatsächlichen Werts bereitstellen, den Sie einstellen möchten (genau wie bei setState):

setTime(prevTime => prevTime + 1);

Aber ich würde Sie ermutigen, Ihren eigenen useIntervalHook zu erstellen, damit Sie Ihren Code durch setInterval deklarative Verwendung TROCKNEN und vereinfachen können , wie Dan Abramov hier in Making setInterval Declarative with React Hooks vorschlägt :

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/[email protected]/umd/react.development.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>

<div id="app"></div>

Abgesehen davon, dass Sie einfacheren und saubereren Code erstellen, können Sie das Intervall automatisch anhalten (und löschen), indem Sie es übergeben, delay = nullund die Intervall-ID zurückgeben, falls Sie es selbst manuell abbrechen möchten (dies wird in Dans Beiträgen nicht behandelt).

Eigentlich könnte dies auch verbessert werden, damit es nicht neu delaygestartet wird, wenn es nicht angehalten ist, aber ich denke, für die meisten Anwendungsfälle ist dies gut genug.

Wenn Sie nach einer ähnlichen Antwort suchen setTimeoutund nicht nach setInterval, lesen Sie diese: https://stackoverflow.com/a/59274757/3723993 .

Sie können auch deklarative Version finden setTimeoutund setInterval, useTimeoutund useInterval, sowie ein benutzerdefinierter useThrottledCallbackin in Typoskript geschrieben Haken https://gist.github.com/Danziger/336e75b6675223ad805a88c2dfdcfd4a .

Danziger
quelle
5

Eine alternative Lösung wäre die Verwendung useReducer, da immer der aktuelle Status übergeben wird.

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/[email protected]/umd/react.development.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>

<div id="app"></div>

Bärenfuß
quelle
3

Gehen Sie wie folgt vor, es funktioniert gut.

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

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

//call increment function
increment(count);
Vidya
quelle
Der count => count + 1Rückruf hat bei mir funktioniert, danke!
Nickofthyme
0

Sagen Sie React React erneut rendern, wenn sich die Zeit ändert. Optischer Ausgang

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/[email protected]/umd/react.development.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>

<div id="app"></div>

sumail
quelle
2
Das Problem dabei ist, dass der Timer nach jeder countÄnderung gelöscht und zurückgesetzt wird .
Sumail
Und weil dies setTimeout()bevorzugt wird, wie von Estus
Chayim Friedman
0

Diese Lösungen funktionieren bei mir nicht, weil ich die Variable abrufen und einige Dinge tun muss, um sie nicht nur zu aktualisieren.

Ich bekomme eine Problemumgehung, um den aktualisierten Wert des Hooks mit einem Versprechen zu erhalten

Z.B:

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

Damit kann ich den Wert innerhalb der setInterval-Funktion wie folgt erhalten

let dateFrom = await getCurrentHackValue(setSelectedDateFrom);
Jhonattan Oliveira
quelle
0

Wenn jemand eine Warteschlange verwalten muss

Nehmen wir an, Sie benachrichtigen Benachrichtigungen mit einem Intervall von 3 Sekunden (First In, First Out) und können jederzeit neue Nachrichten senden.

Beispiel für Codesandbox.

import React, {useState, useRef, useEffect} from "react";
import ReactDOM from "react-dom";

import "./styles.css";

let x = 1 // for testing
const fadeTime = 3000 // 3 sec 

function App() {
  // our messages array in what we can push at any time
  const [queue, setQueue] = useState([]) 

  // our shiftTimer that will change every 3 sec if array have items
  const [shiftTimer, setShiftTimer] = useState(Date.now())

  // reference to timer
  const shiftTimerRef = useRef(null)

  // here we start timer if it was mot started yet
  useEffect(() => {
    if (shiftTimerRef.current === null && queue.length != 0) {
      startTimer()
    }
  }, [queue])

  // here we will shift first message out of array (as it was already seen)
  useEffect(() => {
    shiftTimerRef.current = null
    popupShift()
  }, [shiftTimer])

  function startTimer() {
    shiftTimerRef.current = setTimeout(() => {
      setShiftTimer(Date.now)
    }, fadeTime )
  }

  function startTimer() {
    shiftTimerRef.current = setTimeout(() => setShiftTimer(Date.now), fadeTime )
  }

  function popupPush(newPopup) {
    let newQueue = JSON.parse(JSON.stringify(queue))
    newQueue.push(newPopup)
    setQueue(newQueue)
  }

  function popupShift() {
    let newQueue = JSON.parse(JSON.stringify(queue))
    newQueue.shift()
    setQueue(newQueue)
  }

  return (
    <div>
      <button onClick={() => popupPush({ message: x++ })}>Push new message</button>
      <div>{JSON.stringify(queue)}</div>
    </div>
  )
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
ZiiMakc
quelle