Несколько вызовов в state updater из useState в компоненте вызывает многократное повторное удаление

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

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

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

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

Ответ 2

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

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

Ответ 3

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

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

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

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
}

Ответ 4

Небольшое дополнение, чтобы ответить fooobar.com/questions/16652933/...

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

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

Ответ 5

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

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

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

Ответ 6

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

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

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

Посмотрите этот пример