Skip to content

fix: search page removeChild crash — stop mutating React-owned no-results message - #795

Merged
n00bcodr merged 1 commit into
mainfrom
fix/react-safe-no-results-message
Aug 30, 2026
Merged

fix: search page removeChild crash — stop mutating React-owned no-results message#795
n00bcodr merged 1 commit into
mainfrom
fix/react-safe-no-results-message

Conversation

@4eh5xitv6787h645ebv

Copy link
Copy Markdown
Collaborator

Summary

Fixes the NotFoundError: Failed to execute 'removeChild' on 'Node' crash on the search page (the bug behind #792) with a minimal, root-cause-targeted change — no features removed.

Root cause

jellyfin-web's SearchResults renders both the no-results message and the results container as plain keyless divs, so React reuses the same DOM node when switching between those states. With react-query's cache, retyping a previously-searched query transitions directly between them (no <Loading/> commit in between). The plugin's positionSection() did:

noResultsMessage.textContent = desiredText;

on the React-owned message div. That replaces React's own text node — so on the next reconciliation of that (reused) div, React calls removeChild on a node that no longer exists and throws. This matches the reported repro exactly: search → delete part → type another string → delete → correct it, a few times, with a no-results state along the way.

Notably, merely inserting the Seerr section among React's children is tolerated by the reconciler — the text mutation was the sole trigger.

Fix

  • Never mutate the React-owned message: hide it with a CSS class (attribute-only changes are safe) and show a plugin-owned replacement message with the same localized text beside it.
  • New clearInjectedSearchResults() helper removes everything the plugin injected and restores the native message; all four cleanup paths (new query, query cleared, navigation away, Seerr inactive) go through it so no stale takeover survives.
  • Seerr-only filter toggle now handles both messages.

3 files, +50/−14. Inline placement, infinite scroll, the Seerr-only filter, the search-field icon, and unbounded results all keep working.

Verification

  • Deterministic repro (React 18 + jsdom harness mimicking the jellyfin-web search page): old code crashes with the exact reported error inside react-dom's text commit; fixed code survives the same input gauntlet (24 no-results takeovers) with zero errors and no leftover nodes.
  • Real environment: built the plugin (JellyfinTarget=jf12, .NET 10) and ran it in fresh jellyfin/jellyfin:12.0-rc6 and jellyfin/jellyfin:unstable containers with a scanned movie library and a stubbed Seerr backend, driving the real web UI with headless Chromium through the reporter's repro sequence (type/delete/retype across result and no-result queries):
    • stock plugin: crashes on both images with the exact reported error (node_modules.react-dom.bundle.js, search page torn down by React Router)
    • this branch: 0 errors on both images, Seerr section + cards render inline, no-results takeover shows the localized message and restores the native one cleanly when results return
  • node --check passes on all touched files; git diff --check clean.

Why not #792's approach

Tested that branch in the same environment: the crash disappears, but only because the integration stops running entirely — jellyfin-web rewrites ?query= on every keystroke, and that branch's onNavigate teardown cancels its own pending search each time, so no Seerr request is ever made and nothing renders (details in the comment on #792).

The search page's no-results message is React-owned, and jellyfin-web's
SearchResults renders both the message and the results container as plain
keyless divs — so React reuses the same DOM node across those states, and
with react-query's cache a previously-typed query transitions directly
between them with no <Loading/> commit in between. Overwriting the
message's textContent replaced React's own text node, so the next
reconciliation of that div threw:

  NotFoundError: Failed to execute 'removeChild' on 'Node': The node to
  be removed is not a child of this node.

Reproduced deterministically (React 18 + jsdom harness mimicking the
jellyfin-web search page) by typing, deleting, and retyping queries with
a no-results query in between — exactly the reported repro. The crash
disappears with this change and remains absent across the same input
sequence.

Instead of rewriting the native message's text, hide it with a CSS class
(attribute-only changes are safe on React-owned nodes) and insert a
plugin-owned replacement message beside it. All cleanup paths now go
through clearInjectedSearchResults(), which also removes the replacement
message and unhides the native one, so no stale takeover survives query
changes, navigation, or Seerr deactivation.

Fixes the crash behind PR #792 with a minimal, targeted change.
@github-actions

Copy link
Copy Markdown

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

@n00bcodr
n00bcodr merged commit d03df5d into main Aug 30, 2026
8 checks passed
@n00bcodr
n00bcodr deleted the fix/react-safe-no-results-message branch August 30, 2026 15:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants