Skip to content

fix: keep Image error state in React so images recover - #1125

Open
ebeigarts wants to merge 1 commit into
thesysdev:mainfrom
mitigate-dev:fix/image-sticky-error-hide
Open

fix: keep Image error state in React so images recover#1125
ebeigarts wants to merge 1 commit into
thesysdev:mainfrom
mitigate-dev:fix/image-sticky-error-hide

Conversation

@ebeigarts

@ebeigarts ebeigarts commented Sep 4, 2026

Copy link
Copy Markdown

What

Image sometimes renders a valid, fully-loaded image that is invisible.

The error path wrote display: none straight onto the DOM node:

onError={(e) => {
  e.currentTarget.style.display = "none";
  console.error(`Failed to load image: ${src}`);
}}

React does not manage that inline style — style={styles} is normally undefined, so React never writes a style attribute and never has a reason to overwrite it. The hide therefore survives every later render, including a change of src. There is no onLoad to undo it and no retry, so one failure hides that image for the rest of the session.

Streaming makes this routine rather than rare. The <img> mounts while src is still arriving — autoClose() (packages/lang-core/src/parser/statements.ts) terminates the unterminated string so partial input parses, and nothing in the renderer skips nodes flagged partial, so a truncated URL is a valid prop and gets rendered. The browser fires error on it, display: none is written, and then the completed URL arrives, loads fine, and stays hidden.

Observed in the wild as a correct src that returns HTTP 200 sitting next to style="display: none;":

<img alt="" class="openui-image openui-image-fit"
     src="https://www.example.com/product-images/…-thumb.jpg"
     style="display: none;">

ImageBlock already handles this correctly (React state, and onLoad resets hasError); this brings Image in line with it.

Changes

  • packages/react-ui/src/components/Image/Image.tsx — track the failure in React state (hasError), set it in onError, clear it in onLoad, and apply the hide through an openui-image--error class instead of mutating node.style.
  • packages/react-ui/src/components/Image/image.scss — add &--error { visibility: hidden; }, matching the --error modifiers in imageBlock.scss.

Behaviour after the change: the truncated src errors and hides, the completed URL is committed, the browser loads it (neither visibility nor display blocks the fetch), onLoad fires, and the image becomes visible.

Test Plan

  • Verified locally

Verified the browser semantics this fix relies on, in Chrome 152, logging every load/error with the src at dispatch time:

sequence events fired
truncated src (404), settle, then full URL error(truncated) → load(full)
truncated src, swap to full URL after 1 ms (mid-flight) load(full) only
good URL, swap to a 404 after 1 ms error(404) only

An aborted request delivers nothing, matching the spec's abort the image request ("discarding any pending tasks generated by that algorithm"), so an <img> only ever reports events for its current request and a boolean flag cannot be poisoned by a stale event.

Also confirmed the reported URL is healthy (HTTP 200, image/webp, 34 KB, with and without a Referer), i.e. the hide was self-inflicted rather than a broken asset.

No regression test is included: react-ui has no jsdom or testing-library setup and its existing component tests are SSR-string only, so real load/error events cannot be exercised. jsdom is already in the workspace catalog and used by react-lang, so I can add it and write a test here if you would like that in this PR.

Checklist

  • I linked a related issue, if applicable
  • I updated docs/README when needed
  • I considered backwards compatibility

On the issue link: I did not find an existing issue for this, and I opened this PR before filing one, contrary to the issue-first flow in CONTRIBUTING.md — happy to open an issue and move the discussion there if you prefer.

On backwards compatibility: the hide moves from an inline display: none to a class applying visibility: hidden. Inside the AspectRatio.Root wrapper the element already occupies a fixed box, so the two look the same, and visibility matches ImageBlock. Anyone who was targeting the old inline style would be affected, but the new openui-image--error class is a more stable hook. The console.error on failure is dropped to match ImageBlock; it was also firing on every partial-src mount, i.e. for images that load fine a moment later.

Note for reviewers

This fix stops the symptom but not the cause: the renderer still mounts partial nodes, so the request for the truncated URL is still made and still fails. Deferring <img> mounting (or debouncing src) while a node is partial would fix it at the source and help any other URL-valued prop with the same shape. Out of scope here — let me know if you would like an issue for it.

Image hid failed loads by writing display:none directly to the DOM node.
React never manages that inline style, so the hide survived every later
render: once an image errored it stayed invisible for the rest of the
session, even after src changed to a working URL.

Streaming makes this routine. The <img> mounts while src is still
arriving (the parser auto-closes the unterminated string, so a truncated
URL is a valid prop), the browser errors on it, and the completed URL
then loads fine but stays hidden.

Track the failure in state and clear it on load, hiding via an --error
class, matching ImageBlock.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Sep 4, 2026

Copy link
Copy Markdown

@ebeigarts is attempting to deploy a commit to the thesys-devs Team on Vercel.

A member of the Team first needs to authorize it.

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.

1 participant