Множественные вызовы средства обновления состояния из 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 };
};

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

Если у вас есть зависимые состояния, вам, вероятно, следует использовать useReducer

thinklinux 18.11.2019 12:29

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

Andy 03.07.2020 11:33

«кажется довольно произвольным, что поведение в асинхронном обратном вызове отличается от поведения в обычном обработчике событий» - это не произвольно, а скорее по реализации [1]. React пакетирует все вызовы setState, выполняемые во время обработчика событий React, и применяет их непосредственно перед выходом из собственного обработчика событий браузера. Однако несколько setStates вне обработчиков событий (например, в сетевых ответах) не будут объединены в пакет. Так что в этом случае вы получите два повторных рендера. [1] github.com/facebook/react/issues/10231#issuecomment-31664495‌ 0

soumyadityac 12.02.2021 18:47

«но« способ перехвата », кажется, состоит в том, чтобы разделить состояние на более мелкие единицы» - это немного вводит в заблуждение, потому что множественные повторные отрисовки происходят только тогда, когда функции setX вызываются в рамках асинхронного обратного вызова. Источники: github.com/facebook/react/issues/14259#issuecomment-43963262‌ 2, blog.logrocket.com/…

allieferr 11.11.2021 03:08
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
В настоящее время производительность загрузки веб-сайта имеет решающее значение не только для удобства пользователей, но и для ранжирования в...
Безумие обратных вызовов в javascript [JS]
Безумие обратных вызовов в javascript [JS]
Здравствуйте! Юный падаван 🚀. Присоединяйся ко мне, чтобы разобраться в одной из самых запутанных концепций, когда вы начинаете изучать мир...
Система управления парковками с использованием HTML, CSS и JavaScript
Система управления парковками с использованием HTML, CSS и JavaScript
Веб-сайт по управлению парковками был создан с использованием HTML, CSS и JavaScript. Это простой сайт, ничего вычурного. Основная цель -...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
182
4
136 727
9
Перейти к ответу Данный вопрос помечен как решенный

Ответы 9

Ответ принят как подходящий

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

Примечание: В отличие от setState в компонентах класса, 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/[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>

Спасибо, Яншун, я думаю, что слияние состояния - это ответ, но не идеальный. Я просто не уверен насчет твоего последнего предложения. Если у меня есть datatable, и он дважды отображается с одними и теми же данными, разве все строки и их дочерние элементы не отображаются и обновляются в DOM дважды? Не уверен, что вы имели в виду под пакетной обработкой, но я исхожу из фона Angular 1.x, где все постоянно перерисовывается :)

jonhobbs 01.12.2018 22:06

Я согласен, что это не идеально, но все же это разумный способ сделать это. Вы можете написать свой собственный хук, который выполняет слияние, если вы не хотите выполнять слияние самостоятельно. Я обновил последнее предложение, чтобы было понятнее. Вы знакомы с тем, как работает React? Если нет, вот несколько ссылок, которые могут помочь вам лучше понять - responsejs.org/docs/reconciliation.html, blog.vjeux.com/2013/javascript/react-performance.html.

Yangshun Tay 01.12.2018 22:12

@jonhobbs Я добавил альтернативный ответ, который создает ловушку useMergeState, чтобы вы могли автоматически слить состояние.

Yangshun Tay 01.12.2018 22:22

Превосходно, еще раз спасибо. Я все больше знаком с React, но могу многое узнать о его внутреннем устройстве. Я их прочту.

jonhobbs 01.12.2018 23:23

Хорошее решение, я использую его немного модифицированным способом, см. stackoverflow.com/a/55102681/121143

mschayna 11.03.2019 14:17

Любопытно, при каких обстоятельствах это может быть правдой ?: "React может выполнять пакетные вызовы setState и обновлять DOM браузера с последним новым состоянием."

ecoe 25.09.2019 15:23

Хорошо ... я подумал об объединении состояния или использовании отдельного состояния полезной нагрузки, а также о том, чтобы другие состояния считывались из объекта полезной нагрузки. Тем не менее, очень хороший пост. 10/10 прочитаю еще раз

Anthony Moon Beam Toorie 03.08.2020 06:11

Пакетное обновление в react-hookshttps://github.com/facebook/react/issues/14259

React currently will batch state updates if they're triggered from within a React-based event, like a button click or input change. It will not batch updates if they're triggered outside of a React event handler, like an async call.

Это очень полезный ответ, поскольку он решает проблему, вероятно, в большинстве случаев, без необходимости что-либо делать. Спасибо!

davnicwil 18.02.2020 15:23

Обновление: начиная с React 18 (функция подписки) все обновления будут автоматически пакетироваться, независимо от того, откуда они исходят. github.com/reactwg/react-18/discussions/21

Sureshraj 09.06.2021 11:01

Небольшое дополнение к ответу 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 27.12.2019 18:11

Это также имеет другое решение, использующее 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
}

Поддержка машинописного текста. Спасибо П. Гэлбрейт, который ответил на это решение, Те, кто использует машинописный текст, могут использовать это:

useReducer<Reducer<MyState, Partial<MyState>>>(...)

где MyState - это тип вашего объекта состояния.

например В нашем случае это будет так:

interface MyState {
   loading: boolean;
   data: any;
   something: string;
}

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

Предыдущая государственная поддержка. В комментариях user2420374 попросил предоставить доступ к prevState внутри нашего setState, так что вот способ достижения этой цели:

const [state, setState] = useReducer(
    (state, newState) => {
        newWithPrevState = isFunction(newState) ? newState(state) : newState
        return (
            {...state, ...newWithPrevState}
        )
     },
     initialState
)

// And then use it like this...
setState(prevState => {...})

isFunction проверяет, является ли переданный аргумент функцией (что означает, что вы пытаетесь получить доступ к prevState) или простым объектом. Вы можете найти эта реализация isFunction Алекса Гранде здесь.


Уведомление. Для тех, кто хочет много использовать этот ответ, я решил превратить его в библиотеку. Вы можете найти это здесь:

Github: https://github.com/thevahidal/react-use-setstate

NPM: https://www.npmjs.com/package/react-use-setstate

Expected 0 arguments, but got 1.ts(2554) - это ошибка, которую я получаю, пытаясь использовать useReducer таким образом. Точнее setState. Как это исправить? Файл - js, но мы по-прежнему используем проверки ts.
ZenVentzi 01.02.2020 19:38

Я думаю, что идея слияния двух государств осталась прежней. Но в некоторых случаях вы не можете объединить состояния.

user1888955 15.08.2020 23:49

Я думаю, что это отличный ответ в сочетании с пакетным обновлением хуков React, опубликованным ниже (github.com/facebook/react/issues/14259). К счастью, мой вариант использования был прост, и я смог просто переключиться на useReducer с useState, но в других случаях (например, в некоторых, перечисленных в выпуске GitHub) это не так просто.

cjones26 09.02.2021 20:52

Для тех, кто использует машинописный код useReducer<Reducer<MyState, Partial<MyState>>>(...), где MyState - это тип вашего объекта состояния.

P. Galbraith 05.05.2021 03:09

Спасибо, приятель, это действительно помогло мне. Также спасибо @ P.Galbraith за решение TypeScript :)

Johan Jarvi 17.05.2021 05:29

Я искал реализовать это решение, но как мы можем получить доступ к предыдущему состоянию точно так же, как функция this.setState компонента на основе класса? вот так: this.setState (prevState => {})

user2420374 06.09.2021 04:24

@ user2420374 добавил новое обновление по вашему вопросу, надеюсь, еще не поздно!

Vahid Al 16.09.2021 14:57

@VahidAl, в итоге я реализовал setState с оператором распространения, но с этого момента я выберу эту реализацию, спасибо за обновление!

user2420374 08.10.2021 23:27

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

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

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

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

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

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

Смотрите это пример

Почти год спустя эта функция все еще кажется полностью недокументированной и все еще отмечена как нестабильная :-(

Andy 03.07.2020 11:13

Чтобы воспроизвести поведение слияния this.setState из компонентов класса, React docs рекомендовать для использования функциональной формы 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.

Проголосовал за это! Это одна из самых полезных функций, которые я нашел в React API. Обнаружил его, когда попытался сохранить функцию как состояние (я знаю, что хранить функции странно, но мне пришлось по какой-то причине, которую я не помню), и это не сработало должным образом из-за этой зарезервированной сигнатуры вызова

elquimista 27.04.2020 11:26

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

Вы можете уточнить? Он уже пользуется useEffect.

bluenote10 27.11.2020 23:30

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

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