コンポーネントのuseStateから状態アップデーターを複数回呼び出すと、複数の再レンダリングが発生します


120

初めて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 };
};

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

回答:


119

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

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

パフォーマンスの問題があると判断するまで、レンダリングを過度に呼び出すことについてはあまり心配しません。(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/react@16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16.7.0-alpha.0/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/react@16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>


61

これには、useReducer!を使用した別の解決策もあります。まず、新しいを定義しますsetState

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

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

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

新しいものでお気づきかもしれませんがsetState(以前のようにthis.setState)、すべての状態を一緒に更新する必要はありません!たとえば、次のように状態の1つを変更できます(他の状態は変更されません!):

setState({loading: false})

すごい、ハ?

それでは、すべての要素をまとめましょう。

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
}

45

react-hooksでのバッチ更新 https://github.com/facebook/react/issues/14259

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


7

これは行います:

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

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

4

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

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

ここでダン・アブラモフが推奨

この例を参照してください


4

複製するにはthis.setState、クラスの構成要素からマージの動作を、ドキュメントが反応お勧めの関数形式を使用してuseStateオブジェクトの広がりを持つ-不要のためにuseReducer

setState(prevState => {
  return {...prevState, loading, data};
});

これで、2つの状態が1つに統合され、レンダリングサイクルが節約されます。

1つの状態オブジェクトには別の利点があります。loadingそれdata従属状態です。状態をまとめると、無効な状態の変化がより明らかになります。

setState({ loading: true, data }); // ups... loading, but we already set data

あなたがより良いことができ、一貫した状態を保証 すること)1.して、ステータス- 、loadingsuccesserrorなど-明示的な使用して、あなたの状態および2)でuseReducer減速中にカプセル化する状態論理に:

const useData = () => {
  const [state, dispatch] = useReducer(reducer, /*...*/);

  useEffect(() => {
    api.get('/people').then(test => {
      if (test.ok) dispatch(["success", test.data.results]);
    });
  }, []);
};

const reducer = (state, [status, payload]) => {
  if (status === "success") return { ...state, data: payload, status };
  // keep state consistent, e.g. reset data, if loading
  else if (status === "loading") return { ...state, data: undefined, status };
  return state;
};

const App = () => {
  const { data, status } = useData();
  return status === "loading" ? <div> Loading... </div> : (
    // success, display data 
  )
}

PS:カスタムフックの前に必ずuseuseDataではなくgetData)を付けてください。また、コールバックをに渡すuseEffectことはできませんasync


1

https://stackoverflow.com/a/53575023/121143に答えるための少しの追加

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

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];
};

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

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];
};

0

useEffect状態の変化を検出し、それに応じて他の状態値を更新するために使用することもできます

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.