programing

후크 사용 시 React batch state update가 기능합니까?

golfzon 2023. 3. 15. 20:07
반응형

후크 사용 시 React batch state update가 기능합니까?

클래스 컴포넌트의 경우this.setState는, 이벤트 핸들러내의 경우에 배치 콜을 실시합니다.그러나 이벤트 핸들러 외부에서 상태를 업데이트하여useState후크?

function Component() {
  const [a, setA] = useState('a');
  const [b, setB] = useState('b');

  function handleClick() {
    Promise.resolve().then(() => {
      setA('aa');
      setB('bb');
    });
  }

  return <button onClick={handleClick}>{a}-{b}</button>
}

렌더링할 것인가?aa - bb지금 당장요?그렇지 않으면aa - b그리고 나서.aa - bb?

TL;DR – 상태 변경이 비동기적으로 트리거되면(예: 약속으로 포장됨) 일괄 처리되지 않고 직접 트리거되면 일괄 처리됩니다.

이것을 시험하기 위해서 샌드박스를 설정했습니다.https://codesandbox.io/s/402pn5l989

import React, { Fragment, useState } from 'react';
import ReactDOM from 'react-dom';

import './styles.css';

function Component() {
  const [a, setA] = useState('a');
  const [b, setB] = useState('b');
  console.log('a', a);
  console.log('b', b);

  function handleClickWithPromise() {
    Promise.resolve().then(() => {
      setA('aa');
      setB('bb');
    });
  }

  function handleClickWithoutPromise() {
    setA('aa');
    setB('bb');
  }

  return (
    <Fragment>
    <button onClick={handleClickWithPromise}>
      {a}-{b} with promise
    </button>
    <button onClick={handleClickWithoutPromise}>
      {a}-{b} without promise
    </button>
      </Fragment>
  );
}

function App() {
  return <Component />;
}

const rootElement = document.getElementById('root');
ReactDOM.render(<App />, rootElement);

두 개의 버튼을 만들었습니다. 하나는 약속으로 포장된 상태 변경을 트리거하고 다른 하나는 상태 변경을 직접 트리거합니다.

콘솔을 보면 "with promise" 버튼을 누르면 먼저 표시됩니다.a aa그리고.b b,그리고나서a aa그리고.b bb.

그래서 대답은 '아니오'입니다. 이 경우엔,aa - bb상태가 변경될 때마다 새 렌더가 트리거되므로 배치 작업이 수행되지 않습니다.

단, [약속 없음]버튼을 클릭하면 콘솔이 표시됩니다.a aa그리고.b bb지금 당장.

이 경우 React는 상태 변경을 배치하고 두 가지 렌더링을 함께 수행합니다.

현재 React v16 이전 버전에서는 다음과 같은 React 이벤트 핸들러 내에서만 갱신됩니다.click또는onChange등의 배치가 디폴트로 설정되어 있습니다.따라서 클래스 상태 업데이트는 후크에서 동일한 방식으로 배치됩니다.

드물게 필요할 때 이벤트 핸들러 외부에서 일괄 처리를 강제하는 불안정한 API가 있습니다.

ReactDOM.unstable_batchedUpdates(() => { ... })

향후 버전에서의 모든 상태 업데이트를 v17 이상으로 일괄 처리할 계획이 있습니다.

또한 이벤트 핸들러 내에서 상태 업데이트 호출이 비동기 함수이거나 비동기 코드로 인해 트리거된 경우 직접 업데이트가 배치되는 위치에 배치되지 않습니다.

동기 코드 상태 업데이트가 일괄 처리되고 비동기 코드 업데이트가 되지 않는 경우

function App() {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  // async update from useEffect
  useEffect(() => {
    setTimeout(() => {
      setCount1(count => count + 1);
      setCount2(count => count + 2);
    }, 3000);
  }, []);

  const handleAsyncUpdate = async () => {
    await Promise.resolve("state updated");
    setCount1(count => count + 2);
    setCount2(count => count + 1);
  };

  const handleSyncUpdate = () => {
    setCount1(count => count + 2);
    setCount2(count => count + 1);
  };

  console.log("render", count1, count2);
  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <h2>Start editing to see some magic happen!</h2>
      <button type="button" onClick={handleAsyncUpdate}>
        Click for async update
      </button>
      <button type="button" onClick={handleSyncUpdate}>
        Click for sync update
      </button>
    </div>
  );
}

https://codesandbox.io/s/739rqyyqmq

이벤트 핸들러가react-based그런 다음 업데이트를 배치합니다.이는 setState 콜과 useState 콜 모두에 해당됩니다.

그러나 이벤트가 다음과 같은 경우 자동으로 배치되지 않습니다.non-reactsetTimeout, Promise 콜 등입니다.즉, Web API로부터의 이벤트입니다.

@Patrick Hund가 이미 대답했습니다.React 18 배치 상태 업데이트가 Promise, setTimeout에 대해 기본적으로 가능하다는 것을 여기에서 업데이트하고자 합니다.

React 18까지는 React 이벤트 핸들러 중에만 업데이트를 일괄 처리했습니다.약속, setTimeout, 네이티브이벤트 핸들러 또는 기타 이벤트 내의 업데이트는 기본적으로 React에서 배치되지 않았습니다.

자세한 것은, https://github.com/reactwg/react-18/discussions/21 를 참조해 주세요.

과 반응 18:createRoot 모든 장소에 관계없이, 「 」, 「 」, 「 」, 「 」, 「 」, 「 」, 「 」, 「 」, 「 」, 「 」의 어느쪽이든 상관없습니다.

React 18은 React 18이라는 하십시오.ReactDOM.render()오래된 동작을 유지합니다.시간 초과, 약속 또는 기타 이벤트 기간 내에 업데이트를 일괄 처리하려는 경우 사용합니다.

여기서는 타임아웃 시간 내에 상태를 두 번 업데이트하지만 React는 한 번만 렌더링합니다.

import React, { useState } from "react";
import ReactDOM from "react-dom";

function App() {
  const [x, setX] = useState(0);
  const [y, setY] = useState(0);

  function handleClick() {
    setTimeout(() => {
      setX((p) => p + 1);
      setY((p) => p + 1);
    }, 100);
  }

  console.log(`render x: ${x} y: ${y}`);

  return (
    <div className="App">
      <button onClick={handleClick}>Update with promise</button>

      <div>X: {x} </div>
      <div>Y: {y} </div>
    </div>
  );
}

ReactDOM.createRoot(document.getElementById("root")).render(<App />);

이는 React 18용으로 업데이트되었습니다.그들은 "자동 배치"라고 불리는 것을 도입했다.이전 버전의 React에서는 브라우저 이벤트에 의해 트리거/업데이트된 상태에 대해서만 배치 처리가 이루어졌지만 React 18에서는 어디에서 왔는지 상관없이 모든 상태에 대해 배치 처리가 수행됩니다.

컴포넌트를 검토합니다.


    const App = () => {
      const [count, setCount] = useState(0);
      const [trigger, setTreigger] = useState(false);

      const handleClick = () => {
        setTimeout(() => {
          setCount(count => count++);
          setTrigger(trigger => !trigger);
        }, 100)    
      }
      
      console.log("Re-render", count, trigger);

      return (
        <div>
          <button onClick={handleClick}>
            Click Me!
          </button>
        </div>);
    }

버전 18에서 React는 이 경우에도 배치 처리를 수행합니다.이는 React가 어디에서 왔는지에 관계없이 주(州)에서 효율화되고 있음을 보여줍니다.

위 구성 요소의 console.log 출력에서 확인할 수 있습니다.

여기 Jest 테스트가 있습니다.이것은 렌더링과 렌더링만 트리거하는 것을 나타냅니다.useEffect

  it("two set states will trigger only one render with effect check", async () => {
    const renderFn = jest.fn();
    const effectFn = jest.fn();
    function MyComponent() {
      const [foo, setFoo] = useState("foo");
      const [bar, setBar] = useState("bar");

      const handleClick = useCallback(() => {
        setFoo("blah");
        setBar("blah");
      }, [])

      useEffect(()=> {
        noop(foo);
        noop(bar);
        effectFn();
      },[foo,bar]);
      renderFn();
      return <div data-testid="test" onClick={handleClick}>{foo}{bar}</div>;
    }

    const { unmount } = render(<MyComponent />);
    expect(screen.getByTestId("test").textContent).toEqual("foobar");
    expect(renderFn).toBeCalledTimes(1);
    expect(effectFn).toBeCalledTimes(1);
    fireEvent.click(screen.getByTestId("test"))
    expect(renderFn).toBeCalledTimes(2);
    expect(effectFn).toBeCalledTimes(2);
    expect(screen.getByTestId("test").textContent).toEqual("blahblah");
    unmount();
    expect(renderFn).toBeCalledTimes(2);
    expect(effectFn).toBeCalledTimes(2);
  })

와 함께 사용하는 경우async배치하지 않고 다음 세트를 수행하기 위해 다른 렌더가 필요합니다.

  it("two set states will trigger render with effect check with async handler per await", async () => {
    const renderFn = jest.fn();
    const effectFn = jest.fn();
    function MyComponent() {
      const [foo, setFoo] = useState("foo");
      const [bar, setBar] = useState("bar");

      const handleClick = useCallback(async () => {
        await new Promise<void>((resolve) => { setFoo("blah"); resolve() })
        await new Promise<void>((resolve) => { setBar("blah"); resolve() })
      }, [])

      useEffect(() => {
        noop(foo);
        noop(bar);
        effectFn();
      }, [foo, bar]);
      renderFn();
      return <div data-testid="test" onClick={handleClick}>{foo}{bar}</div>;
    }

    const { unmount } = render(<MyComponent />);
    expect(screen.getByTestId("test").textContent).toEqual("foobar");
    expect(renderFn).toBeCalledTimes(1);
    expect(effectFn).toBeCalledTimes(1);
    fireEvent.click(screen.getByTestId("test"))
    expect(renderFn).toBeCalledTimes(2);
    expect(effectFn).toBeCalledTimes(2);
    expect(screen.getByTestId("test").textContent).toEqual("blahbar");
    // second state update after await
    await act(() => Promise.resolve());
    expect(renderFn).toBeCalledTimes(3);
    expect(effectFn).toBeCalledTimes(3);
    expect(screen.getByTestId("test").textContent).toEqual("blahblah");
    unmount();
    expect(renderFn).toBeCalledTimes(3);
    expect(effectFn).toBeCalledTimes(3);
  })

기타 시나리오를 포함한 모든 출처는 https://github.com/trajano/react-hooks/blob/master/src/__tests__/useStateBatchTest.tsx에 있습니다.

언급URL : https://stackoverflow.com/questions/53048495/does-react-batch-state-update-functions-when-using-hooks

반응형