Как я реализовал Reparenting с помощью нескольких строк кода

Я разрабатываю приложение, похожее на Trello. На главной странице мне нужны несколько вертикальных списков и несколько карточек, которые можно перетаскивать из одного списка в другой.

Как перенести компонент Card после перетаскивания? С React это кажется довольно простым. Чтобы изменить родительский компонент дочернего компонента, компоненты должны быть повторно отрисованы с этим дочерним элементом в его новом родительском компоненте.

Таким же образом я могу перенести <Card> в новый <List>.

Я реализую первый черновик кода и пробую его, я беру Карту мышкой и перетаскиваю ее между различными списками. Перенос происходит, но, к сожалению, компонент Card отключен, перемонтирован и теряет свое внутреннее состояние.

Более того, отзывы от анимации перетаскивания не такие положительные. Когда я быстро выполняю несколько перетаскиваний подряд, приложение замедляется, и на некоторое время наблюдается значительная потеря кадров.

Фактически, элементы DOM Карты воссоздаются с нуля, и это отрицательно сказывается на производительности. Кроме того, одним из элементов является прокручиваемый <div>, который теряет свою позицию прокрутки, я думаю, другие элементы, такие как <video> и <audio>, могут иметь аналогичные проблемы.

Приложив некоторые усилия, я могу перепроектировать приложение для использования компонентов карты без локального состояния, но в любом случае я не могу избежать воссоздания элементов DOM.

Можно ли предотвратить повторную установку компонента?

Что ж, если вы читаете эту статью, скорее всего, ответ будет положительным :), но когда я впервые задал себе вопрос, я не нашел однозначного ответа, вероятно, потому, что его еще не было. Продолжим рассказ.

Начинаю искать ответ в репозитории React на Github, может в разделе проблем есть что-то полезное. Я обнаружил, что есть термин для обозначения того, что я ищу, и это Воспроизведение.

«Reparenting направлен на улучшение как разработчика, так и пользовательского опыта».

Некоторые открытые проблемы подтверждают, что React еще не предоставляет конкретных API для их решения, и мои надежды на существование чего-то вроде React.transferComponent( ) быстро угасают.

Я обнаружил подход ReactDOM.unstable_renderSubtreeIntoContainer( ), название выглядит круто, но тега unstable и того факта, что этот API устарел, достаточно, чтобы заставить меня искать что-то еще. Поиски продолжаются на Medium, Dev.to и других платформах, единственное возможное решение - использование Порталов. Твит Дэна Абрамова определенно убеждает меня попробовать их.

Порталы приближаются

Открываю документацию React в разделе Порталы. Я начинаю читать руководство и выполнять некоторые тесты, чтобы познакомиться с этими API.

const element = document.createElement('div');
const PortalComponent = ({children}) => {
  return ReactDOM.createPortal(children, element);
};

Я знаю, что я не могу переместить компонент в другое место в приложении, иначе он будет повторно смонтирован, поэтому каждый дочерний компонент должен быть частью того же родителя.

Должен ли я использовать портал для каждого ребенка? Таким образом, я мог решить, в каком элементе контейнера отображать каждый из них. Но как мне создавать контейнеры? Надо ли писать что-то вроде document.createElement('div') 🤨? Вместо этого я мог бы использовать ref для других компонентов. Где мне визуализировать эти компоненты? Ссылки изначально пустые, следует ли принудительно выполнить второй рендеринг? Я хотел, чтобы у каждого Родителя был свой контекст. Как я могу это сделать, если я вынужден использовать только одного Родителя?…

Какой бардак, чем больше я пытаюсь это реализовать, тем более форсированным мне кажется подход. Это не дает мне ощущения, что я очень «реагирую», вероятно потому, что порталы были разработаны для других целей:

«Порталы предоставляют первоклассный способ визуализации дочерних элементов в узле DOM, который существует вне иерархии DOM родительского компонента». - Реагировать на документы.

Этот процесс больше связан с DOM, на «уровне реакции» дочерний элемент все еще является частью того же родителя, а не совсем то, что я ищу.

Новое решение

Возможно, я ищу решение не в том месте, возможно, что, если оно существует, оно более внутреннее для React, чем я думаю.

Я знаю, что React представляет мое приложение в виде дерева экземпляров, где каждый экземпляр соответствует компоненту. При повторном рендеринге части приложения его поддерево воссоздается и сравнивается со старым, чтобы найти внесенные изменения и обновить DOM.

Из-за способа реализации этого сравнения не существует способа сообщить React о передаче компонента. Действительно, если я попытаюсь повторно отрендерить компонент Card в другом месте, результатом будет размонтирование компонента и установка нового.

Как я могу изменить это поведение? Я мог бы попытаться взаимодействовать с внутренним деревом, найти экземпляр Карты, который я хочу передать, и вставить его в новый Список. Таким образом, после повторного рендеринга и старое, и новое дерево будут иметь перенесенную Карту в одном месте, и сравнение не приведет к повторному монтированию компонента. Это может сработать. !

Прежде чем приступить к разработке решения, чтобы не попасть в тупик, я налагаю некоторые ограничения, которые должен соблюдаться в конечном результате:

  • Он не должен полагаться на какие-либо нестабильные методы
  • Воспроизведение должно работать без изменения дизайна приложения
  • Он должен уважать философию и шаблоны React

У меня есть прочная отправная точка, теперь я должен понять, как на самом деле реализованы эти внутренние механизмы реакции. Я обнаружил, что, начиная с версии 16, React развернул новую реализацию этого внутреннего дерева экземпляров под названием Fiber. Я прочитал несколько статей об этом, чтобы получить более полную картину, и когда мне кажется, что у меня есть достаточно широкое представление по теме, я начинаю просматривать исходный код React в поисках решения.

После нескольких дней тестирования и исследований у меня наконец-то есть первый черновик кода, который я могу попробовать, в файле с именем react-reparenting.js. Я импортирую ее в свое приложение, добавляю несколько строк кода и ... Она работает! Карта не переустанавливается, и все цели, которые я поставил перед собой, были соблюдены.

У этой истории наконец-то может быть хороший конец, я могу продолжить разработку своего приложения. Может быть, я найду такую ​​историю, чтобы прочитать следующее препятствие, с которым я столкнусь.

Конец истории

Эта история заканчивается публикацией пакета на Github и написанием этой статьи. Прежде чем представить его, я хочу поделиться с вами своим видением в конце этого проекта.

Я твердо верю, что Воспроизведение - это не только способ управления этими ситуациями. , но Путь и я также верю, что в будущем React будет реализовывать его изначально.

На мой взгляд, причина, по которой эта функция еще не реализована, заключается в том, что случаев, когда она действительно необходима, не так много. Часто передаваемые компоненты не имеют состояния и очень просты, поэтому их повторное монтирование является приемлемым компромиссом, поскольку разница в производительности практически равна нулю и нет состояния или жизненного цикла, которые можно было бы прервать.

Я не говорю, что React будет реализовывать Reparenting, как это было реализовано здесь, или что API, которые будут предоставлены, будут аналогичны этим, но я надеюсь, что этот пакет, также благодаря своей простоте, сможет заложить основы для использования и распространения Reparenting.

«Отключение одного компонента и установка другого идентичного компонента - это простой компромисс, который работает в большинстве случаев. Компонент должен всегда передаваться без прерывания его жизненного цикла ».

Вы можете найти пакет на Github.

На странице GitHub вы также найдете документацию и ссылки на различные примеры в Codesandbox. Теперь давайте посмотрим на простую реализацию.

Сначала давайте определим <Child> компонент, мы будем использовать очень простой.

Теперь мы можем использовать компонент <Reparentable>, он должен быть прямым родителем дочерних элементов для повторного родителя. У каждого <Reparentable> должен быть уникальный идентификатор.

Теперь мы можем переродить <Child>. Сначала мы должны отправить его внутренний экземпляр с помощью метода sendReparentableChild( ), а затем нам просто нужно повторно отрендерить приложение. Перенесенный компонент не будет повторно смонтирован.

Это все. Также возможно создать собственный родительский компонент и использовать внутри него <Reparentable>.

Специальная благодарность

Во время разработки этого проекта я подумал, что потеряю рассудок, управляя каждым вариантом использования (контекст, памятка, некоторые крайние случаи с волокнами…). React с приятным удивлением работал в каждом из этих случаев без изменений, что свидетельствует об удивительной работе, проделанной React Team за эти годы.

Я также хочу поблагодарить авторов этих замечательных статей, без них работа была бы более долгой и утомительной.