สถานะไม่อัพเดตเมื่อใช้ React state hook ภายใน setInterval


120

ฉันกำลังทดลองใช้React Hooksใหม่และมีส่วนประกอบนาฬิกาพร้อมตัวนับซึ่งควรจะเพิ่มขึ้นทุกวินาที อย่างไรก็ตามมูลค่าไม่ได้เพิ่มขึ้นเกินหนึ่ง

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>

คำตอบ:


170

สาเหตุเป็นเพราะการเรียกกลับที่ส่งเข้าสู่setIntervalการปิดจะเข้าถึงtimeตัวแปรในการเรนเดอร์ครั้งแรกเท่านั้นจึงไม่สามารถเข้าถึงtimeค่าใหม่ในการเรนเดอร์ที่ตามมาเนื่องจากuseEffect()ไม่ได้เรียกใช้ในครั้งที่สอง

timeมักจะมีค่าเป็น 0 ภายในการsetIntervalเรียกกลับ

เช่นเดียวกับที่setStateคุณคุ้นเคย state hooks มีสองรูปแบบคือรูปแบบที่ใช้ในสถานะที่อัปเดตและรูปแบบการเรียกกลับที่ส่งผ่านสถานะปัจจุบันคุณควรใช้แบบฟอร์มที่สองและอ่านค่าสถานะล่าสุดภายในการsetStateเรียกกลับไปที่ ตรวจสอบให้แน่ใจว่าคุณมีค่าสถานะล่าสุดก่อนที่จะเพิ่มขึ้น

โบนัส: แนวทางทางเลือก

Dan Abramov เจาะลึกหัวข้อเกี่ยวกับการใช้setIntervalกับ hooks ในโพสต์บล็อกของเขาและให้ทางเลือกอื่น ๆ เกี่ยวกับปัญหานี้ ขอแนะนำให้อ่าน!

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

8
ยินดีต้อนรับคุณฉันใช้เวลาสักพักหนึ่งในการจ้องดูรหัสเพื่อพบว่าฉันทำข้อผิดพลาดพื้นฐาน (แต่อาจไม่ชัดเจน?) และต้องการแบ่งปันสิ่งนี้กับชุมชน
Yangshun Tay

3
ที่ดีที่สุด👍
Patrick Hund

46
ฮ่าฮ่าฉันมาที่นี่เพื่อเสียบโพสต์ของฉันและมันก็เป็นคำตอบแล้ว!
Dan Abramov

2
โพสต์บล็อกที่ยอดเยี่ยม เปิดตา
benjaminadk

19

useEffect ฟังก์ชันจะได้รับการประเมินเพียงครั้งเดียวบนการติดตั้งส่วนประกอบเมื่อมีรายการอินพุตว่าง

อีกทางเลือกหนึ่งsetIntervalคือกำหนดช่วงเวลาใหม่setTimeoutในแต่ละครั้งที่มีการอัปเดตสถานะ:

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

ผลกระทบด้านประสิทธิภาพsetTimeoutไม่มีนัยสำคัญและสามารถละเลยได้โดยทั่วไป เว้นแต่ว่าองค์ประกอบนั้นจะไวต่อเวลาจนถึงจุดที่การหมดเวลาที่ตั้งค่าใหม่ทำให้เกิดผลกระทบที่ไม่พึงปรารถนาทั้งสองวิธีsetIntervalและsetTimeoutแนวทางเป็นที่ยอมรับ


16

ตามที่คนอื่น ๆ ได้ชี้ให้เห็นปัญหาคือ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);

แต่ฉันขอแนะนำให้คุณสร้างuseIntervalhook ของคุณเองเพื่อให้คุณสามารถ DRY และทำให้โค้ดของคุณง่ายขึ้นโดยใช้อย่างsetInterval เปิดเผยตามที่ Dan Abramov แนะนำที่นี่ในMaking setInterval Declarative with React Hooks :

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มากกว่าsetInterval, ตรวจสอบนี้ออก: https://stackoverflow.com/a/59274757/3723993

นอกจากนี้คุณยังสามารถหารุ่นที่เปิดเผยของsetTimeoutและsetInterval, useTimeoutและuseIntervalบวกที่กำหนดเองuseThrottledCallbackเบ็ดเขียนใน typescript ในhttps://gist.github.com/Danziger/336e75b6675223ad805a88c2dfdcfd4a


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ถูกเรียกใช้เพียงครั้งเดียวเมื่อคอมโพเนนต์แสดงผลเป็นครั้งแรก แต่ข้างในนั้นมีsetIntervalหน้าที่เปลี่ยนเวลาอยู่เป็นประจำ ฉันขอแนะนำให้คุณอ่านเกี่ยวกับsetIntervalสิ่งต่างๆควรจะชัดเจนขึ้นหลังจากนั้น! developer.mozilla.org/en-US/docs/Web/API/…
Bear-Foot

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 re-render เมื่อเวลาเปลี่ยนไป เลือกออก

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การเปลี่ยนแปลง
sumail

และเนื่องจากsetTimeout()เป็นที่ต้องการตามที่ Estus
Chayim Friedman
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.