useState setメソッドが変更をすぐに反映しない


167

フックを習得しようとしていますが、そのuseState方法によって混乱しました。配列の形で状態に初期値を割り当てています。のsetメソッドはuseStatespread(...)またはを使用しても機能しません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)機能していません。ここでいくつかのヘルプを使用できます。

結果変数がmovies配列にプッシュされることを期待しています。

回答:


219

React.Componentまたはによって作成されたClassコンポーネントのsetStateと同様にReact.PureComponentuseStateフックによって提供されるアップデーターを使用した状態の更新も非同期であり、すぐには反映されません

また、ここでの主な問題は、非同期の性質だけでなく、現在のクロージャと状態の更新に基づいて関数によって状態値が使用されるという事実は、既存のクロージャが影響を受けずに新しいクロージャが作成される次の再レンダリングに反映されます。現在の状態では、フック内の値は既存のクロージャーによって取得され、再レンダリングが発生すると、クロージャーは関数が再作成されるかどうかに基づいて更新されます

setTimeout関数を追加しても、再レンダリングが発生するまでにタイムアウトが発生しますが、setTimoutは以前のクロージャーの値を使用し、更新された値は使用しません。

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

状態の更新時にアクションを実行する場合は、useEffectフックを使用する必要があります。これは、useState componentDidUpdateによって返されるセッターにはコールバックパターンがないため、クラスコンポーネントで使用する場合と同様です。

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

セッターが非同期であるためではなく、古いクロージャーをログに記録しているため、誤った結果がログに記録されています。非同期が問題だった場合は、タイムアウト後にログに記録できますが、1時間のタイムアウトを設定しても、非同期が問題の原因ではないため、誤った結果をログに記録できます。
HMR

126

以前の回答への追加の詳細:

React setState 非同期(クラスとフックの両方)であり、観察された動作を説明するためにその事実を使用するのは魅力的ですが、それが発生する理由ではありません。

TLDR:その理由は、不変の値に関するクロージャスコープconstです。


ソリューション:

  • (ネストされた関数内ではなく)render関数で値を読み取ります。

    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()です。

どちらもprops1つのレンダー中に不変でstateあると見なされます

this.state不変であるかのように扱います。

フックを使用すると、この仮定はキーワードで定数値を使用することによって強化されますconst

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

値は2つのレンダー間で異なる場合がありますが、レンダー自体とクロージャー(たとえば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>


11
すばらしい答えです。IMO、この答えは、それを読み通して吸収するのに十分な時間を費やした場合、将来的に最も悲痛な思いを救います。
スコットマロン

これは、ルーターと組み合わせてコンテキストを使用することを非常に難しくします、そうですか?次のページに進むと、状態が消えてしまいます。そして、useEffectフックの内部でのみレンダリングできないため、setStateを設定できません...答えの完全性に感謝します。それを打つ方法を理解することはできません。コンテキストで関数を渡して、効果的に永続化されない値を更新しています...それで、コンテキスト内のクロージャーからそれらを取り除く必要がありますよね?
アルジョスリン

@AlJoslinは一見すると、たとえそれがクロージャスコープによって引き起こされたとしても、別の問題のように見えます。具体的な質問がある場合は、コード例とすべてを含む新しいstackoverflow質問を作成してください...
Aprillion

1
実際、私は@kentcdobsの記事(下記参照)に従ってuseReducerで書き換えを完了しました。これにより、これらのクロージャの問題の影響を受けない確かな結果が得られました。(参照:kentcdodds.com/blog/how-to-use-react-context-effectively
アルジョスリン

1

@kentcdobsの記事(下記参照)に従って、useReducerによる書き換えを完了しました。これにより、これらのクロージャの問題の影響を受けない確かな結果が得られました。

参照してください: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.