Деструктуризация, которая появилась в стандарте ES6, уже не вызывает вопросов у многих из нас, есть большое количество статей, раскрывающих ее возможности. В основном, мы все тесно с ней дружим и пользуемся.
Казалось бы, что можно рассказать о том, о чем все и так знают? Но практика и чтение статей на Хабре, показали, что есть некоторые нюансы использования деструктуризации в React, о которых не все из нас знают или просто не задумываются, хотя они и являются очевидными.
Как часто Вам приходилось сталкиваться с подобным кодом?
export default function ParentComponent ({ post }) {
const { images, title } = post
return (
<ChildComponent images={images} title={title} />
)
}
Думаю, что довольно часто. Все просто, удобно и отлично работает.
Теперь предположим, что данных для ParentComponent
может и не быть, но, по каким либо причинам, нам все-таки нужно, чтобы у них было значение по умолчанию:
export default function ParentComponent ({ post }) {
const { images = [], title = 'New Post' } = post
return (
<ChildComponent images={images} title={title} />
)
}
Снова, все просто, удобно и отлично работает. Но что может пойти не так?
Давайте предположим, что мы решили оптимизировать ререндер нашего ChildComponent,
т.е. мемоизируем его, с помощью React.memo
:
const ChildComponent = React.memo(({ images, title }) => {
return (
<>
<PostTitle title={title} />
<ImagesList images={images} />
</>
)
})
И вот, мы столкнулись с проблемой, так как в случае, когда в нашем post
нет массива images
т.е. мы устанавливаем в него значение по умолчанию, а именно пустой массив, то для ChildComponent
это будет является новым пропсом на каждый ререндер родителя или источника этой переменной. Это равнозначно с объявлением новой переменной с пустым массивом в компоненте, которая будет создаваться на каждый его рендер, собственно, это так и есть:
const images = []
Хорошо, что у нас один уровень вложенности, мы легко найдем проблемное место и сможем быстро исправить это недоразумение. А что, если уровень вложенности у нас больше, и есть ненавистный props drilling?
export default function ParentComponent ({ post }) {
const { images = [], title = 'New Post' } = post
return (
<ChildComponent images={images} title={title} />
)
}
const ChildComponent = ({ images, title }) => {
return (
<>
<PostTitle title={title} />
<ImagesList images={images} />
</>
)
}
const ImagesList = React.memo(({ images }) => {
return (
<>
{images.map((image) => (
<Image key={image.id} image={image} />
))}
</>
)
})
Да, ImagesList
будет ререндериться каждый раз, когда будет происходит ререндер ParentComponent
. Пример, конечно, простой, но ведь мемоизированный ImagesList
может применяться по всему проекту десятки раз, иметь больше уровней вложенности, триггеров ререндера источника тоже может быть много и найти все места, где добавленное значение по умолчанию ломает нашу оптимизацию - может оказаться хоть и простой, но затратной по времени задачей.
Кстати, добавление значения по умолчанию в деструктуризации props компонента сразу в аргументах, очевидно, приводит к той же самой проблеме, но встречается на практике еще чаще:
const ChildComponent = ({ images = [], title }) => {
return (
<>
<PostTitle title={title} />
<ImagesList images={images} />
</>
)
}
Тем временем, значение по умолчанию примитивами, как в примере выше:
const { title = 'New Post' } = post
Не приведет к подобной проблеме, т.к. сверять memo
будет именно примитивные данные, а не ссылки на массивы или объекты (вспоминаем базу).
В заключение, хочу добавить, что для многих такое поведение будет очевидным и писать об этом - не нужным, но на практике такие мелкие проблемы встречаются очень часто в разного уровня проектах, не в одной статье Хабра связанной с деструктуризацией в React не нашел уточнения по этому поводу, потому уверен, что данный материал найдет своего читателя и сделает нашу работу немного лучше. Особенно это актуально для тех проектов, где оптимизацией занимаются во вторую очередь, путем рефакторинга существующей кодовой базы, там, где не подготавливают данные с помощью адаптеров и иных способов.
Комментарии (9)
anton_nix
31.03.2024 09:31+2Непонятно, как деструктуризация относится к описываемой проблеме. Без деструктуризации, при передаче props.images ?? [] было бы то же самое. И, наоборот, с деструктуризацией, но если можно передать, например, undefined в качестве images, то проблемы нет.
KataevAS Автор
31.03.2024 09:31Смысл как раз в том, что это и есть одно и тоже, но на проектах часто не воспринимают добавление значения по умолчанию в момент деструктуризации, как создание новой переменной с присваиванием ссылки на новый пустой массив или объект. Цель была - обратить на это внимание тех, кто об этом ранее не задумывался.
Alexandroppolus
31.03.2024 09:31Особенно забавно, если проп с дефолтным объектным значением залетает в депенденсы useEffect, в котором меняется стейт - тут уже бесконечный ререндер. Помнится, столкнулись с таким на работе в прошлом году.
KataevAS Автор
31.03.2024 09:31Согласен. Могут быть даже более "забавные" события, например, когда такая переменная залетает в массив зависимостей useCallback какой-нибудь невзрачной функции, которая прокидывается на несколько уровней вложенности, уже где-то там эта функция залетает в зависимости useEffect, где изменяет стейт на уровне, где применяется эта "злополучная" деструктуризация с установкой значения по умолчанию и потенциально может войти в бесконечный цикл выполнения, но случай, когда эта переменная прилетает пустая и применяется значение по умолчанию сам по себе довольно редкий кейс, то еще надо словить его, прежде чем хотя бы начать поиск источника проблемы. А если это проект, где не проводят тестирование на получение минимального объема данных сущности, то можно и не знать, что где-то на проде есть 1-2 страницы товара, например, где страница пользователя зависает без шансов на спасение. Я, честно говоря, конкретно с таким не сталкивался на практике, просто дофантазировал один из примеров, но, теоретически, это вполне реально)
Nikitakun1
31.03.2024 09:31Все так. Поэтому кажется норм решением стараться максимально писать логику прилаги вне Реакта (классы с MobX) и максимально избегать useEffect'ов
KataevAS Автор
31.03.2024 09:311) Не на каждом проекте нужен стейт менеджер, не на каждом проекте применяется именно MobX;
2) useEffect имеет свое прямое назначение, его, в принципе, не стоит применять для того, чего он не предназначен, т.к. он является инструментом работы с жизненным циклом функционального компонента React;
3) Если вернуться к теме материала, то пытаться исправить проблему из-за необдуманного использования значений по умолчанию в деструктуризации путем прикручивания стейт менеджера к проекту и только из-за этого начать задумываться зачем нужен useEffect, вместо того, чтобы задуматься над правильным использованием дефолтных значений, звучит странно.
savostin
Так а как "правильно"-то?
KataevAS Автор
В данном случае, я намеренно не стал описывать "правильный" вариант, т.к. вариантов избежать подобной проблемы много, главное знать о ней. Самый простой - не использовать значение по умолчанию для ссылочных данных (т.е. иметь необходимые проверки, чтобы не допускать ошибок работы с undefined), можно подготавливать данные заранее на уровне работы с API различными способами, хранить значения по умолчанию в константах, тогда при проверках - это будет одна и та же ссылка, назначать такие значения по умолчанию в низкоуровневых компонентах, которые мемоизированы и не передают их ниже, мемоизировать сами эти значения. Зависит от походов на проекте.