Как я реализовал 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 за эти годы.
Я также хочу поблагодарить авторов этих замечательных статей, без них работа была бы более долгой и утомительной.