useState 설정 메소드가 즉시 변경 사항을 반영하지 않음


168

나는 후크를 배우려고 노력하고 있으며 그 useState방법으로 인해 혼란스러워했다. 배열의 형태로 초기 값을 상태에 할당하고 있습니다. 세트 방법에는 useState심지어 나를 위해 작동하지 않습니다 spread(...)without spread operator. 다른 PC에서 API를 작성하여 상태로 설정하려는 데이터를 호출하고 가져옵니다.

내 코드는 다음과 같습니다.

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

const StateSelector = () => {
  const initialValue = [
    {
      category: "",
      photo: "",
      description: "",
      id: 0,
      name: "",
      rating: 0
    }
  ];

  const [movies, setMovies] = useState(initialValue);

  useEffect(() => {
    (async function() {
      try {
        //const response = await fetch(
        //`http://192.168.1.164:5000/movies/display`
        //);
        //const json = await response.json();
        //const result = json.data.result;
        const result = [
          {
            category: "cat1",
            description: "desc1",
            id: "1546514491119",
            name: "randomname2",
            photo: null,
            rating: "3"
          },
          {
            category: "cat2",
            description: "desc1",
            id: "1546837819818",
            name: "randomname1",
            rating: "5"
          }
        ];
        console.log(result);
        setMovies(result);
        console.log(movies);
      } catch (e) {
        console.error(e);
      }
    })();
  }, []);

  return <p>hello</p>;
};

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

setMovies(result)뿐만 아니라 setMovies(...result)작동하지 않습니다. 여기에 도움이 필요합니다.

결과 변수가 영화 배열로 푸시 될 것으로 기대합니다.

답변:


221

많은 확장함으로써 생성 클래스 구성 요소들에 setState를 추천 React.Component또는 React.PureComponent의해 제공 업데이터를 이용한 상태 업데이트 useState후크 비동기이며, 즉시 반영되지

또한 여기서 주요 이슈는 비동기 특성뿐만 아니라 현재 클로저를 기반으로 함수가 상태 값을 사용한다는 사실과 상태 업데이트는 기존 클로저가 영향을받지 않고 새로운 클로저가 생성되는 다음 다시 렌더링에 반영된다는 것입니다 . 현재 상태에서 후크 내 값은 기존 클로저에 의해 얻어지며 다시 렌더링이 발생하면 클로저는 함수가 다시 생성되는지 여부에 따라 업데이트됩니다.

setTimeout함수 를 추가하더라도 다시 렌더링이 발생한 시간이 지나면 시간 초과가 실행되지만 setTimout은 업데이트 된 값이 아니라 이전 클로저의 값을 계속 사용합니다.

setMovies(result);
console.log(movies) // movies here will not be updated

상태 업데이트에 대한 조치를 수행하려면 useState에 componentDidUpdate의해 리턴 된 setter에 콜백 패턴이 없으므로 클래스 컴포넌트에서 사용 하는 것과 유사하게 useEffect 후크를 사용해야합니다.

useEffect(() => {
    // action on update of movies
}, [movies]);

상태를 업데이트하는 구문과 관련 하여 상태 setMovies(result)의 이전 movies값을 비동기 요청에서 사용 가능한 값으로 대체합니다.

그러나 응답을 이전의 기존 값과 병합하려면 상태 업데이트의 콜백 구문과 스프레드 구문의 올바른 사용을 사용해야합니다.

setMovies(prevMovies => ([...prevMovies, ...result]));

17
안녕하세요, 양식 제출 핸들러 내에서 useState를 호출하는 것은 어떻습니까? 복잡한 양식의 유효성을 검사하는 중이며 submitHandler useState 후크 내부를 호출하지만 불행히도 변경 사항이 즉시 발생하지 않습니다!
da45

2
useEffect비동기 호출을 지원하지 않기 때문에 최상의 솔루션이 아닐 수도 있습니다. 따라서 movies상태 변경 에 대한 비동기 유효성 검사를 수행하려는 경우 제어 할 수 없습니다.
RA.

2
조언이 매우 좋은 반면, 원인의 설명이 개선되지 않을 수 있습니다 - 있는지 여부를 사실과는 아무 the updater provided by useState hook 달리, 비동기 this.state경우 그 돌연변이 수 있었다 this.setState동기했다는 폐쇄 약 const movies같은 경우에도 남아있을 것입니다 useState동기 함수 제공-내 답변의 예 참조
Aprillion

setMovies(prevMovies => ([...prevMovies, ...result]));나를 위해 일했다
Mihir

setter가 비동기 적이기 때문에 오래된 폐쇄를 로깅하기 때문에 잘못된 결과를 로깅합니다 . 비동기가 문제인 경우 시간 초과 후 로그를 기록 할 수 있지만 한 시간 동안 시간 초과를 설정하고 비동기가 문제의 원인이 아니기 때문에 여전히 잘못된 결과를 기록 할 수 있습니다.
HMR

127

받는 사람 Addional 자세한 내용은 이전의 대답 :

React setState 비동기식 (클래스와 후크 모두)이며 관찰 된 동작을 설명하기 위해 그 사실을 사용하고 싶은 유혹이 있지만 그것이 일어나는 이유 는 아닙니다 .

TLDR : 이유는 불변 값 주위의 클로저 범위 const입니다.


솔루션 :

  • 렌더링 함수 (내포 된 함수가 아닌)의 값을 읽습니다.

    useEffect(() => { setMovies(result) }, [])
    console.log(movies)
  • 변수를 종속 항목에 추가하고 react-hooks / exhaustive-deps eslint 규칙을 사용하십시오.

    useEffect(() => { setMovies(result) }, [])
    useEffect(() => { console.log(movies) }, [movies])
  • 변경 가능한 참조를 사용하십시오 (위에서 불가능한 경우).

    const moviesRef = useRef(initialValue)
    useEffect(() => {
      moviesRef.current = result
      console.log(moviesRef.current)
    }, [])

왜 발생하는지 설명하십시오.

비동기가 유일한 이유 였다면 가능할 것 await setState()입니다.

Howerver, 모두 props하고 state있습니다 1 렌더링 동안 변하지 않는 것으로 가정 .

this.state불변 인 것처럼 취급하십시오 .

후크를 사용 하면 키워드 와 함께 상수 값 을 사용하여이 가정이 향상됩니다 const.

const [state, setState] = useState('initial')

값은 두 렌더간에 다를 수 있지만 렌더 자체와 클로저 내에서 일정하게 유지됩니다 (렌더가 완료된 후에도 더 오래 지속되는 함수 (예 : useEffect이벤트 핸들러, Promise 또는 setTimeout 내)).

가짜이지만 동기식 의 React와 유사한 구현을 따르십시오 .

// sync implementation:

let internalState
let renderAgain

const setState = (updateFn) => {
  internalState = updateFn(internalState)
  renderAgain()
}

const useState = (defaultState) => {
  if (!internalState) {
    internalState = defaultState
  }
  return [internalState, setState]
}

const render = (component, node) => {
  const {html, handleClick} = component()
  node.innerHTML = html
  renderAgain = () => render(component, node)
  return handleClick
}

// test:

const MyComponent = () => {
  const [x, setX] = useState(1)
  console.log('in render:', x) // ✅
  
  const handleClick = () => {
    setX(current => current + 1)
    console.log('in handler/effect/Promise/setTimeout:', x) // ❌ NOT updated
  }
  
  return {
    html: `<button>${x}</button>`,
    handleClick
  }
}

const triggerClick = render(MyComponent, document.getElementById('root'))
triggerClick()
triggerClick()
triggerClick()
<div id="root"></div>


12
훌륭한 답변입니다. IMO,이 답변은 당신이 그것을 읽고 흡수하는 데 충분한 시간을 투자한다면 미래에 가장 가슴 아픈 것을 구할 것입니다.
Scott Mallon

이것은 라우터와 함께 컨텍스트를 사용하는 것을 매우 어렵게 만듭니다. 다음 페이지에 도착하면 상태가 사라졌습니다. 그리고 렌더링 할 수 없기 때문에 useEffect 후크 내부에서만 State를 설정할 수 없습니다 ... 답변의 완전성을 높이 평가하고 내가보고있는 것을 설명하지만 이길 방법을 알 수 없습니다. 컨텍스트에서 함수를 전달하여 효과적으로 유지되지 않는 값을 업데이트합니다 ... 그래서 컨텍스트에서 닫히지 않아야합니다.
알 조슬린

@AlJoslin은 언뜻보기에 폐쇄 범위로 인해 발생할 수도 있지만 별도의 문제처럼 보입니다. 구체적인 질문이 있으시면 코드 예제와 함께 모든 stackoverflow 질문을
만드십시오

1
실제로 @kentcdobs 기사 (아래 참조)에 따라 useReducer로 다시 작성을 마쳤습니다.이 클로저 문제로 인해 1 비트가 아닌 확실한 결과를 얻었습니다. (ref : kentcdodds.com/blog/how-to-use-react-context-effectively )
Al Joslin

1

@kentcdobs 기사 (아래 참조)에 따라 useReducer로 다시 작성을 마쳤습니다.이 클로저 문제로 인해 1 비트가 아닌 확실한 결과를 얻었습니다.

참조 : https://kentcdodds.com/blog/how-to-use-react-context-effectively

나는 그의 읽을 수있는 상용구를 내가 선호하는 DRYness 수준으로 압축했다. 그의 샌드 박스 구현을 읽으면 실제로 어떻게 작동하는지 보여줄 것이다.

즐기십시오.

import React from 'react'

// ref: https://kentcdodds.com/blog/how-to-use-react-context-effectively

const ApplicationDispatch = React.createContext()
const ApplicationContext = React.createContext()

function stateReducer(state, action) {
  if (state.hasOwnProperty(action.type)) {
    return { ...state, [action.type]: state[action.type] = action.newValue };
  }
  throw new Error(`Unhandled action type: ${action.type}`);
}

const initialState = {
  keyCode: '',
  testCode: '',
  testMode: false,
  phoneNumber: '',
  resultCode: null,
  mobileInfo: '',
  configName: '',
  appConfig: {},
};

function DispatchProvider({ children }) {
  const [state, dispatch] = React.useReducer(stateReducer, initialState);
  return (
    <ApplicationDispatch.Provider value={dispatch}>
      <ApplicationContext.Provider value={state}>
        {children}
      </ApplicationContext.Provider>
    </ApplicationDispatch.Provider>
  )
}

function useDispatchable(stateName) {
  const context = React.useContext(ApplicationContext);
  const dispatch = React.useContext(ApplicationDispatch);
  return [context[stateName], newValue => dispatch({ type: stateName, newValue })];
}

function useKeyCode() { return useDispatchable('keyCode'); }
function useTestCode() { return useDispatchable('testCode'); }
function useTestMode() { return useDispatchable('testMode'); }
function usePhoneNumber() { return useDispatchable('phoneNumber'); }
function useResultCode() { return useDispatchable('resultCode'); }
function useMobileInfo() { return useDispatchable('mobileInfo'); }
function useConfigName() { return useDispatchable('configName'); }
function useAppConfig() { return useDispatchable('appConfig'); }

export {
  DispatchProvider,
  useKeyCode,
  useTestCode,
  useTestMode,
  usePhoneNumber,
  useResultCode,
  useMobileInfo,
  useConfigName,
  useAppConfig,
}

이와 비슷한 사용법으로 :

import { useHistory } from "react-router-dom";

// https://react-bootstrap.github.io/components/alerts
import { Container, Row } from 'react-bootstrap';

import { useAppConfig, useKeyCode, usePhoneNumber } from '../../ApplicationDispatchProvider';

import { ControlSet } from '../../components/control-set';
import { keypadClass } from '../../utils/style-utils';
import { MaskedEntry } from '../../components/masked-entry';
import { Messaging } from '../../components/messaging';
import { SimpleKeypad, HandleKeyPress, ALT_ID } from '../../components/simple-keypad';

export const AltIdPage = () => {
  const history = useHistory();
  const [keyCode, setKeyCode] = useKeyCode();
  const [phoneNumber, setPhoneNumber] = usePhoneNumber();
  const [appConfig, setAppConfig] = useAppConfig();

  const keyPressed = btn => {
    const maxLen = appConfig.phoneNumberEntry.entryLen;
    const newValue = HandleKeyPress(btn, phoneNumber).slice(0, maxLen);
    setPhoneNumber(newValue);
  }

  const doSubmit = () => {
    history.push('s');
  }

  const disableBtns = phoneNumber.length < appConfig.phoneNumberEntry.entryLen;

  return (
    <Container fluid className="text-center">
      <Row>
        <Messaging {...{ msgColors: appConfig.pageColors, msgLines: appConfig.entryMsgs.altIdMsgs }} />
      </Row>
      <Row>
        <MaskedEntry {...{ ...appConfig.phoneNumberEntry, entryColors: appConfig.pageColors, entryLine: phoneNumber }} />
      </Row>
      <Row>
        <SimpleKeypad {...{ keyboardName: ALT_ID, themeName: appConfig.keyTheme, keyPressed, styleClass: keypadClass }} />
      </Row>
      <Row>
        <ControlSet {...{ btnColors: appConfig.buttonColors, disabled: disableBtns, btns: [{ text: 'Submit', click: doSubmit }] }} />
      </Row>
    </Container>
  );
};

AltIdPage.propTypes = {};

이제 모든 페이지의 모든 곳에서 모든 것이 매끄럽게 유지됩니다.

좋은!

고마워 켄트!


-2
// replace
return <p>hello</p>;
// with
return <p>{JSON.stringify(movies)}</p>;

지금 당신은 당신의 코드가 실제로 것을 볼 수 않는 일을. 작동하지 않는 것은 console.log(movies)입니다. 이전 상태를movies 가리 키기 때문 입니다. 의 바로 바깥쪽으로 이동 하면 업데이트 된 영화 개체가 표시됩니다.console.log(movies)useEffect

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