RFC: Keeping real dom elements across renders in React
Tags: react
Published: 2023-02-14 16:46:17

Hello, dev.to world! This is my first article on dev.to, I wrote some Japanese articles tho. So today, I want to join on dev.to to show my new idea and hear the opinions of folks here.

The idea is: how about if we can keep the same real DOM elements across renders.

## What is "keep DOM element"?

First, the same dom element cannot appear in the DOM tree twice; it looks like the element just jumped to the other place if you try to add the same element to whether in the dom tree a second time.
This is actually moved the element, so even if the DOM API doesn't have a "move" method, we can do that.

This is a demo in vanilla JavaScript; click that button, and you can see it works.

{% stackblitz https://stackblitz.com/edit/js-awr2ts?embed=1&file=index.js %}

But why do you need to move that? React doesn't do it for you? I'll show you why.

Another demo here. This time enter something to the text field first, then click that button; you can see the input element moved without losing what you entered!

{% stackblitz https://stackblitz.com/edit/js-zqa8af?file=index.js %}

So its element state remained. This is what I mean by "keeping the real dom element"! React doesn't do it usually; it just deletes and creates in other places something where the components tree changed, like page navigation.

## Why not just use React's Controlled Components for "keeping" the state?

Right. You should use Controlled Components in most cases, and that's enough.

Controlled Components is a good design of React. It is very helpful for representing user interfaces for the state of each rendering. React does this by updating DOM to fit with the components tree, but React does not know "reuse" the dom element across the tree ― React only reuses it in lists; This is why [you need to specify the `key` attribute in lists](https://beta.reactjs.org/learn/rendering-lists#keeping-list-items-in-order-with-key) ―

_By the way, the `value` prop of `<input>` could be understood as a synchronous __effect__ executes after rendering. But that's not all; the `value` prop also __fixes__ the state, so you cannot edit it unless you trigger rerendering to update the `value` prop._

Is there any problem if the DOM element is not reused? That's what we do; no problem at all, except in special cases where the element has an internal state that cannot be controlled.

Let's say you are looking at Twitter on your browser and find an interesting video, then click that tweet to see more. As you know, Twitter auto-plays videos, so the video started playing a few seconds ago. Do you remember what happened this time? – If you wanna try just now, [@nextjs](https://twitter.com/nextjs) often tweets a cool video –

This is what happens: the video starts playing from the beginning again. It would be great if the video will playing as is, isn't it?

So `<video>` element has no value attribute or something else specifying how far the video is to play. In other words, `<video>` cannot be controlled. Maybe we could save how far the video played and [synchronize with effects](https://beta.reactjs.org/learn/synchronizing-with-effects) after page navigation, but how about the loaded video data go? Some special HTML elements have an "internal state" that we cannot control and are not easy to manipulate. The only way to keep that completely is to reuse the element itself.

## How to do that?

The best way to "keep dom elements" is to wait for React officially support it and then use the feature.

Doing that in userland is not just [manipulating the dom](https://beta.reactjs.org/learn/manipulating-the-dom-with-refs) things. It needs to manage the rendered dom, track element across trees or renders, and take over the rendering for reuse. To maintain consistency, it should consider many cases, e.g., concurrent or async rendering, streaming ssr, and some cases may require special handling.

However, it is still possible to create a Proof of Concept. This is a simplistic one:

{% stackblitz https://stackblitz.com/edit/react-singleton-poc?embed=1&file=components/Singleton.js %}

This is what it does: when an `<Singleton id="videos:nextjs">` is rendered for the first time, save its children with its id. Then, when rendered a second time, replace its children with the saved one in useLayoutEffect. Note this is a simplistic one that is not so useful as it reuses the element which React will remove, so the element is no longer affected by React.

There is also a more useful one that I was inspired by someone; This PoC uses React [Portals](https://beta.reactjs.org/reference/react-dom/createPortal), and it allows to render a component to the same dom element regardless of where the element is.

{% stackblitz https://stackblitz.com/edit/react-warp-poc?embed=1&file=components/Warp.tsx %}

_Tbh, this almost code was from the person I was last year. I forgot I created the same thing last year until I saw how the PoC works. I even cannot understand what made me feel "oh, using portals could keep React rendering to the same element" last year. And that nice name "warp" was also from me last year._

As mentioned, getting involved in rendering in React is not an easy thing; both PoC below have a quirk now, you cannot access with useLayoutEffect and refs to the components of children in `<Warp>` to be kept at first render because the `<Warp>` is also using useLayoutEffect to insert the element, so the outside useLayoutEffect you write is executed before `<Warp>` inserted real dom.

That's it!

If you want to try it in your apps, there is [a library published to npm](https://www.npmjs.com/package/@oosawy/react-warp), which is very alpha tho. Let me know if you created something cool!

As well as `<video>`, it would be helpful for `<iframe>` or some other widgets written in plain JavaScript that directly access DOM, such as pdf viewer and 3d graphics. Of course, it may not always be needed, but there is potential in this idea.

Feel free to leave a comment to review this idea. Feedback on my English is also welcome!

Let's make the web fun, more! 🥳