In React Native Testing Library, what does waitForElementToBeRemoved require before it waits, and when is waitFor with a queryBy check the better fit?
answer
- the element must exist first
- takes a callback, not an element
- removed means throws, null or empty
- fast mocks can skip the loader
- waitFor plus queryBy for absence
basics
~20 swaitForElementToBeRemoved takes a query callback whose element must exist on the first call, or it throws at once, then polls until the query throws or returns null or an empty array. If the element may already be gone, use waitFor with queryBy.
solid answer
~40 s`waitForElementToBeRemoved(() => screen.getByText('Loading catalog…'))` is RNTL's disappearance helper. It calls the callback immediately and **requires** a match: if the query returns nothing then, it throws right away with an 'already removed' error. After that it polls through `waitFor` with the same 1000 ms / 50 ms defaults, treating the element as removed once the callback throws (a `getBy*`) or returns `null` or an empty array (a `queryBy*`/`queryAllBy*`). In RNTL it must be given a callback; passing an element fails. It fits when the loader is guaranteed to render first. When a fast mock may resolve before your first check, the precondition becomes a flaky failure, so prefer `await waitFor(() => expect(screen.queryByText('Loading catalog…')).not.toBeOnTheScreen())`, or simply wait for the loaded content with a `findBy*` query.
code
typescript · 13 linesimport { render, screen, waitForElementToBeRemoved } from '@testing-library/react-native';
test('shows the loader, then the catalog', async () => {
let resolveBooks!: (books: Book[]) => void;
mockFetchBooks.mockReturnValue(new Promise((resolve) => { resolveBooks = resolve; }));
await render(<CatalogScreen />);
expect(screen.getByText('Loading catalog…')).toBeOnTheScreen();
resolveBooks([{ id: 'b1', title: 'Dune' }]);
await waitForElementToBeRemoved(() => screen.getByText('Loading catalog…'));
expect(screen.getByText('Dune')).toBeOnTheScreen();
});go deeper
Recall that waitForElementToBeRemoved needs the element to be on screen first and takes a function that queries it.
Explain its steps: an initial required match, then waitFor polling where a throw, null or empty array means removed, with the usual timeout and interval.
Spot the flake a fast mock causes with the precondition, and choose between the removal helper, waitFor with queryBy, and waiting on the loaded content instead.
Decide what loading-state behaviour deserves its own tests with controlled mocks, and keep the rest of the suite waiting on results rather than on transient indicators.
## What the helper is for Some UI changes are a **disappearance**: a "Loading catalog…" indicator goes away when books arrive, an error toast closes, a modal is dismissed. **React Native Testing Library (RNTL)** has a dedicated helper for this, `waitForElementToBeRemoved`, alongside `findBy*` (waiting for appearance) and `waitFor` (waiting for anything). ```tsx await render(<CatalogScreen />); await waitForElementToBeRemoved(() => screen.getByText('Loading catalog…')); expect(screen.getByText('Dune')).toBeOnTheScreen(); ``` ## How it works, step by step 1. It calls your callback once, synchronously. You may use a `getBy*`, `getAllBy*`, `queryBy*` or `queryAllBy*` query inside it. 2. If that first call finds nothing (returns `null` or an empty array), it **throws immediately**: the element must be present before waiting for its removal makes sense. 3. Otherwise it hands a checking function to `waitFor`, which polls every `interval` (50 ms) until `timeout` (1000 ms, or `asyncUtilTimeout` if configured). 4. Each poll runs your callback again. The element counts as **removed** when the callback **throws** or returns `null` or an empty array. While it still finds something, the poll fails and is retried. 5. On success the promise resolves with the elements found on the first call; on timeout it rejects with "Timed out in waitForElementToBeRemoved." It accepts the same `timeout`, `interval` and `onTimeout` options as `waitFor`, and under Jest fake timers it advances the fake clock the way `waitFor` does, because it is built on it. ## Under fake timers and on success Because it delegates to `waitFor`, the helper behaves the same way under Jest fake timers: each poll advances the fake clock by the interval, so a loader hidden by a 2000 ms timer needs `{ timeout: 2000 }` or more, measured in fake milliseconds. On success the promise resolves with what the **first** call returned, the elements that were present, which is rarely useful beyond confirming what was waited on. On failure the error comes from RNTL rather than from your query, so the message names the helper instead of repeating the query text; pass `onTimeout` with `screen.debug()` to see the tree. ## RNTL-specific details - **It takes a callback only.** RNTL calls the argument as a function on the first line, so `waitForElementToBeRemoved(screen.getByText('Loading catalog…'))` fails with a type error instead of waiting. - **The callback must re-query.** Returning a captured variable, `() => loader`, would find the same object every time and never report removal. - It must be **awaited**, like every async utility in RNTL. ## The precondition is the trap The "must exist first" rule is the helper's value: it proves the loader really was shown, catching a screen that never displays its loading state. It is also its weakness. In the catalog test, the API mock resolves on the next microtask, and `await render()` flushes React's queued work; depending on how the fetch chain is written, the books can already be in the tree when the helper's first call runs. The helper then throws "already removed", and the test fails for reasons unrelated to the feature. | Situation | Best tool | |---|---| | Loader is guaranteed to render first and you want to prove it | `waitForElementToBeRemoved` | | Loader may already be gone; you only care it is gone | `waitFor` + `queryBy*` + `.not.toBeOnTheScreen()` | | What you really care about is the loaded content | `findBy*` on the content, then `queryBy*` for the loader | The `waitFor` form succeeds immediately if the loader is already gone and polls if not: ```tsx await waitFor(() => expect(screen.queryByText('Loading catalog…')).not.toBeOnTheScreen(), ); ``` A `getBy*` would be wrong in that callback, because `getBy*` throws when the element is absent, which would be read as "not yet". ## Choosing in practice - If the test is **about** the loading state (it must appear, then go), assert its presence explicitly after render, with a mock that stays pending until the test releases it, then use `waitForElementToBeRemoved`. - If the test is about the **result**, wait for the result with `findBy*`; the loader's absence can then be asserted synchronously with `queryBy*`. - Do not add sleeps to "make the loader visible"; control the mock instead.
- Why does passing a getBy query in the callback work for detecting removal, even though getBy throws when nothing matches?After the first successful call, waitForElementToBeRemoved treats a throw from the callback as "the element is gone". So a getBy* that throws once the loader unmounts is exactly the removal signal, while the same throw on the very first call is ruled out by the precondition check.
- The loader disappears but the helper times out. What is a likely cause?The callback still matches something: another element with the same text, such as a second loader in a list footer, or a callback that returns a captured element instead of re-querying. Narrow the query, for example with `within` on the right container, and make sure the callback runs a fresh query each time.
saying these in an interview costs you the question
- waitForElementToBeRemoved resolves at once if the element is already gone.
- Passing the element itself is the recommended way to call it in RNTL.
- Inside the removal wait you must use queryBy, because getBy throwing breaks it.
- waitFor with getBy inside is a correct way to wait for absence.
- Add a short sleep so the loader is visible before calling the helper.