Компоненты и хуки должны быть чистыми
Чистые функции выполняют только вычисления и ничего более. Это делает ваш код более понятным, удобным для отладки и позволяет 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://18.react.dev/reference/rules/components-and-hooks-must-be-pure