Деструктуризация, которая появилась в стандарте 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 не нашел уточнения по этому поводу, потому уверен, что данный материал найдет своего читателя и сделает нашу работу немного лучше. Особенно это актуально для тех проектов, где оптимизацией занимаются во вторую очередь, путем рефакторинга существующей кодовой базы, там, где не подготавливают данные с помощью адаптеров и иных способов.