loading...
Cover image for ⚛️ Reparenting is now possible with React

⚛️ Reparenting is now possible with React

paolimi profile image Paolo Longo ・7 min read

Originally published on Medium

I am designing an app similar to Trello. On the main page, I want some vertical Lists and some Cards that can be dragged from one List to another.

How can I transfer a Card component after dragging? With React it seems quite easy. To change the Parent component of a Child component, the components have to be re-rendered with that Child in its new Parent.

Re-render

In the same way, I can transfer a <Card> into a new <List>.

I implement a first draft of the code and try it, I take a Card with the mouse and drag it between the various Lists. The transfer takes place, but, unfortunately, the Card component is unmounted, re-mounted, and loses its internal state.

Moreover the feedback from the drag animation isn't so positive. When I perform several drags quickly in succession, the App slows down and for a few moments there is a considerable loss of frames.

In fact, the DOM elements of the Card are recreated from scratch and this is having a negative impact on performance. Also, one of the elements is a scrollable <div> that loses its scroll position, I guess other elements such as <video> and <audio> can have similar problems.

With some effort, I can redesign the App to use Card components without a local state, but in any case I cannot avoid that the DOM elements are recreated.

Is it possible to prevent the component from being re-mounted?

Well, if you are reading this article it is likely that the answer is positive :), but when I asked myself the question for the first time I did not find a definitive answer, probably because it was not there yet. Let's continue with the story.

I start looking for an answer in the React repository on Github, maybe there is something useful in the issues section. I find there is a term for what I'm looking for, and it's Reparenting.

"Reparenting aims to improve both the Developer and the User Experience."

Some open issues confirm that React does not yet provide specific APIs to handle it, my hopes that something like React.transferComponent( ) exists quickly fade away.

An approach I discover is ReactDOM.unstable_renderSubtreeIntoContainer( ), the name looks cool but the unstable tag and the fact that this API has been deprecated are enough to make me look for something else. Searches continue on Medium, Dev, and other platforms, the only possible solution seems to be the use of the Portals. A Tweet by Dan Abramov definitely convinces me to try them.

Tweet

The portals approach

I open the React documentation in the Portals section. I start reading the guide and doing some tests to get familiar with these APIs.

const element = document.createElement('div');

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

I know that I cannot move a component elsewhere in the App or it will be re-mounted, so every Child component must be part of the same Parent.

Should I use a portal for each Child? That way I could decide in which container element to render each of them. But how do I create containers? Do I have to write something like document.createElement('div') 🤨? I could instead use ref to other components. Where do I render those components? Refs are empty initially, should I force a second render? I wanted each Parent to provide a different Context, How can I do that if I am forced to use only one Parent?…

What a mess, the more I try to implement it, the more forced the approach seems to me. It doesn't give me the feeling of being very "reactive", probably because portals have been designed for other purposes:

"Portals provide a first-class way to render children into a DOM node that exists outside the DOM hierarchy of the parent component." - React docs.

This process is more related to the DOM, at the "React level" the Child is still part of the same Parent, not exactly what I am looking for.

The new solution

Maybe I'm looking for a solution in the wrong place, it is possible that, if it exists, it is more internal to React than I think.

What I know is that React represents my App with a tree of instances, where each instance corresponds to a component. When re-rendering a part of the App, its sub-tree is recreated and compared with the old one, so as to find the changes that have been made and update the DOM.

ReactTree

Due to the way this comparison is implemented, there is no way to make React aware of the transfer of a component. Indeed, If I try to re-render a Card component somewhere else, the result will be the unmounting of the component and the mounting of a new one.

How can I change this behavior? I could try to interact with the internal tree, find the instance of the Card that I want to transfer, and insert it in the new List. In this way, after a re-rendering, both the old and the new tree would have the transferred Card in the same place and the comparison would not cause the re-mount of the component, It might work!

Before starting to design a solution, to avoid running into dead ends I impose some constraints that the final result must respect:

  • It must not rely on any unstable method
  • Reparenting must be able to work without redesigning the App
  • It must respect the philosophy and patterns of React

I have a solid starting point, now I have to understand how these react internals are actually implemented. I find out that starting from version 16, React rolled out a new implementation of that internal instances tree named Fiber. I read some articles about it to get a more complete picture, and when I think I have a fairly broad view on the topic, I start to browse the React source code in search of a solution.

After several days of testing and research, I finally have a first draft of code to try, inside a file named react-reparenting.js. I import it into my App, add a few lines of code, and… It works! The Card is not re-mounted and the goals that I have set for myself have all been respected.

This story can finally have a nice ending, I can continue the development of my App. Maybe, for the next obstacle that I will face, I will find a story like this to read.

The end of the story

This story ends with the publication of the package on Github and with the writing of this article. Before presenting it, I want to share with you what my vision is at the end of this project.
 
I strongly believe that Reparenting is not only a way of managing these situations, but The way, and I also believe that in the future React will implement it natively.

In my opinion, the reason why this feature has not yet been implemented is that the cases in which it is really necessary are not many. Often the elements to be transferred are stateless and very simple, so it is an acceptable compromise to re-mount them since the difference in performance is almost zero, and there is no state or lifecycle to be interrupted.

I'm not saying that React will implement Reparenting as it has been implemented here, or that the APIs that will be provided will be similar to these, but I hope this package, thanks also to its simplicity, can lay the foundations for the use and diffusion of Reparenting.

"Unmounting one component and mounting another identical one is just a simple compromise that works in most cases. The component should always be transferred, without its lifecycle being interrupted."


You can find the package on Github. On the GitHub page you will also find the documentation and links to various examples on Codesandbox.
Now let's see a simple implementation.

First, let's define the <Child> component, we will use a very simple one.

Now we can use the <Reparentable> component, it has to be the direct parent of the children to reparent. Each <Reparentable> must have a unique id.

Now we can reparent a <Child>. First we have to send its fibers using the sendReparentableChild( ) method, then we just have to re-render the App. The transferred component will not be re-mounted.

That's all. It is also possible to create a custom Parent component and use the <Reparentable> inside it.

Special thanks

During the development of this project, I thought that I would lose my mind managing every use case (context, memo, some edge cases with fibers…). With pleasant surprise React worked in each of these cases without modification, a sign of the amazing work that the React Team has done over the years.

I want also to thank the authors of these amazing articles, without them the work would have been longer and more tedious.

Posted on by:

paolimi profile

Paolo Longo

@paolimi

21 years old Italian student of computer engineering at the Milan Polytechnic.

Discussion

pic
Editor guide
 

Really interesting! Thanks for this article showcasing the development process. One alternate solution to this would be to use an orthogonal state. You should check out Recoil which is an implementation of the orthogonal state by the Facebook team. You could also look at this video from the creator which shows something similar.

 

Or, if you like a mature solution, you can use Redux ;). While it's possible to not lose state with existing tools, the value of this library seems to be that you can now move a component to a different parent and re-use the same dom nodes. No centralized state solution can manage that! (afaik)

 

Redux wouldn't really work since every time you create a new list item it will have to re-render the tree. In this case, the best solution would be to use reparenting or orthogonal state.

Maybe I misunderstood recoil. My understanding is that if you have two state atoms with lists of items, and you move an item from one list to another, the same dom elements will not be reused with separate parents rendering like this:

<List>{taskList1.map(t => <Mytask task={t} />)}</List>
<List>{taskList2.map(t => <Mytask task={t} />)}</List>

If recoil does support reusing dom elements across parents, please let me know! I didn't find anything about it in the docs, so i don't see how it would be any more suited to this task than redux. My understanding of redux vs recoil is that it comes down to whether you want your state centralized or not.

Instead of only creating an atom for every list, you can create an atom for each item. This means that it will be separate from the list state and whenever you change its state you only affect that item. Furthermore, with selectors, you can prevent the list from re-rendering where it doesn't need to. Redux does not make this easy since it has a centralized state.

Child components with the same key are not reused across separate parents. Whether you're storing the items as separate atoms and storing lists of ids in another atom (which, btw is simply normalization, a practice that's extremely common and simple with Redux) or not. React-Redux also uses selectors to determine the props that will be passed to components based on the centralized state, and if the props haven't changed, the component won't re-render. There are also libraries like reselect for setting up multiple layers of memoization.

The issue here isn't preventing unnecessary renders, it's preserving dom nodes of a child component when reparenting it. Neither Redux nor Recoil can accomplish that on their own.

 

Very interesting read and nice work on the API.

 

Replying to myself, but having read this excellent article I dug into some of the linked references which got me very excited about utilising browser idle time. (It's what Fiber is up to)

I grabbed the keyboard and crafted a bit of a library for it - then wrote this article announcing it.

Thanks again for the inspiration Paolo!

 

Your project seems very interesting, Nice to have contributed to your Eureka moment :)

 

Very cool stuff! And a good reminder that "impossible" always has some give. I've been having trouble building a custom drag interaction that moves components across parents without killing the interaction, hopefully this will do the trick 🤞

 

It worked like a charm :). I'm using react-reparenting with react-draggable for preserving drag interactions on components that are being moved between parent containers. It doesn't preserve useState, but that's perfectly acceptable for my use case. Thank you!!