修复我的收藏/查找聊天记录加载失败时卡死在"加载中…",没有任何错误提示或重试入口

根因:Favorites 和 History(content-library-view.jsx)两个组件的 load() 里,请求失败的
.catch() 只调用 onToast() 弹一条 2 秒自动消失的提示,从未把 items 从初始值 null 变成别的
状态;页面渲染逻辑是"items === null 就显示'加载中…'",两者叠加导致一次真实失败之后页面永远
停在"加载中…",等用户反应过来去看的时候提示早就消失了,分不清"还在加载"和"永远不会加载完"。
这个坑本文件自己的 DownloadPage、group-management-view.jsx 的 GroupFiles、
contact-management-view.jsx 的 Profile 三处早就各自修过(都有一个独立的 loadError 状态),
唯独 Favorites/History 这两个用得最多的列表页当时漏掉了。

真实触发场景:用两个全新账号互加好友、发送图片/视频/文件/语音/文本等真实消息并收藏后,
发现收藏一条较大的(out-of-band)文件附件之后,GET /favorites?type=all 从此稳定返回
500 INTERNAL_ERROR(已确认是纯后端问题,不在前端能修的范围内,另行记录)——但这个后端
问题一暴露出来,"我的收藏"默认落地的"全部"tab 就会用真实数据 100% 复现这里描述的前端缺口:
打开就卡在"加载中…",没有任何办法知道发生了什么、也无法重试。

修复:Favorites/History 都新增 loadError 状态,非 append(首次/切换 tab、类型、搜索词)请求
失败时置为 true、成功时置回 false,切换 type/topic/view/query 时重置;渲染时 items === null
且 loadError 为真则显示"加载失败,请重试"而不是死等的"加载中…"。"加载更多"(append)请求
失败保持原样只弹 toast,不影响已经显示出来的列表。

验证:复现上述 500 场景(GET /favorites?type=all 持续 500),修复前"我的收藏"全部 tab
永久停在"加载中…";重新构建后同一账号同一坏数据状态下正确显示"加载失败,请重试"。
This commit is contained in:
Your Name
2026-09-06 16:39:13 +08:00
parent 0d2637861d
commit 3790472b4c
+30 -4
View File
@@ -290,6 +290,16 @@ function Favorites({view, hashParams, tinode, onToast}) {
const [items, setItems] = React.useState(() => readCachedData('favorites:' + type) || null);
const [cursor, setCursor] = React.useState('');
const [hasMore, setHasMore] = React.useState(false);
// A failed initial load (no cached list to fall back on) previously left this stuck on "加载中…"
// forever -- the catch handler only fired a toast, which fades in 2s, and `items` never left null
// so the loading branch below never had anything else to show. Same fix already applied to this
// file's own DownloadPage/GroupFiles(group-management-view.jsx)/Profile(contact-management-view.jsx)
// for the identical reason; Favorites/History were missed. Concretely hit live: favoriting a large
// (out-of-band) file attachment makes the *server* 500 on GET /favorites?type=all from then on
// (a real backend bug, tracked separately -- not something the frontend can prevent), and every
// subsequent visit to "全部" silently hung on the loading spinner with only a 2s toast, no way to
// tell "still loading" from "never going to load", no retry affordance.
const [loadError, setLoadError] = React.useState(false);
const [fileUrls, setFileUrls] = React.useState({});
const [sourceNames, setSourceNames] = React.useState({});
// An in-flight request isn't guaranteed to resolve before a newer one its own type/query change
@@ -302,6 +312,7 @@ function Favorites({view, hashParams, tinode, onToast}) {
const requestId = ++requestIdRef.current;
getFavorites({type, query, cursor: append ? cursor : ''}).then(data => {
if (requestIdRef.current !== requestId) return;
setLoadError(false);
setItems(prev => {
const list = append ? (prev || []).concat(data.items || []) : (data.items || []);
if (!query) writeCachedData('favorites:' + type, list);
@@ -309,9 +320,14 @@ function Favorites({view, hashParams, tinode, onToast}) {
});
setCursor(data.next_cursor || '');
setHasMore(!!data.next_cursor);
}).catch(err => onToast(err.message || '加载失败'));
}).catch(err => {
if (requestIdRef.current !== requestId) return;
onToast(err.message || '加载失败');
if (!append) setLoadError(true);
});
}, [type, query, cursor]);
React.useEffect(() => {
setLoadError(false);
setItems((!query && readCachedData('favorites:' + type)) || null);
load(false);
}, [type, query]);
@@ -380,7 +396,7 @@ function Favorites({view, hashParams, tinode, onToast}) {
// 类型列表内的勾选,没有跨 tab 累加的设计)。
onClick={() => { setType(id); setSelected([]); if (TYPE_ROUTE[id] && !selecting) navigateReplace(TYPE_ROUTE[id]); }}>{label}</button>)}</nav>
<main className="library-favorite-list">
{items === null ? <p className="library-empty-hint">加载中…</p> :
{items === null ? <p className="library-empty-hint">{loadError ? '加载失败,请重试' : '加载中…'}</p> :
list.length ? <>{list.map(item => <FavoriteItem key={item.favorite_id} item={{...item, _thumbUrl: fileUrls[item.favorite_id],
source_display: sourceNames[item.source && item.source.topic]}}
selecting={selecting} selected={selected.includes(item.favorite_id)} onSelect={() => toggle(item.favorite_id)}
@@ -614,6 +630,10 @@ function History({view, hashParams, myUserId, onToast}) {
const [items, setItems] = React.useState(() => (cacheKey && readCachedData(cacheKey)) || null);
const [cursor, setCursor] = React.useState('');
const [hasMore, setHasMore] = React.useState(false);
// Same "stuck on 加载中… forever" gap as Favorites' loadError above, same fix -- a failed initial
// load with nothing cached to fall back on left this on the loading branch permanently, with only
// a 2s toast (already faded by the time anyone looks back) as the only sign anything went wrong.
const [loadError, setLoadError] = React.useState(false);
const [mediaUrls, setMediaUrls] = React.useState({});
const [senderNames, setSenderNames] = React.useState({});
// Same stale-response guard as Favorites' load() above and for the same reason -- a slower
@@ -628,15 +648,21 @@ function History({view, hashParams, myUserId, onToast}) {
getTopicMedia(topic, type, append ? cursor : '');
call.then(data => {
if (requestIdRef.current !== requestId) return;
setLoadError(false);
setItems(prev => {
const list = append ? (prev || []).concat(data.items || []) : (data.items || []);
if (!query && cacheKey) writeCachedData(cacheKey, list);
return list;
});
setCursor(data.next_cursor || ''); setHasMore(!!data.next_cursor);
}).catch(err => onToast(err.message || '加载失败'));
}).catch(err => {
if (requestIdRef.current !== requestId) return;
onToast(err.message || '加载失败');
if (!append) setLoadError(true);
});
}, [topic, view, query, cursor]);
React.useEffect(() => {
setLoadError(false);
setItems((!query && cacheKey && readCachedData(cacheKey)) || null);
load(false);
}, [topic, view, query]);
@@ -687,7 +713,7 @@ function History({view, hashParams, myUserId, onToast}) {
{view === 'history-search' ? <Search value={queryInput} onChange={setQueryInput} placeholder="搜索当前会话" /> : null}
<HistoryNav active={view} />
<main className="library-history-content">
{items === null ? <p className="library-empty-hint">加载中…</p> : !list.length ?
{items === null ? <p className="library-empty-hint">{loadError ? '加载失败,请重试' : '加载中…'}</p> : !list.length ?
<div className="library-empty"><Icon>{view === 'history-search' ? 'search_off' : 'inbox'}</Icon><strong>暂无内容</strong></div> :
view === 'history-search' ? <section className="library-message-results">
{/* 同一处"问题13"修复暴露的缺口,History 自己的一份,只影响 type==='text':非 text 类型