Trạng thái không cập nhật khi sử dụng hook trạng thái React trong setInterval


109

Tôi đang thử React Hooks mới và có thành phần Đồng hồ với bộ đếm được cho là sẽ tăng lên mỗi giây. Tuy nhiên, giá trị không tăng quá một.

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>

Câu trả lời:


158

Lý do là bởi vì lệnh gọi lại được truyền vào setInterval's close chỉ truy cập vào timebiến trong lần hiển thị đầu tiên, nó không có quyền truy cập vào timegiá trị mới trong lần hiển thị tiếp theo vì biến useEffect()không được gọi lần thứ hai.

timeluôn có giá trị 0 trong lệnh setIntervalgọi lại.

Giống như setStatebạn đã quen thuộc, các hook trạng thái có hai dạng: một dạng ở đó nó ở trạng thái cập nhật và dạng gọi lại mà trạng thái hiện tại được chuyển vào. Bạn nên sử dụng biểu mẫu thứ hai và đọc giá trị trạng thái mới nhất trong lệnh setStategọi lại tới đảm bảo rằng bạn có giá trị trạng thái mới nhất trước khi tăng nó.

Phần thưởng: Các cách tiếp cận thay thế

Dan Abramov, đi sâu vào chủ đề về việc sử dụng setIntervalhook trong bài đăng trên blog của mình và cung cấp các cách thay thế xung quanh vấn đề này. Rất khuyên bạn nên đọc nó!

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
Đây là thông tin rất hữu ích. Cảm ơn bạn Yangshun!
Tholle

8
Bạn & # 39; tái hoan nghênh, tôi đã dành một thời gian nhìn chằm chằm vào mã duy nhất để tìm hiểu tôi đã thực hiện một lỗi rất cơ bản (nhưng có lẽ không rõ ràng?) Và muốn chia sẻ điều này với cộng đồng
Yangshun Tây

3
VẬY ở mức tốt nhất 👍
Patrick Hund

42
Haha Tôi đến đây để cắm bài viết của mình và nó đã có trong câu trả lời!
Dan Abramov

2
Bài đăng trên blog tuyệt vời. Mở rộng tầm mắt.
benjaminadk

19

useEffect hàm chỉ được đánh giá một lần trên giá đỡ thành phần khi danh sách đầu vào trống được cung cấp.

Một giải pháp thay thế setIntervallà đặt khoảng thời gian mới với setTimeoutmỗi lần trạng thái được cập nhật:

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

Tác động hiệu suất của setTimeoutlà không đáng kể và thường có thể được bỏ qua. Trừ khi thành phần nhạy cảm với thời gian đến điểm mà thời gian chờ mới được thiết lập gây ra các tác dụng không mong muốn, cả hai phương pháp setIntervalsetTimeoutphương pháp đều được chấp nhận.


12

Như những người khác đã chỉ ra, vấn đề là nó useStatechỉ được gọi một lần (vì nó deps = []để thiết lập khoảng thời gian:

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

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

Sau đó, mỗi khi tích setIntervaltắc, nó sẽ thực sự gọi setTime(time + 1), nhưng timesẽ luôn giữ giá trị mà nó có ban đầu khi lệnh setIntervalgọi lại (đóng) được xác định.

Bạn có thể sử dụng dạng thay thế của useStatesetter và cung cấp một lệnh gọi lại thay vì giá trị thực mà bạn muốn đặt (giống như với setState):

setTime(prevTime => prevTime + 1);

Nhưng tôi khuyến khích bạn tạo useIntervalhook của riêng mình để bạn có thể KHÔ và đơn giản hóa mã của mình bằng cách sử dụng một cách setInterval khai báo , như Dan Abramov đề xuất ở đây trong Tạo setInterval Decl Compare với 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>

Ngoài việc tạo ra mã đơn giản hơn và rõ ràng hơn, điều này cho phép bạn tạm dừng (và xóa) khoảng thời gian tự động bằng cách vượt qua delay = nullvà cũng trả về ID khoảng thời gian, trong trường hợp bạn muốn tự hủy nó theo cách thủ công (điều này không được đề cập trong các bài viết của Dan).

Trên thực tế, điều này cũng có thể được cải thiện để nó không khởi động lại delaykhi không bị tạm dừng, nhưng tôi đoán đối với hầu hết các trường hợp sử dụng, điều này là đủ tốt.

Nếu bạn đang tìm kiếm câu trả lời tương tự setTimeoutthay vì tìm kiếm setInterval, hãy xem phần này: https://stackoverflow.com/a/59274757/3723993 .

Bạn cũng có thể tìm thấy phiên bản tường thuật của setTimeoutsetInterval, useTimeoutuseInterval, cộng với một phong tục useThrottledCallbackmóc viết bằng bản sao vào https://gist.github.com/Danziger/336e75b6675223ad805a88c2dfdcfd4a .


5

Một giải pháp thay thế sẽ được sử dụng useReducer, vì nó sẽ luôn được chuyển sang trạng thái hiện tại.

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>


Tại sao useEffectở đây lại được gọi nhiều lần để cập nhật thời gian, trong khi mảng phụ thuộc trống, có nghĩa là chỉ useEffectnên gọi lần đầu tiên thành phần / ứng dụng hiển thị?
BlackMath

1
@BlackMath Hàm bên trong useEffectchỉ được gọi một lần, khi thành phần thực sự hiển thị lần đầu tiên. Nhưng bên trong nó, có một setIntervalphụ trách thay đổi thời gian một cách thường xuyên. Tôi đề nghị bạn đọc một chút về setInterval, mọi thứ sẽ rõ ràng hơn sau đó! developer.mozilla.org/en-US/docs/Web/API/…
Bear-Foot

3

Làm như dưới đây nó hoạt động tốt.

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

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

//call increment function
increment(count);

Cuộc count => count + 1gọi lại là những gì đã làm việc cho tôi, cảm ơn!
Nickofthyme

1

Các giải pháp này không phù hợp với tôi bởi vì tôi cần lấy biến và làm một số công việc chứ không chỉ cập nhật nó.

Tôi nhận được một giải pháp để có được giá trị cập nhật của hook với một lời hứa

Ví dụ:

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

Với điều này, tôi có thể lấy giá trị bên trong hàm setInterval như thế này

let dateFrom = await getCurrentHackValue(setSelectedDateFrom);

0

Yêu cầu React re-render khi thời gian thay đổi. chọn không tham gia

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
Vấn đề với điều này là bộ đếm thời gian sẽ bị xóa và đặt lại sau mỗi lần countthay đổi.
sumail

Và bởi vì điều đó setTimeout()được ưa thích như được chỉ ra bởi Estus
Chayim Friedman,
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.