Обновление и слияние объекта состояния с использованием хука React useState()

Я нахожу эти две части документации React Hooks немного запутанными. Какой из них является наилучшей практикой для обновления объекта состояния с помощью хука состояния?

Представьте, что вы хотите сделать следующее обновление состояния:

INITIAL_STATE = {
  propA: true,
  propB: true
}

stateAfter = {
  propA: true,
  propB: false   // Changing this property
}

ОПЦИЯ 1

Из статьи Использование React-хука получаем, что это возможно:

const [count, setCount] = useState(0);
setCount(count + 1);

Итак, я мог бы сделать:

const [myState, setMyState] = useState(INITIAL_STATE);

А потом:

setMyState({
  ...myState,
  propB: false
});

ВАРИАНТ 2

И из Справочник по хукам мы получаем, что:

Unlike the setState method found in class components, useState does not automatically merge update objects. You can replicate this behavior by combining the function updater form with object spread syntax:

setState(prevState => {
  // Object.assign would also work
  return {...prevState, ...updatedValues};
});

Насколько я знаю, оба работают. Итак, в чем разница? Какой из них является лучшей практикой? Должен ли я использовать функцию передачи (ВАРИАНТ 2) для доступа к предыдущему состоянию, или я должен просто получить доступ к текущему состоянию с синтаксисом распространения (ВАРИАНТ 1)?

вы путаетесь с локальным состоянием и состоянием «крючков». Они разные.

flppv 25.03.2019 17:48

Разве useState не является крючком для управления локальным состоянием? Если я не ошибаюсь, такой вещи, как состояние "хуков", не существует.

TA3 08.05.2019 08:23

Я надеюсь, что у меня есть это право для использования с вашим явным примером: setState(prevState => { return { ...prevState, propB: false } } ) Кажется, это сработало для меня! ?

gfmoore 20.09.2021 22:07

Я предпочитаю этот способ, используя useReducerstackoverflow.com/a/71093607/8798220

Nisharg Shah 12.02.2022 17:18
Поведение ключевого слова "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) для оценки ваших знаний,...
92
4
128 531
9
Перейти к ответу Данный вопрос помечен как решенный

Ответы 9

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

Оба варианта допустимы, но, как и в случае с setState в компоненте класса, вам нужно быть осторожным при обновлении состояния, полученного из того, что уже находится в состоянии.

Если вы, например. обновить счетчик два раза подряд, он не будет работать должным образом, если вы не используете версию функции обновления состояния.

const { useState } = React;

function App() {
  const [count, setCount] = useState(0);

  function brokenIncrement() {
    setCount(count + 1);
    setCount(count + 1);
  }

  function increment() {
    setCount(count => count + 1);
    setCount(count => count + 1);
  }

  return (
    <div>
      <div>{count}</div>
      <button onClick = {brokenIncrement}>Broken increment</button>
      <button onClick = {increment}>Increment</button>
    </div>
  );
}

ReactDOM.render(<App />, document.getElementById("root"));
<script src = "https://unpkg.com/react@16/umd/react.development.js"></script>
<script src = "https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>

<div id = "root"></div>

Вот ссылка на эту часть документации: reactjs.org/docs/hooks-reference.html#functional-updates

Nicolás Arévalo 23.04.2019 07:20

Да, хук useState является асинхронным, как и setState, поэтому в приведенном выше случае счетчик не обновляется до добавления следующего счетчика. Всегда используйте функциональный способ, если вы работаете с данными из состояния.

Goran Jakovljevic 24.09.2019 01:01

Когда я вставляю console.info('render') перед функцией brokenIncrement, затем нажимаю кнопку Broken increment или Increment, «рендеринг» будет напечатан один раз, кажется, что функция setCount может быть объединена в одну, поэтому компонент функции отображается один раз. Но если я изменю функцию brokenIncrement или increment , как и function brokenIncrement() { Promise.resolve().then(() => {setCount(count + 1); setCount(count + 1);}}, «рендеринг» будет напечатан дважды. Итак, можем ли мы объединить setCount в одну асинхронную функцию, как Promise.

poplark 16.09.2020 05:49

Есть ссылка на код

poplark 16.09.2020 05:50

Лучшей практикой является использование отдельных вызовов:

const [a, setA] = useState(true);
const [b, setB] = useState(true);

Вариант 1 может привести к большему количеству ошибок, потому что такой код часто оказывается внутри замыкания, которое имеет устаревшее значение myState.

Вариант 2 следует использовать, когда новое состояние основано на старом:

setCount(count => count + 1);

Для сложной структуры состояния рассмотрите возможность использования использованиередьюсер

Для сложных структур, которые имеют общую форму и логику, вы можете создать собственный хук:

function useField(defaultValue) {
  const [value, setValue] = useState(defaultValue);
  const [dirty, setDirty] = useState(false);
  const [touched, setTouched] = useState(false);

  function handleChangeEvent(e) {
    setValue(e.target.value);
    setTouched(true);
  }

  return {
    value, setValue,
    dirty, setDirty,
    touched, setTouched,
    handleChangeEvent
  }
}

function MyComponent() {
  const username = useField('some username');
  const email = useField('[email protected]');

  return <input name = "username" value = {username.value} onChange = {username.handleChangeEvent}/>;
}

Я думаю, что не могу разделить звонки. Я создаю компонент формы, и он имеет такое состояние inputs: {'username': {dirty: true, touched: true, etc}, 'email': {dirty: false, touched: false, etc}. Я закончу с огромным количеством переменных состояния. Мне действительно нужно состояние с вложенным объектом.

cbdeveloper 25.03.2019 18:27

Я обновил свой ответ. Вам нужно использовать useReducer или специальный хук.

UjinT34 25.03.2019 18:53

Оба прекрасно подходят для этого варианта использования. Функциональный аргумент, который вы передаете setState, действительно полезен только тогда, когда вы хотите условно установить состояние, изменив предыдущее состояние (я имею в виду, что вы можете просто сделать это с логикой, окружающей вызов setState, но я думаю, что это выглядит чище в функции) или если вы устанавливаете состояние в замыкании, у которого нет немедленного доступа к самой свежей версии предыдущего состояния.

Примером может служить что-то вроде прослушивателя событий, который привязывается только один раз (по какой-то причине) при монтировании к окну. Например.

useEffect(function() {
  window.addEventListener("click", handleClick)
}, [])

function handleClick() {
  setState(prevState => ({...prevState, new: true }))
}

Если бы handleClick устанавливала состояние только с помощью опции 1, это выглядело бы как setState({...prevState, new: true }). Однако это, вероятно, приведет к ошибке, потому что prevState будет фиксировать состояние только при первоначальном рендеринге, а не при каких-либо обновлениях. Аргумент функции, переданный setState, всегда будет иметь доступ к самой последней итерации вашего состояния.

В зависимости от вашего варианта использования может подойти один или несколько вариантов, касающихся типа состояния.

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

Во-первых: связаны ли отдельные состояния

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

Например, свойства пользователя, такие как name, email, связаны, и вы можете сгруппировать их вместе, тогда как для обслуживания нескольких счетчиков вы можете использовать multiple useState hooks

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

В приведенном выше случае лучше использовать useReducer для определения состояния. Такой сценарий очень распространен, когда вы пытаетесь создать, например, приложение, в котором вы хотите использовать элементы update, create и delete для различных взаимодействий.

Should I use pass the function (OPTION 2) to access the previous state, or should I simply access the current state with spread syntax (OPTION 1)?

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

Шаблон обратного вызова для обновления состояния также удобен, когда установщик не получает обновленное значение из закрытого замыкания из-за того, что оно определено только один раз. Пример такого случая, когда useEffect вызывается только при начальном рендеринге, когда добавляется прослушиватель, который обновляет состояние для события.

Which one is the best practice for updating a state object using the state hook?

Они оба действительны, как указывали другие ответы.

what is the difference?

Похоже, путаница связана с "Unlike the setState method found in class components, useState does not automatically merge update objects", особенно с частью «слияния».

Давайте сравним this.setState и useState

class SetStateApp extends React.Component {
  state = {
    propA: true,
    propB: true
  };

  toggle = e => {
    const { name } = e.target;
    this.setState(
      prevState => ({
        [name]: !prevState[name]
      }),
      () => console.info(`this.state`, this.state)
    );
  };
  ...
}

function HooksApp() {
  const INITIAL_STATE = { propA: true, propB: true };
  const [myState, setMyState] = React.useState(INITIAL_STATE);

  const { propA, propB } = myState;

  function toggle(e) {
    const { name } = e.target;
    setMyState({ [name]: !myState[name] });
  }
...
}

Оба они переключают propA/B в обработчик toggle. И они оба обновляют только один реквизит, переданный как e.target.name.

Посмотрите, какая разница, когда вы обновляете только одно свойство в setMyState.

Следующая демонстрация показывает, что нажатие на propA вызывает ошибку (которая возникает только setMyState),

Вы можете следить за

Edit nrrjqj30wp

Warning: A component is changing a controlled input of type checkbox to be uncontrolled. Input elements should not switch from controlled to uncontrolled (or vice versa). Decide between using a controlled or uncontrolled input element for the lifetime of the component.

error demo

Это потому, что когда вы нажимаете на флажок propA, значение propB сбрасывается, и переключается только значение propA, что делает значение propBchecked неопределенным, что делает флажок неконтролируемым.

И this.setState обновляет только одно свойство за раз, но это merges другое свойство, поэтому флажки остаются под контролем.


Я копался в исходном коде, и поведение связано с useState вызовом useReducer

Внутри useState вызывает useReducer, который возвращает любое состояние, возвращаемое редюсером.

https://github.com/facebook/react/blob/2b93d686e3/packages/react-reconciler/src/ReactFiberHooks.js#L1230

    useState<S>(
      initialState: (() => S) | S,
    ): [S, Dispatch<BasicStateAction<S>>] {
      currentHookNameInDev = 'useState';
        ...
      try {
        return updateState(initialState);
      } finally {
        ...
      }
    },

где updateState — внутренняя реализация для useReducer.

function updateState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  return updateReducer(basicStateReducer, (initialState: any));
}

    useReducer<S, I, A>(
      reducer: (S, A) => S,
      initialArg: I,
      init?: I => S,
    ): [S, Dispatch<A>] {
      currentHookNameInDev = 'useReducer';
      updateHookTypesDev();
      const prevDispatcher = ReactCurrentDispatcher.current;
      ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
      try {
        return updateReducer(reducer, initialArg, init);
      } finally {
        ReactCurrentDispatcher.current = prevDispatcher;
      }
    },

Если вы знакомы с Redux, вы обычно возвращаете новый объект, распространяя его на предыдущее состояние, как в варианте 1.

setMyState({
  ...myState,
  propB: false
});

Таким образом, если вы установите только одно свойство, другие свойства не будут объединены.

Оба варианта допустимы, но они имеют значение. Используйте вариант 1 (setCount(count + 1)), если

  1. Свойство визуально не имеет значения, когда оно обновляет браузер
  2. Пожертвуйте частотой обновления ради производительности
  3. Обновление состояния ввода на основе события (т.е. event.target.value); если вы используете вариант 2, он установит для события значение null из-за соображений производительности, если у вас нет event.persist() — см. объединение событий.

Используйте вариант 2 (setCount(c => c + 1)), если

  1. Свойство имеет значение, когда оно обновляется в браузере
  2. Пожертвуйте производительностью ради лучшей частоты обновления

Я заметил эту проблему, когда некоторые оповещения с функцией автоматического закрытия, которые должны закрываться последовательно, закрывались пакетами.

Примечание. У меня нет статистики, подтверждающей разницу в производительности, но она основана на конференции React по оптимизации производительности React 16.

Если кто-то ищет обновление хуков использовать состояние() для объект

  • Через ввод

    const [state, setState] = useState({ fName: "", lName: "" });
    const handleChangeEvent = e => {
        const { name, value } = e.target;
        setState(prevState => ({
            ...prevState,
            [name]: value
        }));
    };
    
    <input
        value = {state.fName}
        type = "text"
        onChange = {handleChangeEvent}
        name = "fName"
    />
    <input
        value = {state.lName}
        type = "text"
        onChange = {handleChangeEvent}
        name = "lName"
    />
    
  • Через onSubmit или нажатие кнопки

        setState(prevState => ({
           ...prevState,
           fName: 'your updated value here'
        }));
    

Огромное спасибо

EvanPoe 07.10.2021 21:45

Решение, которое я собираюсь предложить, это намного проще и легче, чтобы не испортить то, что указано выше, и имеет такое же использование, как useState API.

Используйте пакет npm use-merge-state (здесь). Добавьте его в свои зависимости, а затем используйте его как:

const useMergeState = require("use-merge-state") // Import
const [state, setState] = useMergeState(initial_state, {merge: true}) // Declare
setState(new_state) // Just like you set a new state with 'useState'

Надеюсь, это поможет всем. :)

Мне очень удобно использовать useReducer хук для управления сложным состоянием вместо od useState. Вы инициализируете состояние и функцию обновления следующим образом:

const initialState = { name: "Bob", occupation: "builder" };
const [state, updateState] = useReducer(
  (state, updates) => ({
    ...state,
    ...updates,
  }),
  initialState
);

И тогда вы сможете обновить свое состояние, передав только частичные обновления:

updateState({ ocupation: "postman" })

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