setInterval 내에서 React 상태 후크를 사용할 때 상태가 업데이트되지 않음


109

저는 새로운 React Hooks를 시험 해보고 있으며 매초마다 증가해야하는 카운터가있는 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이 첫 번째 렌더링 의 변수 에만 액세스 하고 두 번째로 호출되지 time않기 때문에 후속 렌더링 의 새 값에 액세스 할 수 없기 때문 useEffect()입니다.

timesetInterval콜백 내에서 항상 값이 0 입니다.

setState익숙한 것처럼 상태 후크에는 두 가지 형식이 있습니다. 하나는 업데이트 된 상태를 취하는 곳이고 다른 하나는 현재 상태가 전달되는 콜백 양식입니다. 두 번째 양식을 사용하고 setState콜백 내의 최신 상태 값을 읽어야합니다. 증분하기 전에 최신 상태 값이 있는지 확인하십시오.

보너스 : 대체 접근 방식

Dan Abramov는 블로그 게시물setInterval 에서 with 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
천만에요. 제가 아주 기본적인 (하지만 분명하지 않은) 오류를
범했다는

3
SO 👍 최상의
패트릭 개자리

42
하하 나는 내 게시물을 연결하기 위해 여기에 왔으며 이미 답변에 있습니다!
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은 미미하며 일반적으로 무시할 수 있습니다. 구성 요소가 새로 설정된 시간 제한으로 인해 원하지 않는 결과가 발생하는 시점에 시간에 민감하지 않으면 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의 setter 의 대체 형식을 사용하고 설정하려는 실제 값 대신 콜백을 제공 할 수 있습니다 (와 마찬가지로 setState).

setTime(prevTime => prevTime + 1);

그러나 Dan Abramov가 Make setInterval Declarative with React Hooks 에서 제안한 것처럼 선언적useInterval 으로 사용하여 코드를 건조하고 단순화 할 수 있도록 자체 후크 를 생성하는 것이 좋습니다 .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, 수동으로 취소하려는 경우 (Dan의 게시물에서 다루지 않음) 전달하여 간격을 자동으로 일시 중지 (및 삭제) 하고 간격 ID를 반환 할 수 있습니다.

실제로 이것은 delay일시 중지 되지 않은 상태에서 다시 시작되지 않도록 개선 할 수도 있지만 대부분의 사용 사례에서이 정도면 충분하다고 생각합니다.

setTimeout대신 에 대한 비슷한 답변을 찾고 있다면 https://stackoverflow.com/a/59274757/3723993을setInterval 확인 하십시오 .

선언적 버전 setTimeoutsetInterval, useTimeout및 및 TypeScript로 작성된 useInterval사용자 지정 useThrottledCallback후크를 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에게 다시 렌더링하도록 알립니다. 옵트 아웃

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 프리드먼
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.