Spec-Zone.ru › React

Обновление объектов в состоянии

Состояние может содержать любой тип JavaScript-значения, включая объекты. Но вы не должны изменять объекты, хранящиеся в состоянии React, напрямую. Вместо этого, когда вам нужно обновить объект, вам необходимо создать новый (или скопировать существующий), а затем установить состояние, используя эту копию.

Вы узнаете

  • Как правильно обновить объект в состоянии React
  • Как обновить вложенный объект, не изменяя его
  • Что такое неизменяемость и как её не нарушить
  • Как уменьшить повторение копирования объектов с помощью Immer

Что такое мутация?

Вы можете хранить любые типы JavaScript-значений в состоянии.

const [x, setX] = useState(0);

До сих пор вы работали с числами, строками и булевыми значениями. Эти типы JavaScript-значений являются «неизменяемыми», то есть неизменяемыми или «только для чтения». Вы можете вызвать перерисовку, чтобы заменить значение:

setX(5);

Состояние изменилось с 0 на 5, но само число 0 не изменилось. Невозможно внести какие-либо изменения в встроенные примитивные значения, такие как числа, строки и булевы значения в JavaScript.

Теперь рассмотрим объект в состоянии:

const [position, setPosition] = useState({ x: 0, y: 0 });

Технически, можно изменить содержимое самого объекта. Это называется мутацией:

position.x = 5;

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

Обращайтесь к состоянию как к только для чтения

Другими словами, вы должны обращаться с любым JavaScript-объектом, который вы помещаете в состояние, как с объектом только для чтения.

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

import { useState } from 'react';

export default function MovingDot() {
  const [position, setPosition] = useState({
    x: 0,
    y: 0
  });
  return (
    <div
      onPointerMove={e => {
        position.x = e.clientX;
        position.y = e.clientY;
      }}
      style={{
        position: 'relative',
        width: '100vw',
        height: '100vh',
      }}>
      <div style={{
        position: 'absolute',
        backgroundColor: 'red',
        borderRadius: '50%',
        transform: `translate(${position.x}px, ${position.y}px)`,
        left: -10,
        top: -10,
        width: 20,
        height: 20,
      }} />
    </div>
  );
}

Проблема в этой части кода.

onPointerMove={e => {
  position.x = e.clientX;
  position.y = e.clientY;
}}

Этот код изменяет объект, назначенный position, с предыдущего рендеринга. Но без использования функции установки состояния React не знает, что объект изменился. Таким образом, React ничего не делает в ответ. Это как пытаться изменить порядок после того, как вы уже поели. Хотя изменение состояния может работать в некоторых случаях, мы не рекомендуем этого делать. Вы должны рассматривать значение состояния, к которому у вас есть доступ во время рендеринга, как неизменяемое.

Чтобы фактически вызвать перерисовку в этом случае, создайте новый объект и передайте его в функцию установки состояния:

onPointerMove={e => {
  setPosition({
    x: e.clientX,
    y: e.clientY
  });
}}

С помощью setPosition, вы говорите React:

  • Заменить position этим новым объектом
  • И снова отрисовать этот компонент

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

import { useState } from 'react';

export default function MovingDot() {
  const [position, setPosition] = useState({
    x: 0,
    y: 0
  });
  return (
    <div
      onPointerMove={e => {
        setPosition({
          x: e.clientX,
          y: e.clientY
        });
      }}
      style={{
        position: 'relative',
        width: '100vw',
        height: '100vh',
      }}>
      <div style={{
        position: 'absolute',
        backgroundColor: 'red',
        borderRadius: '50%',
        transform: `translate(${position.x}px, ${position.y}px)`,
        left: -10,
        top: -10,
        width: 20,
        height: 20,
      }} />
    </div>
  );
}

Подробное изучение

Локальное изменение в порядке вещей

Код такого вида является проблемой, потому что он изменяет существующий объект в состоянии:

position.x = e.clientX;
position.y = e.clientY;

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

const nextPosition = {};
nextPosition.x = e.clientX;
nextPosition.y = e.clientY;
setPosition(nextPosition);

Фактически, это полностью эквивалентно написанию этого:

setPosition({
  x: e.clientX,
  y: e.clientY
});

Изменение — это проблема только тогда, когда вы изменяете существующие объекты, которые уже находятся в состоянии. Изменение объекта, который вы только что создали, допустимо, потому что ни другой код на него не ссылается. Изменение его не повлияет случайно на то, что зависит от него. Это называется «локальным изменением». Вы можете даже производить локальные изменения во время рендеринга. Очень удобно и совершенно допустимо!

Копирование объектов с помощью синтаксиса spread

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

Эти поля ввода не работают, потому что обработчики onChange изменяют состояние:

import { useState } from 'react';

export default function Form() {
  const [person, setPerson] = useState({
    firstName: 'Barbara',
    lastName: 'Hepworth',
    email: 'bhepworth@sculpture.com'
  });

  function handleFirstNameChange(e) {
    person.firstName = e.target.value;
  }

  function handleLastNameChange(e) {
    person.lastName = e.target.value;
  }

  function handleEmailChange(e) {
    person.email = e.target.value;
  }

  return (
    <>
      <label>
        First name:
        <input
          value={person.firstName}
          onChange={handleFirstNameChange}
        />
      </label>
      <label>
        Last name:
        <input
          value={person.lastName}
          onChange={handleLastNameChange}
        />
      </label>
      <label>
        Email:
        <input
          value={person.email}
          onChange={handleEmailChange}
        />
      </label>
      <p>
        {person.firstName}{' '}
        {person.lastName}{' '}
        ({person.email})
      </p>
    </>
  );
}

Например, эта строка изменяет состояние с предыдущего рендеринга:

person.firstName = e.target.value;

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

setPerson({
  firstName: e.target.value, // New first name from the input
  lastName: person.lastName,
  email: person.email
});

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

setPerson({
  ...person, // Copy the old fields
  firstName: e.target.value // But override this one
});

Теперь форма работает!

Обратите внимание, что вы не объявляли отдельной переменной состояния для каждого поля ввода. Для больших форм хранение всех данных в объекте очень удобно — при условии правильного обновления!

import { useState } from 'react';

export default function Form() {
  const [person, setPerson] = useState({
    firstName: 'Barbara',
    lastName: 'Hepworth',
    email: 'bhepworth@sculpture.com'
  });

  function handleFirstNameChange(e) {
    setPerson({
      ...person,
      firstName: e.target.value
    });
  }

  function handleLastNameChange(e) {
    setPerson({
      ...person,
      lastName: e.target.value
    });
  }

  function handleEmailChange(e) {
    setPerson({
      ...person,
      email: e.target.value
    });
  }

  return (
    <>
      <label>
        First name:
        <input
          value={person.firstName}
          onChange={handleFirstNameChange}
        />
      </label>
      <label>
        Last name:
        <input
          value={person.lastName}
          onChange={handleLastNameChange}
        />
      </label>
      <label>
        Email:
        <input
          value={person.email}
          onChange={handleEmailChange}
        />
      </label>
      <p>
        {person.firstName}{' '}
        {person.lastName}{' '}
        ({person.email})
      </p>
    </>
  );
}

Обратите внимание, что синтаксис ... распространения «плоский» — он копирует только элементы на один уровень вглубь. Это делает его быстрым, но это также означает, что если вы хотите обновить вложенное свойство, вам придется использовать его несколько раз.

Подробное изучение

Использование одного обработчика событий для нескольких полей

Вы также можете использовать [ и ] фигурные скобки внутри вашего определения объекта, чтобы указать свойство с динамическим именем. Вот тот же пример, но с одним обработчиком событий вместо трёх разных:

import { useState } from 'react';

export default function Form() {
  const [person, setPerson] = useState({
    firstName: 'Barbara',
    lastName: 'Hepworth',
    email: 'bhepworth@sculpture.com'
  });

  function handleChange(e) {
    setPerson({
      ...person,
      [e.target.name]: e.target.value
    });
  }

  return (
    <>
      <label>
        First name:
        <input
          name="firstName"
          value={person.firstName}
          onChange={handleChange}
        />
      </label>
      <label>
        Last name:
        <input
          name="lastName"
          value={person.lastName}
          onChange={handleChange}
        />
      </label>
      <label>
        Email:
        <input
          name="email"
          value={person.email}
          onChange={handleChange}
        />
      </label>
      <p>
        {person.firstName}{' '}
        {person.lastName}{' '}
        ({person.email})
      </p>
    </>
  );
}

Здесь e.target.name относится к свойству name, заданному элементу <input> DOM.

Обновление вложенного объекта

Рассмотрим структуру вложенного объекта такого типа:

const [person, setPerson] = useState({
  name: 'Niki de Saint Phalle',
  artwork: {
    title: 'Blue Nana',
    city: 'Hamburg',
    image: 'https://i.imgur.com/Sd1AgUOm.jpg',
  }
});

Если вам нужно обновить person.artwork.city, ясно, как это сделать с помощью мутации:

person.artwork.city = 'New Delhi';

Но в React состояние считается неизменяемым! Для изменения city, вам сначала нужно создать новый объект artwork (заполненный данными из предыдущего), а затем создать новый объект person, который ссылается на новый artwork:

const nextArtwork = { ...person.artwork, city: 'New Delhi' };
const nextPerson = { ...person, artwork: nextArtwork };
setPerson(nextPerson);

Или, записанное как одна функция вызова:

setPerson({
  ...person, // Copy other fields
  artwork: { // but replace the artwork
    ...person.artwork, // with the same one
    city: 'New Delhi' // but in New Delhi!
  }
});

Это немного громоздко, но работает хорошо во многих случаях:

import { useState } from 'react';

export default function Form() {
  const [person, setPerson] = useState({
    name: 'Niki de Saint Phalle',
    artwork: {
      title: 'Blue Nana',
      city: 'Hamburg',
      image: 'https://i.imgur.com/Sd1AgUOm.jpg',
    }
  });

  function handleNameChange(e) {
    setPerson({
      ...person,
      name: e.target.value
    });
  }

  function handleTitleChange(e) {
    setPerson({
      ...person,
      artwork: {
        ...person.artwork,
        title: e.target.value
      }
    });
  }

  function handleCityChange(e) {
    setPerson({
      ...person,
      artwork: {
        ...person.artwork,
        city: e.target.value
      }
    });
  }

  function handleImageChange(e) {
    setPerson({
      ...person,
      artwork: {
        ...person.artwork,
        image: e.target.value
      }
    });
  }

  return (
    <>
      <label>
        Name:
        <input
          value={person.name}
          onChange={handleNameChange}
        />
      </label>
      <label>
        Title:
        <input
          value={person.artwork.title}
          onChange={handleTitleChange}
        />
      </label>
      <label>
        City:
        <input
          value={person.artwork.city}
          onChange={handleCityChange}
        />
      </label>
      <label>
        Image:
        <input
          value={person.artwork.image}
          onChange={handleImageChange}
        />
      </label>
      <p>
        <i>{person.artwork.title}</i>
        {' by '}
        {person.name}
        <br />
        (located in {person.artwork.city})
      </p>
      <img 
        src={person.artwork.image} 
        alt={person.artwork.title}
      />
    </>
  );
}

Подробное изучение

Объекты на самом деле не вложены

Объект такого вида кажется «вложенным» в коде:

let obj = {
  name: 'Niki de Saint Phalle',
  artwork: {
    title: 'Blue Nana',
    city: 'Hamburg',
    image: 'https://i.imgur.com/Sd1AgUOm.jpg',
  }
};

Однако «вложенность» — это неточное представление о поведении объектов. При выполнении кода нет такого понятия, как «вложенный» объект. Вы на самом деле смотрите на два разных объекта:

let obj1 = {
  title: 'Blue Nana',
  city: 'Hamburg',
  image: 'https://i.imgur.com/Sd1AgUOm.jpg',
};

let obj2 = {
  name: 'Niki de Saint Phalle',
  artwork: obj1
};

Объект obj1 не «внутри» obj2. Например, obj3 может «указывать» на obj1 тоже:

let obj1 = {
  title: 'Blue Nana',
  city: 'Hamburg',
  image: 'https://i.imgur.com/Sd1AgUOm.jpg',
};

let obj2 = {
  name: 'Niki de Saint Phalle',
  artwork: obj1
};

let obj3 = {
  name: 'Copycat',
  artwork: obj1
};

Если вы изменили бы obj3.artwork.city, это затронуло бы как obj2.artwork.city, так и obj1.city. Это потому, что obj3.artwork, obj2.artwork, и obj1 — это один и тот же объект. Это сложно увидеть, когда вы думаете об объектах как о «вложенных». Вместо этого они являются отдельными объектами, «указывая» друг на друга с помощью свойств.

Напишите лаконичный логику обновления с Immer

Если ваше состояние глубоко вложено, вы можете рассмотреть сглаживание его. Но если вы не хотите изменять структуру состояния, вы можете предпочесть сокращение для вложенных spread-операций. Immer — это популярная библиотека, которая позволяет писать с помощью удобного, но изменяемого синтаксиса и заботится о создании копий за вас. С Immer код выглядит так, как будто вы «нарушаете правила» и изменяете объект:

updatePerson(draft => {
  draft.artwork.city = 'Lagos';
});

Но в отличие от обычной мутации, она не перезаписывает прошлое состояние!

Подробное изучение

Как работает Immer?

Обеспечиваемый Immer — это особый тип объекта, называемый прокси, который «записывает» то, что вы с ним делаете. Поэтому вы можете изменять его сколько угодно свободно! Под капотом Immer определяет, какие части draft были изменены и создаёт совершенно новый объект, содержащий ваши правки.

Чтобы попробовать Immer:

  1. Выполните npm install use-immer для добавления Immer в качестве зависимости
  2. Затем замените import { useState } from 'react' на import { useImmer } from 'use-immer'

Вот приведенный выше пример, преобразованный в Immer:

{
  "dependencies": {
    "immer": "1.7.3",
    "react": "latest",
    "react-dom": "latest",
    "react-scripts": "latest",
    "use-immer": "0.5.1"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test --env=jsdom",
    "eject": "react-scripts eject"
  },
  "devDependencies": {}
}

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

Глубокое погружение

Почему изменение состояния не рекомендуется в React?

Есть несколько причин:

  • Отладка: Если вы используете console.log и не изменяете состояние, ваши прошлые логи не будут перезаписаны более поздними изменениями состояния. Таким образом, вы можете четко увидеть, как состояние изменилось между рендерами.
  • Оптимизации: Общие стратегии оптимизации React стратегии оптимизации полагаются на пропуск работы, если предыдущие props или состояние совпадают со следующими. Если вы никогда не изменяете состояние, очень быстро проверить, были ли какие-либо изменения. Если prevObj === obj, вы можете быть уверены, что ничего не могло измениться внутри него.
  • Новые возможности: Новые возможности React, над которыми мы работаем, полагаются на то, что состояние рассматривается как снимок. Если вы изменяете прошлые версии состояния, это может помешать вам использовать новые возможности.
  • Изменения требований: Некоторые функции приложения, такие как реализация функций «Отменить/Повторить», отображение истории изменений или возможность пользователя сбросить форму до предыдущих значений, проще реализовать, когда ничего не изменяется. Это связано с тем, что вы можете сохранять прошлые копии состояния в памяти и повторно использовать их при необходимости. Если вы начинаете с мутативного подхода, такие функции, как эта, могут быть трудно добавить позже.
  • Более простая реализация: Поскольку React не полагается на мутацию, ему не нужно делать ничего особенного с вашими объектами. Ему не нужно вмешиваться в их свойства, всегда оборачивать их в Proxy или выполнять другую работу при инициализации, как это делают многие «реактивные» решения. Именно поэтому React позволяет помещать в состояние любой объект — независимо от его размера — без дополнительных проблем с производительностью или корректностью.

На практике вы часто можете «пройти мимо» изменения состояния в React, но мы настоятельно рекомендуем этого не делать, чтобы вы могли использовать новые возможности React, разработанные с учетом этого подхода. Будущие разработчики, а возможно, и вы сами в будущем поблагодарят вас за это!

Резюме

  • Обращайтесь со всем состоянием в React как с неизменяемым.
  • Когда вы храните объекты в состоянии, изменение их не вызовет перерисовки и изменит состояние в предыдущих «снимках» рендеринга.
  • Вместо изменения объекта создайте новую версию и запустите повторный рендеринг, установив новое состояние.
  • Вы можете использовать синтаксис разброса объектов {...obj, something: 'newValue'} для создания копий объектов.
  • Синтаксис разброса — поверхностный: он копирует только один уровень вглубь.
  • Для обновления вложенного объекта вам нужно создать копии по всей цепочке от места, которое вы обновляете.
  • Для сокращения повторяющегося кода копирования используйте Immer.

Попробуйте решить некоторые задачи

Задача 1 из 3:
Исправить неправильные обновления состояния

В этой форме есть несколько ошибок. Нажмите кнопку, увеличивающую счет несколько раз. Обратите внимание, что он не увеличивается. Затем измените имя, и обратите внимание, что счет внезапно «догнал» ваши изменения. Наконец, измените фамилию, и обратите внимание, что счет полностью исчез.

Ваша задача — исправить все эти ошибки. По мере их исправления объясните, почему каждая из них происходит.

import { useState } from 'react';

export default function Scoreboard() {
  const [player, setPlayer] = useState({
    firstName: 'Ranjani',
    lastName: 'Shettar',
    score: 10,
  });

  function handlePlusClick() {
    player.score++;
  }

  function handleFirstNameChange(e) {
    setPlayer({
      ...player,
      firstName: e.target.value,
    });
  }

  function handleLastNameChange(e) {
    setPlayer({
      lastName: e.target.value
    });
  }

  return (
    <>
      <label>
        Score: <b>{player.score}</b>
        {' '}
        <button onClick={handlePlusClick}>
          +1
        </button>
      </label>
      <label>
        First name:
        <input
          value={player.firstName}
          onChange={handleFirstNameChange}
        />
      </label>
      <label>
        Last name:
        <input
          value={player.lastName}
          onChange={handleLastNameChange}
        />
      </label>
    </>
  );
}

© 2013–present Facebook Inc.
Licensed under the Creative Commons Attribution 4.0 International Public License.
https://react.dev/learn/updating-objects-in-state

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API