web-dev-qa-db-ja.com

コンポーネントのuseStateからState Updaterを複数回呼び出すと、複数の再レンダリングが発生します

初めてReactフックを試してみましたが、データを取得して2つの異なる状態変数(データとロードフラグ)を更新すると、コンポーネント(データテーブル)がレンダリングされることに気付くまで、すべてが良さそうでした状態アップデータへの両方の呼び出しが同じ関数で発生している場合でも、2回。これは、両方の変数をコンポーネントに返すAPI関数です。

const getData = url => {

    const [data, setData] = useState(null);
    const [loading, setLoading] = useState(true);

    useEffect(async () => {

        const test = await api.get('/people')

        if(test.ok){
            setLoading(false);
            setData(test.data.results);
        }

    }, []);

    return { data, loading };
};

通常のクラスコンポーネントでは、複雑なオブジェクトになる可能性のある状態を更新するために単一の呼び出しを行いますが、「フックの方法」は状態を小さな単位に分割することのようです。個別に更新されたときにレンダリングします。これを軽減する方法はありますか?

30
jonhobbs

loading状態とdata状態を1つの状態オブジェクトに結合してから、1つのsetState呼び出しを行うと、レンダリングは1つだけになります。

注:クラスコンポーネントのsetStateとは異なり、setStateから返されるuseStateはオブジェクトをマージしません既存の状態では、オブジェクトを完全に置き換えます。マージを行う場合は、以前の状態を読み取り、自分で新しい値とマージする必要があります。 docs を参照してください。

パフォーマンスの問題があると判断するまで、レンダリングを過度に呼び出すことについてあまり心配しません。レンダリング(Reactコンテキスト)と仮想DOM更新の実際のDOMへのコミットは別の問題です。ここでのレンダリングとは、仮想DOMの生成を指し、ブラウザDOMの更新に関するものではありません。 Reactは、setState呼び出しをバッチ処理し、ブラウザのDOMを最終的な新しい状態に更新します。

const {useState, useEffect} = React;

function App() {
  const [userRequest, setUserRequest] = useState({
    loading: false,
    user: null,
  });

  useEffect(() => {
    // Note that this replaces the entire object and deletes user key!
    setUserRequest({ loading: true });
    fetch('https://randomuser.me/api/')
      .then(results => results.json())
      .then(data => {
        setUserRequest({
          loading: false,
          user: data.results[0],
        });
      });
  }, []);

  const { loading, user } = userRequest;

  return (
    <div>
      {loading && 'Loading...'}
      {user && user.name.first}
    </div>
  );
}

ReactDOM.render(<App />, document.querySelector('#app'));
<script src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>
<div id="app"></div>

代替-独自の状態マージフックを記述する

const {useState, useEffect} = React;

function useMergeState(initialState) {
  const [state, setState] = useState(initialState);
  const setMergedState = newState => 
    setState(prevState => Object.assign({}, prevState, newState)
  );
  return [state, setMergedState];
}

function App() {
  const [userRequest, setUserRequest] = useMergeState({
    loading: false,
    user: null,
  });

  useEffect(() => {
    setUserRequest({ loading: true });
    fetch('https://randomuser.me/api/')
      .then(results => results.json())
      .then(data => {
        setUserRequest({
          loading: false,
          user: data.results[0],
        });
      });
  }, []);

  const { loading, user } = userRequest;

  return (
    <div>
      {loading && 'Loading...'}
      {user && user.name.first}
    </div>
  );
}

ReactDOM.render(<App />, document.querySelector('#app'));
<script src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>
<div id="app"></div>
31
Yangshun Tay

反応フックのバッチ更新https://github.com/facebook/react/issues/14259

ボタンのクリックや入力の変更など、Reactベースのイベント内からトリガーされた場合、Reactは現在、状態の更新をバッチ処理します。非同期呼び出しなど、Reactイベントハンドラーの外部でトリガーされた場合、バッチ更新は行われません。

9
Sureshraj

これには、useReducer!を使用した別のソリューションもあります。まず、新しいsetStateを定義します。

const [state, setState] = useReducer(
  (state, newState) => ({...state, ...newState}),
  {loading: true, data: null}
)

その後、thisなしで、古き良きクラスthis.setStateのように単純に使用できます!

setState({loading: false, data: test.data.results})

したがって、すべて一緒に:

import {useReducer} from 'react'

const getData = url => {
  const [state, setState] = useReducer(
    (state, newState) => ({...state, ...newState}),
    {loading: true, data: null}
  )

  useEffect(async () => {
    const test = await api.get('/people')
    if(test.ok){
      setState({loading: false, data: test.data.results})
    }
  }, [])

  return state
}
7
Vahid Al

答えに少し追加 https://stackoverflow.com/a/53575023/12114

クール!このフックの使用を計画している人のために、次のように、関数を引数として使用するために少し堅牢な方法で記述することができます。

const useMergedState = initial => {
  const [state, setState] = React.useState(initial);
  const setMergedState = newState =>
    typeof newState == "function"
      ? setState(prevState => ({ ...prevState, ...newState(prevState) }))
      : setState(prevState => ({ ...prevState, ...newState }));
  return [state, setMergedState];
};

Update:最適化されたバージョン、着信部分状態が変更されなかった場合、状態は変更されません。

const shallowPartialCompare = (obj, partialObj) =>
  Object.keys(partialObj).every(
    key =>
      obj.hasOwnProperty(key) &&
      obj[key] === partialObj[key]
  );

const useMergedState = initial => {
  const [state, setState] = React.useState(initial);
  const setMergedState = newIncomingState =>
    setState(prevState => {
      const newState =
        typeof newIncomingState == "function"
          ? newIncomingState(prevState)
          : newIncomingState;
      return shallowPartialCompare(prevState, newState)
        ? prevState
        : { ...prevState, ...newState };
    });
  return [state, setMergedState];
};
1
mschayna

サードパーティのフックを使用していて、状態を1つのオブジェクトにマージできない場合、またはuseReducerを使用できない場合、解決策は以下を使用することです。

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

Dan Abramov推奨 こちら

こちらをご覧ください

0
Mohamed Ramrami

これにより:

const [state, setState] = useState({ username: '', password: ''});

// later
setState({
    ...state,
    username: 'John'
});
0
Mehdi Dehghani