Многократные вызовы для обновления состояния из useState в компоненте вызывают многократные повторные рендеринг

Я впервые пробовал перехватывать React, и все казалось хорошим, пока я не понял, что когда я получаю данные и обновляю две разные переменные состояния (данные и флаг загрузки), мой компонент (таблица данных) отображается дважды, хотя оба вызова до состояния обновления происходят те же функции. Вот моя функция 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 };
};

В обычном компоненте класса вы бы сделали один вызов, чтобы обновить состояние, которое может быть сложным объектом, но "способ зацепки", по-видимому, состоит в том, чтобы разделить состояние на более мелкие единицы, побочным эффектом которых является многократный отображает, когда они обновляются отдельно. Любые идеи, как смягчить это?

9 ответов

Решение

Вы могли бы объединить loading состояние и data состояние в один объект состояния, а затем вы можете сделать один setState позвоните, и будет только один рендер.

Примечание: в отличие от setState в компонентах класса setState вернулся из useState не объединяет объекты с существующим состоянием, оно полностью заменяет объект. Если вы хотите выполнить слияние, вам нужно прочитать предыдущее состояние и объединить его с новыми значениями самостоятельно. Обратитесь к документации.

Я бы не стал слишком сильно беспокоиться о том, как коллировать рендеры, пока вы не определите, что у вас проблемы с производительностью Рендеринг (в контексте React) и фиксация обновлений виртуального DOM в реальном DOM - разные вопросы. Рендеринг здесь относится к созданию виртуальных DOM, а не к обновлению DOM браузера. Реагировать может партия 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(() => {
    // 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>

У этого также есть другое решение, использующее useReducer! сначала мы определяем наш новыйsetState.

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

после этого мы можем просто использовать его как старые добрые классы this.setState, только без this!

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

Как вы могли заметить в нашем новом setState (точно так же, как и раньше с this.setState), нам не нужно обновлять все состояния вместе! например, я могу изменить одно из наших состояний следующим образом (и это не меняет другие состояния!):

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
}

Пакетное обновление в реакционных хуках https://github.com/facebook/react/issues/14259

В настоящее время React будет обновлять пакетные состояния, если они запускаются из события на основе React, например, нажатия кнопки или изменения ввода. Обновления не будут пакетироваться, если они запускаются вне обработчика событий React, например, асинхронный вызов.

Это будет делать:

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

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

Чтобы воспроизвести this.setStateповедение слияния компонентов класса, в документации React рекомендуется использовать функциональную формуuseStateс распространением объекта - нет необходимости вuseReducer:

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

Теперь два состояния объединены в одно, что сэкономит вам цикл рендеринга.

У одного объекта состояния есть еще одно преимущество: loading а также dataявляются зависимыми государствами. Некорректные изменения состояния становятся более очевидными, когда состояние объединяется:

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

Вы можете еще лучше обеспечить согласованные состояния, 1.) сделав статус -loading, success, errorи т. д. - явно в вашем состоянии и 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 
  )
}

const useData = () => {
  const [state, dispatch] = useReducer(reducer, {
    data: undefined,
    status: "loading"
  });

  useEffect(() => {
    fetchData_fakeApi().then(test => {
      if (test.ok) dispatch(["success", test.data.results]);
    });
  }, []);

  return state;
};

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

const App = () => {
  const { data, status } = useData();
  const count = useRenderCount();
  const countStr = `Re-rendered ${count.current} times`;

  return status === "loading" ? (
    <div> Loading (3 sec)... {countStr} </div>
  ) : (
    <div>
      Finished. Data: {JSON.stringify(data)}, {countStr}
    </div>
  );
}

//
// helpers
//

const useRenderCount = () => {
  const renderCount = useRef(0);
  useEffect(() => {
    renderCount.current += 1;
  });
  return renderCount;
};

const fetchData_fakeApi = () =>
  new Promise(resolve =>
    setTimeout(() => resolve({ ok: true, data: { results: [1, 2, 3] } }), 3000)
  );

ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity="sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity="sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w=" crossorigin="anonymous"></script>
<div id="root"></div>
<script>var { useReducer, useEffect, useState, useRef } = React</script>

PS: Убедитесь, что к пользовательским хукам добавлен префиксuse (useData вместо того getData). Также передан обратный вызовuseEffect не может быть async.

Если вы используете сторонние хуки и не можете объединить состояние в один объект или использовать useReducer, то решение заключается в использовании:

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

Рекомендован Даном Абрамовым здесь

См. Этот пример

Небольшое дополнение к ответу /questions/1724272/mnogokratnyie-vyizovyi-dlya-obnovleniya-sostoyaniya-iz-usestate-v-komponente-vyizyivayut-mnogokratnyie-povtornyie-rendering/1724286#1724286

Круто! Для тех, кто планирует использовать этот хук, он может быть написан немного надежным способом для работы с функцией в качестве аргумента, например так:

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

В дополнение к ответу user1751946 вам лучше запомнить функцию, чтобы она возвращала одну и ту же ссылку при каждом рендеринге вместо новой функции. Это может иметь решающее значение, если линтер TypeScript заставляет вас YangshunsetMergedStateкак зависимость в useCallbackили же useEffectв родительском компоненте.

      import {useCallback, useState} from "react";

export const useMergeState = <T>(initialState: T): [T, (newState: Partial<T>) => void] => {
    const [state, setState] = useState(initialState);
    const setMergedState = useCallback((newState: Partial<T>) =>
        setState(prevState => ({
            ...prevState,
            ...newState
        })), [setState]);
    return [state, setMergedState];
};

Вы также можете использовать useEffect для обнаружения изменения состояния и соответствующего обновления других значений состояния

Другие вопросы по тегам