Автор Чак Шун Ю✏️
Независимо от того, являетесь ли вы разработчиком React с многолетним опытом или только начинаете работать в этой области, вы гарантированно столкнетесь с сообщениями об ошибках в какой-то момент. Неважно, пишете ли вы код, который вызывает эти ошибки – никто не пишет идеальный код, и нам повезло, что React помогает нам, следя за тем, чтобы мы оставались на правильном пути.
Однако важен ваш подход к решению этих ошибок. Натыкаться на них, искать их в Google и исправлять свой код, основываясь на опыте других людей, – это один путь.
Другой путь – и, возможно, лучший – заключается в том, чтобы понять детали, стоящие за ошибкой, и почему она вообще возникла.
Эта статья поможет вам понять эти детали, рассмотрев некоторые из наиболее распространенных сообщений об ошибках React и объяснив, что они означают, каковы их последствия и как их исправить.
Мы рассмотрим следующие сообщения об ошибках:
- Предупреждение: Каждый дочерний элемент в списке должен иметь уникальный
key
prop - Предотвращение использования индекса массива в ключах
- React Hook
useXXX
вызывается условно. React Hooks должны вызываться в точно таком же порядке в каждом рендере компонента - В React Hook отсутствует зависимость: ‘XXX’. Либо включите ее, либо удалите массив зависимостей.
- Невозможно выполнить обновление состояния React на не смонтированном компоненте
- Слишком много повторных рендеров. React ограничивает количество рендеров для предотвращения бесконечного цикла
- Объекты не действительны как дочерние элементы React / Функции не действительны как дочерние элементы React
- Смежные элементы JSX должны быть обернуты в окружающий тег
Это поможет вам лучше понять основные ошибки и предотвратить совершение подобных ошибок в будущем.
- Предупреждение: Каждый дочерний элемент в списке должен иметь уникальный key реквизит
- Как решить эту проблему
- Предотвращение использования индекса массива в ключах
- Как решить эту проблему
- React Hook useXXX вызывается условно. React Hooks должны вызываться в точно таком же порядке в каждом компоненте рендеринга.
- Как решить эту проблему
- В React Hook отсутствует зависимость: ‘XXX’. Либо включите ее, либо удалите массив зависимостей.
- Как решить эту проблему
- Невозможно выполнить обновление состояния React на немонтированном компоненте
- Как решить эту проблему
- Слишком много повторных рендеров. React ограничивает количество рендеров для предотвращения бесконечного цикла
- Как решить эту проблему
- Объекты не действительны как дочерние элементы React / Функции не действительны как дочерние элементы React
- Как решить эту проблему
- Смежные JSX-элементы должны быть обернуты в объемлющий тег
- Как решить эту проблему
- Заключительные мысли
- Полная видимость производственных приложений React
Предупреждение: Каждый дочерний элемент в списке должен иметь уникальный key
реквизит
import { Card } from "./Card";
const data = [
{ id: 1, text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit." },
{ id: 2, text: "Phasellus semper scelerisque leo at tempus." },
{ id: 3, text: "Duis aliquet sollicitudin neque," }
];
export default function App() {
return (
<div className="container">
{data.map((content) => (
<div className="card">
<Card text={content.text} />
</div>
))}
</div>
);
}
Одна из самых распространенных вещей в разработке React – это получение элементов массива и использование компонента для их рендеринга на основе содержимого элемента. Благодаря JSX мы можем легко встроить эту логику в наш компонент с помощью функции Array.map
и вернуть нужные компоненты из обратного вызова.
Однако часто в консоли браузера появляется предупреждение React о том, что каждый дочерний элемент списка должен иметь уникальное свойство key
. Скорее всего, вы столкнетесь с этим предупреждением несколько раз, прежде чем возьмете за правило давать каждому дочернему элементу уникальный реквизит key
, особенно если у вас мало опыта работы с React. Но как это исправить до того, как вы выработаете привычку?
Как решить эту проблему
Как следует из предупреждения, вам придется добавить key
prop в самый внешний элемент JSX, который вы возвращаете из обратного вызова map
. Однако есть несколько требований к ключу, который вы собираетесь использовать. Ключ должен быть:
- Либо строкой, либо числом
- Уникальным для данного элемента списка
- Репрезентативным для данного элемента списка во всех рендерах.
export default function App() {
return (
<div className="container">
{data.map((content) => (
<div key={content.id} className="card">
<Card text={content.text} />
</div>
))}
</div>
);
}
Хотя ваше приложение не упадет, если вы не будете придерживаться этих требований, это может привести к неожиданному и часто нежелательному поведению. React использует эти ключи для определения того, какие дочерние элементы в списке изменились, и использует эту информацию для определения того, какие части предыдущего DOM могут быть использованы повторно, а какие должны быть вычислены заново при повторном рендеринге компонентов. Поэтому всегда рекомендуется добавлять эти ключи.
Предотвращение использования индекса массива в ключах
Основываясь на предыдущем предупреждении, мы переходим к не менее распространенному предупреждению ESLint на ту же тему. Это предупреждение часто появляется после того, как вы взяли за привычку включать реквизит key
с результирующим JSX из списка.
import { Card } from "./Card";
// Notice that we don't include pre-generated identifiers anymore.
const data = [
{ text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit." },
{ text: "Phasellus semper scelerisque leo at tempus." },
{ text: "Duis aliquet sollicitudin neque," }
];
export default function App() {
return (
<div className="container">
{data.map((content, index) => (
<div key={index} className="card">
<Card text={content.text} />
</div>
))}
</div>
);
}
Иногда у вас нет уникального идентификатора, привязанного к вашим данным. Простым решением является использование индекса текущего элемента в списке. Однако проблема с использованием индекса элемента в массиве в качестве ключа заключается в том, что он не является репрезентативным для этого конкретного элемента во всех рендерах.
Допустим, у нас есть список с несколькими элементами, и пользователь взаимодействует с ними, удаляя второй элемент. Для первого элемента ничего не изменилось в его базовой DOM-структуре; это отражено в его ключе, который остался прежним, 0
.
Для третьего и последующих элементов их содержимое не изменилось, поэтому их базовая структура также не должна измениться. Однако реквизит key
всех остальных элементов изменится, поскольку ключи основаны на индексе массива. React предположит, что они изменились, и пересчитает их структуру – без необходимости. Это негативно сказывается на производительности, а также может привести к непоследовательным и неправильным состояниям.
Как решить эту проблему
Чтобы решить эту проблему, важно помнить, что ключи не обязательно должны быть идентификаторами. Если они уникальны и представляют результирующую структуру DOM, любой ключ, который вы хотите использовать, будет работать.
export default function App() {
return (
<div className="container">
{data.map((content) => (
<div key={content.text} className="card">{/* This is the best we can do, but it works */}
<Card text={content.text} />
</div>
))}
</div>
);
}
React Hook useXXX
вызывается условно. React Hooks должны вызываться в точно таком же порядке в каждом компоненте рендеринга.
В процессе разработки мы можем оптимизировать наш код различными способами. Один из таких способов – убедиться, что определенный код выполняется только в тех ветвях кода, где он необходим. Особенно если речь идет о коде, который требует много времени или ресурсов, это может иметь огромное значение с точки зрения производительности.
const Toggle = () => {
const [isOpen, setIsOpen] = useState(false);
if (isOpen) {
return <div>{/* ... */}</div>;
}
const openToggle = useCallback(() => setIsOpen(true), []);
return <button onClick={openToggle}>{/* ... */}</button>;
};
К сожалению, применение этой техники оптимизации к хукам приведет к предупреждению о том, что нельзя вызывать React Hooks условно, так как вы должны вызывать их в том же порядке при каждом рендере компонента.
Это необходимо, поскольку внутри React использует порядок вызова хуков, чтобы отслеживать их базовые состояния и сохранять их между рендерами. Если вы нарушите этот порядок, React не будет знать, какое состояние соответствует хуку. Это создает серьезные проблемы для React и даже может привести к ошибкам.
Как решить эту проблему
React Hooks должны всегда вызываться на верхнем уровне компонентов – и безоговорочно. На практике это часто сводится к резервированию первой секции компонента для инициализации React Hook.
const Toggle = () => {
const [isOpen, setIsOpen] = useState(false);
const openToggle = useCallback(() => setIsOpen(true), []);
if (isOpen) {
return <div>{/* ... */}</div>;
}
return <button onClick={openToggle}>{/* ... */}</button>;
};
В React Hook отсутствует зависимость: ‘XXX’. Либо включите ее, либо удалите массив зависимостей.
Интересным аспектом React Hooks является массив зависимостей. Почти каждый хук React принимает второй аргумент в виде массива, внутри которого вы можете определить зависимости для хука. Когда какая-либо из зависимостей изменится, React обнаружит это и повторно запустит хук.
В своей документации React рекомендует разработчикам всегда включать все переменные в массив зависимостей, если они используются в хуке и при изменении влияют на рендеринг компонента.
Как решить эту проблему
Для решения этой проблемы рекомендуется использовать правило exhaustive-deps
внутри eslint-plugin-react-hooks
. Его активация предупредит вас, если для какого-либо React Hook не определены все зависимости.
const Component = ({ value, onChange }) => {
useEffect(() => {
if (value) {
onChange(value);
}
}, [value]); // `onChange` isn't included as a dependency here.
// ...
}
Причина, по которой вы должны быть исчерпывающими в вопросах массива зависимостей, связана с концепцией замыканий и диапазонов в JavaScript. Если основной обратный вызов React Hook использует переменные вне своей области видимости, то он может помнить только версию этих переменных на момент выполнения.
Но когда эти переменные изменяются, закрытие обратного вызова не может автоматически подхватить эти измененные версии. Это может привести к тому, что код React Hook будет выполняться с устаревшими ссылками на его зависимости, что приведет к поведению, отличному от ожидаемого.
По этой причине всегда рекомендуется использовать исчерпывающий массив зависимостей. Это решает все возможные проблемы с вызовом React Hooks таким образом, поскольку указывает React на переменные, которые необходимо отслеживать. Когда React обнаружит изменения в любой из переменных, он повторно запустит обратный вызов, что позволит ему уловить измененные версии зависимостей и запуститься, как ожидалось.
Невозможно выполнить обновление состояния React на немонтированном компоненте
При работе с асинхронными потоками данных или логики в ваших компонентах вы можете столкнуться с ошибкой во время выполнения в консоли браузера, сообщающей, что вы не можете выполнить обновление состояния компонента, который уже размонтирован. Проблема заключается в том, что где-то в дереве компонентов происходит обновление состояния компонента, который уже размонтирован.
const Component = () => {
const [data, setData] = useState(null);
useEffect(() => {
fetchAsyncData().then((data) => setData(data));
}, []);
// ...
};
Это вызвано обновлением состояния, которое зависит от асинхронного запроса. Асинхронный запрос запускается где-то в жизненном цикле компонента (например, внутри хука useEffect
), но его выполнение занимает некоторое время.
Тем временем компонент уже размонтирован (например, из-за взаимодействия с пользователем), но оригинальный async-запрос все еще завершается – поскольку он не связан с жизненным циклом React – и вызывает обновление состояния компонента. Ошибка возникает потому, что компонент больше не существует.
Как решить эту проблему
Существует несколько способов решения этой проблемы, все они сводятся к двум различным концепциям. Во-первых, можно отслеживать, смонтирован ли компонент, и выполнять действия на основе этого.
Хотя этот способ работает, он не рекомендуется. Проблема этого метода заключается в том, что он без необходимости сохраняет ссылки на немонтированные компоненты, что приводит к утечкам памяти и проблемам с производительностью.
const Component = () => {
const [data, setData] = useState(null);
const isMounted = useRef(true);
useEffect(() => {
fetchAsyncData().then(data => {
if(isMounted.current) {
setData(data);
}
});
return () => {
isMounted.current = false;
};
}, []);
// ...
}
Второй – и более предпочтительный – способ заключается в отмене асинхронного запроса, когда компонент размонтируется. Некоторые библиотеки асинхронных запросов уже имеют механизм отмены такого запроса. Если это так, то отменить запрос будет проще простого во время обратного вызова очистки хука useEffect
.
Если вы не используете такую библиотеку, вы можете добиться того же самого с помощью AbortController
. Единственным недостатком этих методов отмены является то, что они полностью зависят от реализации библиотеки или поддержки браузера.
const Component = () => {
const [data, setData] = useState(null);
useEffect(() => {
const controller = new AbortController();
fetch(url, { signal: controller.signal }).then((data) => setData(data));
return () => {
controller.abort();
}
}, []);
// ...
};
Слишком много повторных рендеров. React ограничивает количество рендеров для предотвращения бесконечного цикла
Бесконечные циклы – это бич существования каждого разработчика, и разработчики React не являются исключением из этого правила. К счастью, React делает очень хорошую работу по их обнаружению и предупреждению вас об этом до того, как все ваше устройство перестанет реагировать.
Как решить эту проблему
Как следует из предупреждения, проблема заключается в том, что ваш компонент вызывает слишком много повторных рендеров. Это происходит, когда ваш компонент ставит в очередь слишком много обновлений состояния за очень короткий промежуток времени. Наиболее распространенными причинами возникновения бесконечных циклов являются:
- Выполнение обновления состояния непосредственно в процессе рендеринга
- отсутствие надлежащего обратного вызова обработчика событий.
Если вы столкнулись с этим конкретным предупреждением, обязательно проверьте эти два аспекта вашего компонента.
const Component = () => {
const [count, setCount] = useState(0);
setCount(count + 1); // State update in the render
return (
<div className="App">
{/* onClick doesn't receive a proper callback */}
<button onClick={setCount((prevCount) => prevCount + 1)}>
Increment that counter
</button>
</div>
);
}
Объекты не действительны как дочерние элементы React / Функции не действительны как дочерние элементы React
В React существует множество вещей, которые мы можем отображать в DOM в наших компонентах. Выбор практически бесконечен: все теги HTML, любой элемент JSX, любое примитивное значение JavaScript, массив предыдущих значений и даже выражения JavaScript, если они оцениваются в любое из предыдущих значений.
Несмотря на это, к сожалению, React все еще не воспринимает все, что может существовать в качестве дочернего элемента React. Если быть более точным, вы не можете отображать объекты и функции в DOM, потому что эти два значения данных не будут оцениваться как что-то значимое, что React может отобразить в DOM. Поэтому любые попытки сделать это приведут к тому, что React будет жаловаться на это в виде упомянутых ошибок.
Как решить эту проблему
Если вы столкнулись с одной из этих ошибок, рекомендуется проверить, что переменные, которые вы выводите, имеют ожидаемый тип. Чаще всего эта проблема возникает при отрисовке дочернего элемента или переменной в JSX, предполагая, что это примитивное значение – но в действительности это оказывается объект или функция. В качестве профилактического метода, наличие системы типов может существенно помочь.
const Component = ({ body }) => (
<div>
<h1>{/* */}</h1>
{/* Have to be sure the `body` prop is a valid React child */}
<div className="body">{body}</div>
</div>
);
Смежные JSX-элементы должны быть обернуты в объемлющий тег
Одним из самых больших преимуществ React является возможность создания целого приложения путем объединения множества небольших компонентов. Каждый компонент может определить свою часть пользовательского интерфейса в виде JSX, который он должен отрисовать, что в конечном итоге способствует созданию всей структуры DOM приложения.
const Component = () => (
<div><NiceComponent /></div>
<div><GoodComponent /></div>
);
Из-за составной природы React, обычная вещь, которую можно попробовать, это вернуть два элемента JSX в корень компонента, который используется только внутри другого компонента. Однако, сделав это, разработчики React неожиданно получат предупреждение о необходимости обернуть соседние JSX-элементы в закрывающие теги.
С точки зрения обычного разработчика React, этот компонент будет использоваться только внутри другого компонента. Поэтому, в их ментальной модели, возвращать два элемента из компонента имеет смысл, потому что результирующая структура DOM будет одинаковой, независимо от того, определен ли внешний элемент в этом компоненте или в родительском компоненте.
Однако React не в состоянии сделать такое предположение. Потенциально, этот компонент может быть использован в корне и сломать приложение, поскольку это приведет к некорректной структуре DOM.
Как решить эту проблему
Разработчики React должны всегда оборачивать несколько JSX-элементов, возвращаемых из компонента, в объемлющий тег. Это может быть элемент, компонент или фрагмент React, если вы уверены, что компонент не требует внешнего элемента.
const Component = () => (
<React.Fragment>
<div><NiceComponent /></div>
<div><GoodComponent /></div>
</React.Fragment>
);
Заключительные мысли
Столкновение с ошибками во время разработки – это неизбежная часть процесса, независимо от того, сколько у вас опыта. Однако то, как вы справляетесь с этими сообщениями об ошибках, также свидетельствует о ваших способностях как разработчика React. Чтобы делать это правильно, необходимо понимать эти ошибки и знать, почему они возникают.
Чтобы помочь вам в этом, в данной статье мы рассмотрели восемь наиболее распространенных сообщений об ошибках React, с которыми вы можете столкнуться во время разработки React. Мы рассказали о значении сообщений об ошибках, об ошибке, лежащей в их основе, о том, как устранить ошибку и что произойдет, если вы не устраните ошибки.
Теперь, обладая этими знаниями, вы должны лучше понимать эти ошибки и чувствовать себя в силах писать меньше кода, содержащего эти ошибки, что приведет к повышению качества кода.
Полная видимость производственных приложений React
Отладка React-приложений может быть сложной задачей, особенно когда пользователи сталкиваются с проблемами, которые трудно воспроизвести. Если вы заинтересованы в мониторинге и отслеживании состояния Redux, автоматическом выявлении ошибок JavaScript, отслеживании медленных сетевых запросов и времени загрузки компонентов, попробуйте LogRocket.
LogRocket – это как видеорегистратор для веб- и мобильных приложений, записывающий буквально все, что происходит в вашем React-приложении. Вместо того чтобы гадать, почему возникают проблемы, вы можете собрать данные и отчитаться о том, в каком состоянии находилось ваше приложение в момент возникновения проблемы. LogRocket также отслеживает производительность вашего приложения, предоставляя такие показатели, как загрузка процессора клиента, использование памяти клиента и многое другое.
Пакет промежуточного ПО LogRocket Redux добавляет дополнительный уровень видимости пользовательских сессий. LogRocket регистрирует все действия и состояние ваших хранилищ Redux.
Модернизируйте отладку приложений React – начните мониторинг бесплатно.