skip to content

In Cypress, why does a re-render detach the element your chain already holds?

level: juniorimportance: must knowfreq 80%

answer

  1. The chain carries an element, not a selector
  2. Frameworks replace nodes instead of editing them
  3. A finished action fixes the subject
  4. Start a fresh query after every action
  5. Error says Cypress cannot requery the page

basics

~20 s

A Cypress chain carries a concrete DOM node, not the selector that found it. A re-render throws that node away and mounts a fresh one, so the node your chain still holds is no longer in the document.

solid answer

~50 s

A query like `cy.get('[data-cy=room-row]')` resolves to a real element and hands that element down the chain as the subject. That is harmless while the chain is still queries, because Cypress re-resolves them before it acts. Once an action such as `.click()` has run, the subject is fixed to the node it acted on. Modern frameworks do not edit an element in place when state changes; they build a replacement and swap it in, so the node you hold is still a valid object but is no longer connected to the document. The next link then fails with *the page updated as a result of this command, but you tried to continue the command chain*. Nothing revives that node — end the statement at the action and let a new `cy.get()` find whatever replaced the row.

code

javascript · 9 lines
javascript
// FAILS: booking re-renders the row, so .parent() gets the node that left the page
cy.get('[data-cy=room-row]')
  .contains('Deluxe King')
  .click()
  .parent()

// WORKS: the second statement queries again and finds the replacement row
cy.get('[data-cy=room-row]').contains('Deluxe King').click()
cy.get('[data-cy=room-row]').contains('Deluxe King').parent()

go deeper

for a junior

Be ready to say in one sentence that a Cypress chain holds an element rather than a selector, and that a re-render replaces that element. Knowing the standard rewrite — end the statement at the action, query again — is enough at this level.

for a middle

Explain the mechanics: the subject is re-resolved while the chain is queries and becomes fixed once an action has completed, and detachment is a node-level connection check rather than a visual one.

for a senior

Show that you can tell a genuine detachment from a bad selector or a slow load without guessing, and that you know which so-called fixes only suppress the message.

for a principal

Be ready to argue where the remedy belongs. Constant re-rendering of a region is sometimes a product defect the suite is the first to notice, not a chain-shape problem for the tests to absorb.

## What a Cypress chain is actually carrying A query such as `cy.get('[data-cy=room-row]')` does not hand the next step a selector. It runs the selector once, takes the matching element out of the page, and passes that element down the chain as the **subject**. Everything after it — `.first()`, `.contains('Deluxe King')`, `.click()`, `.should(...)` — works on that concrete node. While the chain is still made of queries, holding an element costs you nothing, because Cypress re-resolves the query group again just before it acts. Once an **action** such as `.click()`, `.type()` or `.select()` has completed, that stops. The subject is now one particular node, and every remaining link in the chain gets handed that node and nothing else. ## Why a re-render is not a mutation The intuition that trips people up is *the row is still on screen, so the element is still there*. On a modern framework it usually is not. When a booking is confirmed and the room list re-renders, the framework does not edit the old `<tr>` in place — it builds a new element and swaps it in. The markup can be identical and the user sees no flicker, but at the DOM level: - the node your chain holds has been removed from the document; - a different node carrying the same attributes has been inserted where it used to be; - and the node you hold is still a perfectly valid object, with all its children and text intact. That last point is what makes the failure confusing. Nothing was destroyed. The node is simply no longer **connected** to a document. Cypress's check is node-level: `Cypress.dom.isDetached()`, and the `Cypress.ensure.isAttached` guard that every action runs, ask whether *this* node is still connected to a document with a live window — not whether *an* element matching your selector is on screen. An element that was replaced and an element that was deleted fail exactly the same way. ## What the failure reads like As of Cypress 16 the message names the situation directly: *the page updated as a result of this command, but you tried to continue the command chain. The subject is no longer attached to the DOM, and Cypress cannot requery the page after commands such as …*. The two bullets Cypress prints under it are the two real causes — your framework re-rendered asynchronously, or your app code reacted to an event and removed the element. | chain | what happens when booking re-renders the list | |---|---| | `cy.get('[data-cy=room-row]').contains('Deluxe King').click().parent()` | `.parent()` receives the node `.click()` acted on, finds it detached, and throws | | `cy.get('[data-cy=room-row]').contains('Deluxe King').click()` then a separate `cy.get(...)` | the second statement runs its own query and resolves to the replacement row | | `cy.get('[data-cy=room-row]').contains('Deluxe King').click({ force: true })` | the attached check is skipped, so the click can fire at a node nobody can see | ## Writing the chain so it survives 1. **End the chain at the action.** Treat every action as a full stop. Anything you want to do afterwards — traverse, assert, act again — starts a new statement. 2. **Let a query find the replacement.** The new statement should begin with `cy.get()`, `cy.contains()` or an alias you re-read, so Cypress resolves against the page as it is now. 3. **Carry values, not elements, across the boundary.** If you need the price you saw before the action, read the string out and keep the string; keeping the element keeps the problem. ## What does not help - **A longer `defaultCommandTimeout`.** More time buys more attempts at the same fixed node. The node is gone; waiting does not bring it back, and only a fresh query can resolve to its replacement. - **`{ force: true }`.** Forcing skips the attached check rather than satisfying it. Cypress stops complaining, the event is dispatched at a node that is not in the document, and the application never hears it — a test that now passes while doing nothing. - **Wrapping the chain in `.then()`.** A callback is not retried, so a jQuery element you capture inside one is a snapshot with the same lifetime problem, not a protected reference. - **Re-running the failing step.** Retrying the same detached subject can only ever produce the same detached subject.

  • Does a longer Cypress timeout ever recover a detached subject?
    No. A timeout buys more attempts, and what is being retried is a subject already fixed to a node that has left the document, so every attempt reads the same dead node. Only a fresh query — a new `cy.get()`, or re-reading an alias — resolves to whatever replaced it.
  • In Cypress, does the element have to be removed for the subject to go detached?
    It only has to leave the document, which a framework re-render does even when the new markup is identical. Cypress's check is node-level: it asks whether that specific node is still connected to a live document, not whether a matching element is on screen. A replaced row and a deleted row fail identically.

It is like a key card for a hotel room the front desk has just rekeyed. The card is intact and the room is still there, but that card no longer opens anything.

saying these in an interview costs you the question

  • Says the element is gone when it was only replaced
  • Blames a slow app and raises the Cypress timeout
  • Adds force: true so the detached error disappears
  • Thinks Cypress re-runs the whole chain after an action completes
  • Treats an identical-looking replacement as the same element