fix(B04): prevent image URI flash across initial render and detail remounts

This commit is contained in:
Codex committed 2026-10-21 15:15:29 +09:00
1 parent 0b613e522d
commit 342820690f
7 files changed
+217 -24

No files matched your search

@@ -1,4 +1,4 @@
import { memo, useEffect, useState, type ReactElement } from 'react'
import { memo, useEffect, useMemo, useState, type ReactElement } from 'react'
import './image-renderer.css'
import {
isSafeImageUrl,
@@ -20,7 +20,13 @@ export interface MarkdownImageProps {
}
/** REA ev_717fa5e… src-DbE1_mgM.js [721564,722288); URL resolver is an explicit host adapter. */
export const MarkdownImage = memo(function MarkdownImage({
export const MarkdownImage = memo(function MarkdownImage(props: MarkdownImageProps): ReactElement {
// A changed source owns a fresh lifecycle and DOM image, including A → B → A.
// This also prevents the browser retaining the previous decoded image while loading.
return <MarkdownImageSource key={props.src} {...props} />
})
function MarkdownImageSource({
src,
alt = '',
urlList,
@@ -29,35 +35,40 @@ export const MarkdownImage = memo(function MarkdownImage({
preferHostResolution = false,
onPreviewImage
}: MarkdownImageProps): ReactElement {
const [resolvedUrl, setResolvedUrl] = useState<string>()
const [failed, setFailed] = useState(false)
const request = useMemo(() => ({ src, imageUrlResolver, preferHostResolution, onResolve }),
[src, imageUrlResolver, preferHostResolution, onResolve])
const [resolution, setResolution] = useState<{
request: typeof request
url?: string
failed?: boolean
}>(() => ({ request }))
const directUrl = isSafeImageUrl(src) && !(preferHostResolution && requiresHostImageResolution(src))
? src : undefined
// Reset this component before committing children, not in a passive effect:
// an immediate image error must belong to the new request too.
if (resolution.request !== request) setResolution({ request })
// Never display a previous resolver's result, even before effect cleanup runs.
const current = resolution.request === request ? resolution : undefined
const resolvedUrl = directUrl ?? current?.url
const failed = current?.failed === true
useEffect(() => {
let cancelled = false
setFailed(false)
if (directUrl) return
if (isSafeImageUrl(src) && !(preferHostResolution && requiresHostImageResolution(src))) {
setResolvedUrl(src)
return
}
setResolvedUrl(undefined)
setResolution({ request })
resolveImageUrl(src, imageUrlResolver, preferHostResolution).then((resolved) => {
if (cancelled) return
if (!resolved) {
setFailed(true)
return
}
setResolvedUrl(resolved)
if (resolved !== src) onResolve?.(src, resolved)
setResolution({ request, url: resolved, failed: !resolved })
if (resolved && resolved !== src) onResolve?.(src, resolved)
}).catch(() => {
if (!cancelled) setFailed(true)
if (!cancelled) setResolution({ request, failed: true })
})
return () => {
cancelled = true
}
}, [imageUrlResolver, onResolve, preferHostResolution, src])
}, [directUrl, imageUrlResolver, onResolve, preferHostResolution, request, src])
if (!resolvedUrl || failed) {
return (
@@ -66,7 +77,7 @@ export const MarkdownImage = memo(function MarkdownImage({
aria-busy={!failed}
role="img"
>
{alt || src}
{alt || (failed ? '图片加载失败' : '图片加载中…')}
</span>
)
}
@@ -83,9 +94,10 @@ export const MarkdownImage = memo(function MarkdownImage({
className="cr-image-block markdown-content__image"
src={resolvedUrl}
alt={alt}
onError={() => setFailed(true)}
onError={() => setResolution((previous) => previous.request === request
? { request, failed: true } : previous)}
onClick={openPreview}
draggable={false}
/>
)
})
}