Деструктуризация, которая появилась в стандарте 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)


  1. savostin
    31.03.2024 09:31
    +1

    Так а как "правильно"-то?


    1. KataevAS Автор
      31.03.2024 09:31

      В данном случае, я намеренно не стал описывать "правильный" вариант, т.к. вариантов избежать подобной проблемы много, главное знать о ней. Самый простой - не использовать значение по умолчанию для ссылочных данных (т.е. иметь необходимые проверки, чтобы не допускать ошибок работы с undefined), можно подготавливать данные заранее на уровне работы с API различными способами, хранить значения по умолчанию в константах, тогда при проверках - это будет одна и та же ссылка, назначать такие значения по умолчанию в низкоуровневых компонентах, которые мемоизированы и не передают их ниже, мемоизировать сами эти значения. Зависит от походов на проекте.


  1. anton_nix
    31.03.2024 09:31
    +2

    Непонятно, как деструктуризация относится к описываемой проблеме. Без деструктуризации, при передаче props.images ?? [] было бы то же самое. И, наоборот, с деструктуризацией, но если можно передать, например, undefined в качестве images, то проблемы нет.


    1. KataevAS Автор
      31.03.2024 09:31

      Смысл как раз в том, что это и есть одно и тоже, но на проектах часто не воспринимают добавление значения по умолчанию в момент деструктуризации, как создание новой переменной с присваиванием ссылки на новый пустой массив или объект. Цель была - обратить на это внимание тех, кто об этом ранее не задумывался.


  1. Iznor9507
    31.03.2024 09:31

    Круто, спасибо


  1. Alexandroppolus
    31.03.2024 09:31

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


    1. KataevAS Автор
      31.03.2024 09:31

      Согласен. Могут быть даже более "забавные" события, например, когда такая переменная залетает в массив зависимостей useCallback какой-нибудь невзрачной функции, которая прокидывается на несколько уровней вложенности, уже где-то там эта функция залетает в зависимости useEffect, где изменяет стейт на уровне, где применяется эта "злополучная" деструктуризация с установкой значения по умолчанию и потенциально может войти в бесконечный цикл выполнения, но случай, когда эта переменная прилетает пустая и применяется значение по умолчанию сам по себе довольно редкий кейс, то еще надо словить его, прежде чем хотя бы начать поиск источника проблемы. А если это проект, где не проводят тестирование на получение минимального объема данных сущности, то можно и не знать, что где-то на проде есть 1-2 страницы товара, например, где страница пользователя зависает без шансов на спасение. Я, честно говоря, конкретно с таким не сталкивался на практике, просто дофантазировал один из примеров, но, теоретически, это вполне реально)


      1. Nikitakun1
        31.03.2024 09:31

        Все так. Поэтому кажется норм решением стараться максимально писать логику прилаги вне Реакта (классы с MobX) и максимально избегать useEffect'ов


        1. KataevAS Автор
          31.03.2024 09:31

          1) Не на каждом проекте нужен стейт менеджер, не на каждом проекте применяется именно MobX;
          2) useEffect имеет свое прямое назначение, его, в принципе, не стоит применять для того, чего он не предназначен, т.к. он является инструментом работы с жизненным циклом функционального компонента React;
          3) Если вернуться к теме материала, то пытаться исправить проблему из-за необдуманного использования значений по умолчанию в деструктуризации путем прикручивания стейт менеджера к проекту и только из-за этого начать задумываться зачем нужен useEffect, вместо того, чтобы задуматься над правильным использованием дефолтных значений, звучит странно.