Состояние функционального компонента React сбрасывается с помощью setTimeout

#reactjs #react-hooks

#reactjs #реагирующие хуки

Вопрос:

Может кто-нибудь, пожалуйста, помочь мне понять, почему эта переменная requestCount привязывается к 0? Я ожидаю, что оно будет увеличиваться каждые 3 секунды, но вместо этого этого не происходит.

 const SomeComponent = () => {
  const [requestCount, setRequestCount] = useState(-1);

  const doSomeWork= () => {
    setRequestCount(requestCount   1);
    setTimeout(doSomeWork, 3000);
  };

  return (
    <>
      <button onClick={doSomeWork}>Click</button>
      {requestCount}
    </>
  );
};
 

Ответ №1:

Сначала следует знать некоторые знания: почему я вижу устаревшие реквизиты или состояние внутри моей функции?

У вас есть два варианта:

  1. Используйте функциональные обновления:
  2. Используйте useCallback с зависимостями для создания новых doSomeWork каждый раз при изменении requestCount состояния. Так что doSomeWork вместо устаревшего значения будет считываться последнее requestCount состояние. И вы должны выполнить задачу макроса, поставленную в очередь, setTimeout используя useEffect() hook с doSomeWork зависимостью. Потому что нам нужно дождаться завершения выполнения useCallback и создать новую doSomeWork функцию, чтобы макрос task( doSomeWork ) прочитал последнюю requestCount версию.

В приведенном ниже примере показаны эти два варианта:

SomeComponent.tsx :

 import React, { useCallback, useEffect, useRef } from 'react';
import { useState } from 'react';

export const SomeComponent = () => {
  const [requestCount, setRequestCount] = useState(-1);
  // should execute macro task queued by setTimeout
  const ref = useRef(null);

  // option 1
  // const doSomeWork = () => {
  //   setRequestCount((pre) => pre   1);
  //   setTimeout(doSomeWork, 3000);
  // };

  // option 2
  const doSomeWork = useCallback(() => {
    setRequestCount(requestCount   1);
    if (ref.current) return;
    ref.current = true;
  }, [requestCount]);

  useEffect(() => {
    if (ref.current) {
      setTimeout(doSomeWork, 3000);
    }
  }, [ref.current, doSomeWork]);

  return (
    <>
      <button onClick={doSomeWork}>Click</button>
      {requestCount}
    </>
  );
};
 

Модульный тест:

 import { render, screen, fireEvent, act } from '@testing-library/react';
import React from 'react';
import { SomeComponent } from './SomeComponent';

describe('70297876', () => {
  test('should pass', async () => {
    jest.useFakeTimers();
    const { container } = render(<SomeComponent />);
    expect(container).toMatchInlineSnapshot(`
<div>
  <button>
    Click
  </button>
  -1
</div>
`);
    const button = screen.getByText('Click');
    fireEvent.click(button);
    act(() => {
      jest.advanceTimersByTime(3000);
    });

    expect(container).toMatchInlineSnapshot(`
<div>
  <button>
    Click
  </button>
  1
</div>
`);
    act(() => {
      jest.advanceTimersByTime(3000);
    });
    expect(container).toMatchInlineSnapshot(`
<div>
  <button>
    Click
  </button>
  2
</div>
`);
  });
});
 

Результат теста:

  PASS  stackoverflow/70297876/SomeComponent.test.tsx
  70297876
    ✓ should pass (36 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   3 passed, 3 total
Time:        3.865 s, estimated 4 s