Кілька викликів до оновлення стану від useState у компоненті викликає багаторазові повторні рендери


122

Я вперше пробую хуки 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 };
};

У звичайному компоненті класу ви б зробили один виклик для оновлення стану, який може бути складним об'єктом, але, схоже, "спосіб зачіпки" полягає в розбитті стану на менші одиниці, побічним ефектом яких, здається, є багаторазові повторні відображає, коли вони оновлюються окремо. Будь-які ідеї, як це пом'якшити?


Якщо у вас залежні штати, то, мабуть, вам слід скористатисяuseReducer
thinklinux

Оце Так! Я лише щойно це відкрив, і це повністю роздуло моє розуміння того, як працює реагування на рендеринг. Я не можу зрозуміти жодної переваги такої роботи - здається досить довільним, що поведінка в асинхронному зворотному виклику відрізняється від звичайного обробника подій. До речі, у моїх тестах здається, що примирення (тобто оновлення реального DOM) відбувається лише після того, як всі виклики setState будуть оброблені, тому проміжні виклики рендерингу все одно марно витрачаються.
Енді

Відповіді:


122

Ви можете об'єднати loadingстан і dataстан в один об'єкт стану, і тоді ви можете зробити один setStateвиклик, і буде лише один рендер.

Примітка: На відміну від setStateкомпонентів класу in, setStateповернене з useStateне об'єднує об'єкти з існуючим станом, воно повністю замінює об'єкт. Якщо ви хочете зробити об’єднання, вам потрібно буде прочитати попередній стан та об’єднати його з новими значеннями самостійно. Зверніться до документів .

Я б не надто турбувався про надмірний дзвінок на рендери, поки ви не переконаєтесь, що у вас проблема з продуктивністю. Візуалізація (у контексті 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>


1
Я згоден, що це не ідеально, але це все-таки розумний спосіб зробити це. Ви можете написати свій власний хук, який виконує об’єднання, якщо ви не хочете робити об’єднання самостійно. Я оновив останнє речення, щоб бути зрозумілішим. Чи знайомі ви з тим, як працює React? Якщо ні, ось кілька посилань, які можуть допомогти вам краще зрозуміти - responsejs.org/docs/reconciliation.html , blog.vjeux.com/2013/javascript/react-performance.html .
Yangshun Tay

2
@jonhobbs Я додав альтернативну відповідь, яка створює useMergeStateгачок, щоб ви могли автоматично об'єднати стан.
Yangshun Tay

1
Чудово, ще раз спасибі. Я більше знайомлюсь з використанням React, але я міг би багато дізнатись про його внутрішні функції. Я прочитаю їх.
jonhobbs

2
Цікаво, за яких обставин це може бути правдою ?: " React можеsetState
групувати

1
Приємно .. я думав про комбінування стану, або використання окремого стану корисного навантаження, і про те, щоб інші стани читалися з об’єкта корисного навантаження. Дуже хороший пост. 10/10 читатиметься знову
Ентоні Мун Бім Турі

63

Це також має інше рішення за допомогою 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
}

Expected 0 arguments, but got 1.ts(2554)- це помилка, яку я отримую, намагаючись використовувати useReducer таким чином. Точніше setState. Як це виправити? Файл - js, але ми все ще використовуємо перевірки ts.
ZenVentzi

Чудово, брате, дякую
Нішарг Шах

Я думаю, це все та ж ідея, що дві держави об’єдналися. Але в деяких випадках ви не можете об’єднати держави.
user1888955

45

Пакетне оновлення в реакційних гачках https://github.com/facebook/react/issues/14259

Наразі React буде оновлювати стан пакетного оновлення, якщо воно ініціюється в межах події на основі React, наприклад, натискання кнопки або зміна вводу. Він не буде пакетним оновленням, якщо вони запускаються поза обробником події React, як виклик асинхронізації.


5
Це така корисна відповідь, оскільки вона вирішує проблему, мабуть, у більшості випадків, не роблячи нічого. Дякую!
davnicwil


4

Якщо ви використовуєте сторонні хуки і не можете об’єднати стан в один об’єкт або використання useReducer, то рішення полягає у використанні:

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

Рекомендовано Даном Абрамовим тут

Див. Цей приклад


Приблизно через рік ця функція, здається, повністю недокументована і все ще позначена як нестабільна :-(
Енді

4

Для того, щоб відтворити 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 
  )
}

PS: Переконайтеся, що користувацькі хуки ставляться перед префіксомuse ( useDataзамість getData). Також переданий зворотний дзвінок не useEffectможе бути async.


1
Проголосував за це! Це одна з найбільш корисних функцій, яку я знайшов у React API. Знайшов, коли я намагався зберегти функцію як стан (я знаю, що це дивно зберігати функції, але мені довелося з якихось причин, я не пам’ятаю), і це не спрацювало
належним чином

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

Класно! Я б обернути тільки setMergedStateз useMemo(() => setMergedState, []), тому що, як кажуть документи, setStateне змінюється між повторно надає: React guarantees that setState function identity is stable and won’t change on re-renders. This is why it’s safe to omit from the useEffect or useCallback dependency list., таким чином SetState функції не відтворений на ре-робить.
tonix

0

Ви також можете використовувати useEffectдля виявлення зміни стану та відповідно оновлення інших значень стану


Чи можете ви детальніше сказати? Він уже використовує useEffect.
bluenote10
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.