setInterval内でReact状態フックを使用すると状態が更新されない


109

私は新しいReactHooksを試していますが、毎秒増加するはずのカウンターを備えたClockコンポーネントがあります。ただし、値は1を超えて増加することはありません。

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>

回答:


158

その理由は、setIntervalのクロージャに渡されたコールバックtimeは最初のレンダリングで変数にのみアクセスし、2回目に呼び出されないtimeため、後続のレンダリングで新しい値にアクセスできないためuseEffect()です。

time 内で常に値0を持ちます setIntervalコールバックます。

同じようにsetState、あなたが精通している、状態のフックには2つの形式があります。それが更新された状態になります1、および現在の状態が渡されたコールバックフォームをあなたが二番目の形式を使用し、内部に最新の状態値をお読みください。setStateへのコールバックインクリメントする前に、最新の状態値があることを確認してください。

ボーナス:代替アプローチ

ダン・アブラモフは、setInterval彼のブログ投稿でフックの使用に関するトピックを深く掘り下げ、この問題を回避する別の方法を提供しています。それを読むことを強くお勧めします!

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
これは非常に役立つ情報です。ヤンシュンありがとう!
Tholle 2018年

8
どういたしまして、私はコードをじっと見つめて、非常に基本的な(しかしおそらく自明ではない?)エラーを犯したことを知り、これをコミュニティと共有したいと思いました
Yangshun Tay

3
SO最高の状態で👍–
Patrick Hund

42
ハハ私は私の投稿をプラグインするためにここに来ました、そしてそれはすでに答えにあります!
ダン・アブラモフ

2
素晴らしいブログ投稿。目が開く。
benjaminadk

19

useEffect 空の入力リストが提供されている場合、関数はコンポーネントマウントで1回だけ評価されます。

別の方法setIntervalsetTimeout、状態が更新されるたびに新しい間隔を設定することです。

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

のパフォーマンスへの影響setTimeoutは重要ではなく、通常は無視できます。コンポーネントが、新しく設定されたタイムアウトが望ましくない影響を引き起こすポイントに時間に敏感でない限り、setIntervalsetTimeoutアプローチの両方が許容されます。


12

他の人が指摘しているように、問題はuseState一度だけ呼び出されることです(deps = []間隔を設定するために:

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

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

その後、setIntervalティックするたびに、実際にはを呼び出しますがsetTime(time + 1)time常に最初に持っていた値を保持します。setIntervalコールバック(クロージャ)が定義された。

useStateのセッターの代替形式を使用して、設定する実際の値ではなくコールバックを提供できます(と同様setState)。

setTime(prevTime => prevTime + 1);

ただし、DanAbramovがReactHooksを使用したsetInterval宣言型の作成で提案しているように、宣言型useIntervalを使用してコードをDRYおよび単純化できるように、独自のフックを作成することをお勧めします。setInterval

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>

よりシンプルでクリーンなコードを生成する以外に、これにより、渡すことによって間隔を自動的に一時停止(およびクリア)でき、delay = null手動でキャンセルしたい場合に備えて間隔IDを返すことができます(Danの投稿ではカバーされていません)。

実際には、これを改善して、再起動しないようにすることもできます。 delay、一時停止ときに、ほとんどのユースケースではこれで十分だと思います。

setTimeoutではなく、同様の回答をお探しの場合はsetIntervalhttps//stackoverflow.com/a/59274757/3723993をご覧ください

また、https: //gist.github.com/Danziger/336e75b6675223ad805a88c2dfdcfd4aで、宣言型バージョンのsetTimeoutと、、およびTypeScriptで記述されたカスタムフックを見つけることができますsetIntervaluseTimeoutuseIntervaluseThrottledCallback


5

別の解決策はuseReducer、常に現在の状態が渡されるため、を使用することです。

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>


useEffect依存関係の配列が空のときに、時間を更新するためにここが複数回呼び出されるのはなぜですか。つまりuseEffect、コンポーネント/アプリが最初にレンダリングされるときにのみ呼び出される必要があります。
BlackMath

1
@BlackMath内部の関数useEffectは、コンポーネントが実際に最初にレンダリングされるときに1回だけ呼び出されます。しかし、その中setIntervalには定期的に時刻を変更する担当のがあります。私はあなたがについて少し読むことをお勧めしますsetInterval、その後物事はより明確になるはずです!developer.mozilla.org/en-US/docs/Web/API/...
ベア・フット

3

以下のようにしてください。

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

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

//call increment function
increment(count);

count => count + 1コールバックは、私のために感謝を働いたものでした!
Nickofthyme

1

変数を取得して更新するだけでなく、いくつかのことを行う必要があるため、このソリューションは機能しません。

フックの更新された値を約束で取得するための回避策を取得します

例えば:

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

これで私はこのようにsetInterval関数内の値を取得できます

let dateFrom = await getCurrentHackValue(setSelectedDateFrom);

0

時間が変更されたときにReactの再レンダリングを指示します。身を引く

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
これに伴う問題は、count変更のたびにタイマーがクリアされてリセットされることです。
メール

そしてsetTimeout()、Estusが指摘したようにそう好まれているので
Chayim Friedman
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.