skip to content

In the browser DOM, you take an element that is already in the page and append it to a different container. Do you end up with one node or two, and what happens to the listeners already attached to it?

level: middleimportance: must knowfreq 58%

answer

  1. a node has at most one parent
  2. insertion detaches from the old parent first
  3. same object, so listeners come along
  4. appending twice is not duplication
  5. clone when you need a second copy

basics

~20 s

One node. Inserting a node that already has a parent detaches it from that parent first, so insertion moves rather than copies. The node keeps its identity, so listeners added with addEventListener, dataset values and expando properties all travel with it.

solid answer

~50 s

You get one node, in the new place. Every DOM insertion runs the same pre-insert algorithm, and its first step removes the node from its current parent, because a node has at most one parent — so `newParent.append(existing)` is a *move*, not a copy. Since it is the same object, everything attached to the object survives: listeners registered with `addEventListener`, `dataset` values, custom JavaScript properties, and the node's children. What does not survive is state the browser rebuilds from the layout tree — the element loses focus, a scrolled container loses its scroll offset, and a running CSS animation restarts. If you actually want a second copy you must call `cloneNode(true)` and insert the clone. The same rule is why appending the same node twice in a loop leaves you with one node, not many, and why draining a container with `while (src.firstChild) dst.append(src.firstChild)` works.

code

javascript · 14 lines
javascript
const a = document.createElement('ul');
const b = document.createElement('ul');
const li = document.createElement('li');
li.addEventListener('click', () => console.log('still mine'));
a.append(li);

b.append(li);                 // a move, not a copy
console.log(a.children.length, b.children.length); // 0 1

b.append(li);                 // appending again does not duplicate
console.log(b.children.length); // 1

b.append(li.cloneNode(true)); // a real second node (without the listener)
console.log(b.children.length); // 2

go deeper

for a junior

Remember that inserting an element that is already on the page moves it rather than copying it, and that making a real copy needs cloneNode. Be ready to say a node can only have one parent.

for a middle

Explain the mechanism: every insertion method runs the same pre-insert algorithm, which detaches the node from its current parent first. Point out that node identity is preserved, so addEventListener listeners and expando properties come along.

for a senior

Show what a move costs in practice — focus, scroll offset and running animations are lost even though the node object survives — and say how you save and restore that state around a reparent.

for a principal

Frame the architectural choice: move-and-preserve versus destroy-and-re-render is the difference between imperative DOM ownership and a rendering model that owns the tree, and mixing the two in one codebase is where reparenting bugs come from.

## A node has exactly one parent The DOM is a tree, and in a tree every node has at most one parent. That single constraint explains the whole behaviour. The DOM specification defines one shared "pre-insert" routine that `appendChild`, `insertBefore`, `append`, `prepend`, `before`, `after`, `replaceWith` and `replaceChildren` all funnel into, and one of its first steps is: if the node being inserted already has a parent, remove it from that parent. So there is no separate "move" API. Moving *is* inserting. ```js const li = document.querySelector('#a li'); document.querySelector('#b').append(li); // #a no longer contains li; #b does. One node exists, not two. ``` ## What travels with the node Because it is the same JavaScript object with the same identity, everything hanging off that object comes along: - **Event listeners** added with `addEventListener` — they are stored on the `EventTarget` itself. - **Inline handler attributes** and every other attribute. - **`dataset` entries**, since those are just `data-*` attributes. - **Expando properties** you set yourself, such as `el._row = record`. - **Its entire subtree** — children move with the parent; you never move descendants separately. This is the practical difference between moving and re-rendering. Rebuilding markup from a string throws away node identity and everything bound to it; moving preserves all of it. ## What does not travel The node object survives, but anything the browser derives from the node's position in a rendered document is rebuilt after the move. Focus is dropped when the focused element leaves the document, so the caret disappears. A scrollable container's scroll offset resets, because its layout box was destroyed. Running CSS transitions and animations restart from the beginning. Preserving those means saving and restoring them yourself around the move. ## Consequences people trip over **Appending the same node twice does not duplicate it.** ```js const el = document.createElement('div'); container.append(el); container.append(el); console.log(container.children.length); // 1 ``` The second call removes `el` from `container` and re-appends it — a net move to the end. This is a common bug in code that builds a template element once and inserts it in a loop, expecting N copies and getting one. The fix is `container.append(el.cloneNode(true))` inside the loop. **Re-appending is how you reorder.** To send a row to the bottom of a list, you do not remove and re-create it — you just append it again: ```js list.append(row); // row is now last list.prepend(row); // row is now first row.after(otherRow); // otherRow now sits right after row ``` That is a genuine, cheap reorder that keeps listeners and state intact. **Draining one container into another.** ```js while (source.firstChild) target.append(source.firstChild); ``` This terminates precisely because each `append` removes the child from `source`, shrinking it each pass. The mirror-image mistake is iterating a container's children with an index while moving them out, since the collection shrinks underneath the loop. **Moving out of a `DocumentFragment` empties it.** The same rule applies: inserting a fragment's children elsewhere removes them from the fragment. **Cycles are rejected.** Trying to insert an element into its own descendant throws a `HierarchyRequestError` `DOMException`, because the result would not be a tree. ## Cross-document moves If the node comes from another document — for example, an element read out of a same-origin iframe's `contentDocument` — the insertion algorithm adopts it into the destination document automatically, changing its `ownerDocument`. Older DOM versions required an explicit `document.adoptNode()` and threw a `WrongDocumentError` without it; current browsers adopt for you. `document.importNode(node, true)` is the different operation: it produces an owned *copy* and leaves the original where it is. ## Move or clone? Decide by asking whether the original should still exist afterwards. Reordering, promoting an element into a modal, or reparenting a widget: move, and get listener and state preservation for free. Rendering N rows from one prototype, or keeping a pristine original: clone, and remember that cloning does not carry listeners across.

  • A colleague builds one `<li>` and appends it inside a loop of 50 records, but the list shows a single row. What is happening?
    Each iteration re-inserts the same node object, and insertion first detaches it from its current parent — so all 50 appends are moves of one element to the end of the list. The fix is to create a fresh element per record, or to insert `prototype.cloneNode(true)` each time so each iteration inserts a distinct node.
  • How do you move an element while preserving the caret if the user was typing in it?
    Record the state before the move and restore it after: check `document.activeElement`, and for a text field also capture `selectionStart` and `selectionEnd`. After re-inserting, call `focus()` on the element and reassign the selection offsets. The browser will not do it for you, because focus is dropped the moment the element leaves the document.
  • Does moving an element fire any DOM event you can listen for?
    No — there is no move, insert or remove event on ordinary elements. If you need to observe structural changes, `MutationObserver` reports them as childList records on the affected parents. That is also why cleanup tied to removal has to be arranged explicitly rather than assumed.

saying these in an interview costs you the question

  • Thinking appendChild copies the node into the new parent
  • Expecting listeners to be lost when an element is moved
  • Appending one element repeatedly to produce many copies
  • Believing you must call removeChild before inserting elsewhere
  • Assuming focus and scroll position survive a re-parent

context