Spec-Zone.ru › React

Компоненты и хуки должны быть чистыми

Чистые функции выполняют только вычисления и ничего больше. Это делает ваш код более понятным, удобным для отладки и позволяет React автоматически оптимизировать ваши компоненты и хуки правильно.

Примечание

Эта справочная страница охватывает продвинутые темы и требует знакомства с концепциями, рассмотренными на странице Поддержание чистоты компонентов.

  • Почему чистота важна?
  • Компоненты и хуки должны быть идемпотентными
  • Побочные эффекты должны выполняться вне рендеринга
    • Когда разрешается мутация?
  • Свойства и состояние являются неизменяемыми
    • Не изменяйте свойства
    • Не изменяйте состояние
  • Возвращаемые значения и аргументы для хуков являются неизменяемыми
  • Значения неизменяемы после передачи в JSX

Почему чистота важна?

Одна из ключевых концепций, которые делают React, React это чистота. Чистый компонент или хук — это компонент, который:

  • Идемпотентен – Вы всегда получаете тот же результат каждый раз, когда запускаете его с одинаковыми входными данными — свойства, состояние, контекст для входных данных компонента; и аргументы для входных данных хука.
  • Не имеет побочных эффектов в рендеринге – Код с побочными эффектами должен выполняться отдельно от рендеринга. Например, как обработчик событий — где пользователь взаимодействует с пользовательским интерфейсом и вызывает обновление; или как эффект — который выполняется после рендеринга.
  • Не изменяет значения, не являющиеся локальными: Компоненты и хуки должны никогда не изменять значения, которые не созданы локально в процессе рендеринга.

Когда рендеринг остается чистым, React может понять, как приоритезировать обновления, которые наиболее важны для пользователя, чтобы увидеть их в первую очередь. Это становится возможным благодаря чистоте рендеринга: так как компоненты не имеют побочных эффектов в процессе рендеринга, React может приостановить рендеринг компонентов, которые не так важны для обновления, и вернуться к ним позже, когда это потребуется.

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

Как React выполняет ваш код?

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

Рендеринг относится к вычислению того, как должен выглядеть следующий вариант вашего пользовательского интерфейса. После рендеринга эффекты сбрасываются (то есть они выполняются до тех пор, пока их больше нет) и могут обновить вычисление, если эффекты влияют на макет. React берет это новое вычисление и сравнивает его с вычислением, использованным для создания предыдущей версии вашего пользовательского интерфейса, затем фиксирует только минимальные изменения, необходимые в DOM (что фактически видит пользователь), чтобы привести его к последней версии.

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

Как определить, выполняется ли код в процессе рендеринга

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

function Dropdown() {
  const selectedItems = new Set(); // created during render
  // ...
}

Обработчики событий и эффекты не выполняются в процессе рендеринга:

function Dropdown() {
  const selectedItems = new Set();
  const onSelect = (item) => {
    // this code is in an event handler, so it's only run when the user triggers this
    selectedItems.add(item);
  }
}
function Dropdown() {
  const selectedItems = new Set();
  useEffect(() => {
    // this code is inside of an Effect, so it only runs after rendering
    logForAnalytics(selectedItems);
  }, [selectedItems]);
}

Компоненты и хуки должны быть идемпотентными

Компоненты всегда должны возвращать тот же результат относительно своих входных данных — свойств, состояния и контекста. Это называется идемпотентностью. Идемпотентность — термин, популяризированный в функциональном программировании. Он относится к идее, что вы всегда получаете тот же результат каждый раз, когда запускаете этот фрагмент кода с одинаковыми входными данными.

Это означает, что весь код, который выполняется во время рендеринга, также должен быть идемпотентным, чтобы это правило соблюдалось. Например, эта строка кода не является идемпотентной (и, следовательно, не является идемпотентным и компонент):

function Clock() {
  const time = new Date(); // 🔴 Bad: always returns a different result!
  return <span>{time.toLocaleString()}</span>
}

new Date() не является идемпотентным, так как он всегда возвращает текущую дату и изменяет свой результат каждый раз, когда вызывается. Когда вы отображаете вышеприведенный компонент, отображаемое время будет застревать на времени, в котором был отображен компонент. Точно так же функции, такие как Math.random() также не являются идемпотентными, потому что они возвращают разные результаты каждый раз, когда они вызываются, даже когда входные данные одинаковы.

Это не означает, что вам не следует использовать неидемпотентные функции, такие как new Date() вообще — вам просто следует избегать их использования во время рендеринга. В этом случае мы можем синхронизировать последнее время с этим компонентом, используя эффект:

import { useState, useEffect } from 'react';

function useTime() {
  // 1. Keep track of the current date's state. `useState` receives an initializer function as its
  //    initial state. It only runs once when the hook is called, so only the current date at the
  //    time the hook is called is set first.
  const [time, setTime] = useState(() => new Date());

  useEffect(() => {
    // 2. Update the current date every second using `setInterval`.
    const id = setInterval(() => {
      setTime(new Date()); // ✅ Good: non-idempotent code no longer runs in render
    }, 1000);
    // 3. Return a cleanup function so we don't leak the `setInterval` timer.
    return () => clearInterval(id);
  }, []);

  return time;
}

export default function Clock() {
  const time = useTime();
  return <span>{time.toLocaleString()}</span>;
}

Оборачивая неидемпотентный вызов new Date() в эффект, мы перемещаем это вычисление вне процесса рендеринга.

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


Побочные эффекты должны выполняться вне процесса рендеринга

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

Примечание

Побочные эффекты — это более широкое понятие, чем эффекты. Эффекты конкретно относятся к коду, который заключён в useEffect, в то время как побочный эффект — это общее понятие для кода, который оказывает какое-либо наблюдаемое влияние, помимо своего основного результата возвращения значения вызывающей стороне.

Побочные эффекты обычно записываются внутри обработчиков событий или эффектов. Но никогда не во время рендеринга.

Хотя рендеринг должен оставаться чистым, побочные эффекты необходимы в какой-то момент, чтобы ваше приложение выполняло какие-либо интересные действия, например, отображало что-либо на экране! Ключевая точка этого правила заключается в том, что побочные эффекты не должны выполняться в процессе рендеринга, поскольку React может многократно рендерить компоненты. В большинстве случаев вы будете использовать обработчики событий для обработки побочных эффектов. Использование обработчика событий явно сообщает React, что этот код не должен выполняться во время рендеринга, сохраняя рендеринг чистым. Если вы исчерпали все варианты — и только в крайнем случае — вы также можете обработать побочные эффекты с помощью useEffect.

Когда разрешается мутация?

Локальная мутация

Один распространенный пример побочного эффекта — это мутация, которая в JavaScript относится к изменению значения не-примитивного значения. В общем случае, хотя мутация не является стандартной практикой в React, локальная мутация абсолютно допустима:

function FriendList({ friends }) {
  const items = []; // ✅ Good: locally created
  for (let i = 0; i < friends.length; i++) {
    const friend = friends[i];
    items.push(
      <Friend key={friend.id} friend={friend} />
    ); // ✅ Good: local mutation is okay
  }
  return <section>{items}</section>;
}

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

Даже если кажется, что мы изменяем items, ключевым моментом является то, что этот код делает это только локально — мутация не «запоминается» при повторном рендеринге компонента. Другими словами, items существует только до тех пор, пока существует компонент. Поскольку items всегда пересоздаётся каждый раз, когда рендерится <FriendList />, компонент всегда будет возвращать тот же результат.

С другой стороны, если items был создан за пределами компонента, он сохраняет свои предыдущие значения и запоминает изменения:

const items = []; // 🔴 Bad: created outside of the component
function FriendList({ friends }) {
  for (let i = 0; i < friends.length; i++) {
    const friend = friends[i];
    items.push(
      <Friend key={friend.id} friend={friend} />
    ); // 🔴 Bad: mutates a value created outside of render
  }
  return <section>{items}</section>;
}

Когда <FriendList /> выполняется снова, мы будем продолжать добавлять friends к items каждый раз, когда этот компонент запускается, что приводит к множеству дублируемых результатов. Этот вариант <FriendList /> имеет наблюдаемые побочные эффекты во время рендеринга и нарушает правило.

Ленивая инициализация

Ленивая инициализация также допустима, несмотря на то, что она не является полностью «чистой»:

function ExpenseForm() {
  SuperCalculator.initializeIfNotReady(); // ✅ Good: if it doesn't affect other components
  // Continue rendering...
}

Изменение DOM

Побочные эффекты, непосредственно видимые пользователю, не допускаются в логике рендеринга React-компонентов. Другими словами, просто вызов функции компонента сам по себе не должен вызывать изменения на экране.

function ProductDetailPage({ product }) {
  document.title = product.title; // 🔴 Bad: Changes the DOM
}

Один из способов достижения желаемого результата обновления document.title вне рендеринга — это синхронизировать компонент с document.

До тех пор, пока вызов компонента несколько раз безопасен и не влияет на отображение других компонентов, React не заботится о том, является ли он на 100% чистым в строго функциональном смысле этого слова. Более важно, чтобы компоненты должны быть идемпотентными.


Свойства и состояние являются неизменяемыми

Свойства и состояние компонента являются неизменяемыми снимками. Никогда не изменяйте их напрямую. Вместо этого передавайте новые свойства вниз и используйте функцию-сеттер из useState.

Можно рассматривать значения свойств (props) и состояния (state) как моментальные снимки, которые обновляются после отрисовки. По этой причине вы не изменяете переменные props или state напрямую: вместо этого вы передаёте новые props или используете предоставленную функцию-сеттер, чтобы сообщить React, что состояние должно обновиться при следующем рендере компонента.

Не изменяйте свойства (Props)

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

function Post({ item }) {
  item.url = new Url(item.url, base); // 🔴 Bad: never mutate props directly
  return <Link url={item.url}>{item.title}</Link>;
}
function Post({ item }) {
  const url = new Url(item.url, base); // ✅ Good: make a copy instead
  return <Link url={url}>{item.title}</Link>;
}

Не изменяйте состояние (State)

useState возвращает переменную состояния и функцию-сеттер для обновления этого состояния.

const [stateVariable, setter] = useState(0);

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

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

  function handleClick() {
    count = count + 1; // 🔴 Bad: never mutate state directly
  }

  return (
    <button onClick={handleClick}>
      You pressed me {count} times
    </button>
  );
}
function Counter() {
  const [count, setCount] = useState(0);

  function handleClick() {
    setCount(count + 1); // ✅ Good: use the setter function returned by useState
  }

  return (
    <button onClick={handleClick}>
      You pressed me {count} times
    </button>
  );
}

Возвращаемые значения и аргументы хуков являются неизменяемыми

После передачи значений хуку, их не следует изменять. Как и свойства (props) в JSX, значения становятся неизменяемыми при передаче хуку.

function useIconStyle(icon) {
  const theme = useContext(ThemeContext);
  if (icon.enabled) {
    icon.className = computeStyle(icon, theme); // 🔴 Bad: never mutate hook arguments directly
  }
  return icon;
}
function useIconStyle(icon) {
  const theme = useContext(ThemeContext);
  const newIcon = { ...icon }; // ✅ Good: make a copy instead
  if (icon.enabled) {
    newIcon.className = computeStyle(icon, theme);
  }
  return newIcon;
}

Важным принципом в React является локальное рассуждение: способность понять, что делает компонент или хук, посмотрев на его код изолированно. Хуки следует рассматривать как «чёрные ящики» при их вызове. Например, пользовательский хук мог использовать свои аргументы как зависимости для кеширования значений внутри него:

function useIconStyle(icon) {
  const theme = useContext(ThemeContext);

  return useMemo(() => {
    const newIcon = { ...icon };
    if (icon.enabled) {
      newIcon.className = computeStyle(icon, theme);
    }
    return newIcon;
  }, [icon, theme]);
}

Если вы измените аргументы хука, кеширование пользовательского хука станет неправильным, поэтому важно этого избегать.

style = useIconStyle(icon);         // `style` is memoized based on `icon`
icon.enabled = false;               // Bad: 🔴 never mutate hook arguments directly
style = useIconStyle(icon);         // previously memoized result is returned
style = useIconStyle(icon);         // `style` is memoized based on `icon`
icon = { ...icon, enabled: false }; // Good: ✅ make a copy instead
style = useIconStyle(icon);         // new value of `style` is calculated

Аналогично, важно не изменять возвращаемые значения хуков, так как они могут быть кэшированы.


Значения неизменяемы после передачи в JSX

Не изменяйте значения после их использования в JSX. Переместите изменение до создания JSX.

Когда вы используете JSX в выражении, React может выполнить оценку JSX до завершения рендеринга компонента. Это означает, что изменение значений после их передачи в JSX может привести к устаревшим интерфейсам, так как React не будет знать, что нужно обновить вывод компонента.

function Page({ colour }) {
  const styles = { colour, size: "large" };
  const header = <Header styles={styles} />;
  styles.size = "small"; // 🔴 Bad: styles was already used in the JSX above
  const footer = <Footer styles={styles} />;
  return (
    <>
      {header}
      <Content />
      {footer}
    </>
  );
}
function Page({ colour }) {
  const headerStyles = { colour, size: "large" };
  const header = <Header styles={headerStyles} />;
  const footerStyles = { colour, size: "small" }; // ✅ Good: we created a new value
  const footer = <Footer styles={footerStyles} />;
  return (
    <>
      {header}
      <Content />
      {footer}
    </>
  );
}

© 2013–present Facebook Inc.
Licensed under the Creative Commons Attribution 4.0 International Public License.
https://react.dev/reference/rules/components-and-hooks-must-be-pure

Spec-Zone.ru

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