fix(web): 注册卡片 async 回调加卸载守卫(I8);核实 I7 res.text() 已在 try 内(无需改)
RegisterCard.jsx onSendCode/onRegister 里 await callApi 期间组件若已卸载(用户导航离开
注册页),之后的 setState 会打到已卸载组件——加 mountedRef 守卫,对齐文件内既有
cooldownRef/redirectRef 的 unmount 清理写法。InviteCard.jsx onCopy 的 1.5s setTimeout
从未 clear,存 ref 并在卸载时 clear。
I7(callApi 里 res.text()/res.json() 读 body 阶段断连,声称在 try 之外)经核实:
callApi 自创建(65ce266)起,res.text()+JSON.parse 就已在独立 try/catch 内(279-286行),
断连会被捕获、data 置 null,不会产生 unhandled rejection;未做改动。
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P9G7E3wmAYL9KeYCVZVsqu
This commit is contained in:
@@ -18,7 +18,7 @@
|
||||
* ——与主页行为一致,用户可用 Header 的语言下拉自行切换(切换会带上当前的邀请码路径,见
|
||||
* Header.jsx 的 `pagePath` 机制)。
|
||||
*/
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Copy, Check, Download, UserPlus, Gift, ArrowRight } from 'lucide-react';
|
||||
import { isLoggedIn } from '../lib/authState';
|
||||
import { INVITE_CODE_RE } from '../lib/inviteCode';
|
||||
@@ -163,12 +163,17 @@ export default function InviteCard({ lang = 'en' }) {
|
||||
const [code, setCode] = useState(null);
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [loggedIn, setLoggedIn] = useState(false);
|
||||
// onCopy 里 1.5s 后把 copied 复位的 setTimeout 存 ref,卸载时 clear——
|
||||
// 否则用户复制后立刻离开页面,定时器仍会在已卸载组件上触发 setState。
|
||||
const copyTimeoutRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
setCode(extractCode(window.location.pathname));
|
||||
setLoggedIn(isLoggedIn());
|
||||
}, []);
|
||||
|
||||
useEffect(() => () => clearTimeout(copyTimeoutRef.current), []);
|
||||
|
||||
const t = COPY[lang] || COPY.en;
|
||||
const dlHref = useMemo(() => `${langPath(lang)}#download`, [lang]);
|
||||
const registerHref = useMemo(
|
||||
@@ -181,7 +186,8 @@ export default function InviteCard({ lang = 'en' }) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(code);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 1500);
|
||||
clearTimeout(copyTimeoutRef.current);
|
||||
copyTimeoutRef.current = setTimeout(() => setCopied(false), 1500);
|
||||
} catch {
|
||||
// 剪贴板权限失败时静默,用户仍可长按/选中手动复制。
|
||||
}
|
||||
|
||||
@@ -311,6 +311,9 @@ export default function RegisterCard({ lang = 'en' }) {
|
||||
const [redirectIn, setRedirectIn] = useState(REDIRECT_SECS);
|
||||
const redirectRef = useRef(null);
|
||||
const codeInputRef = useRef(null);
|
||||
// 卸载守卫:onSendCode/onRegister 里 await callApi 期间组件若已卸载(用户导航离开),
|
||||
// 之后的 setState 直接跳过——避免 unmounted 组件 setState 的告警/竞态。
|
||||
const mountedRef = useRef(true);
|
||||
|
||||
const t = COPY[lang] || COPY.en;
|
||||
// 后端错误字段只有 message_zh/message_en 两语;非中文一律取英文兜底(见组件顶部注释)。
|
||||
@@ -334,6 +337,7 @@ export default function RegisterCard({ lang = 'en' }) {
|
||||
|
||||
useEffect(() => () => clearInterval(cooldownRef.current), []);
|
||||
useEffect(() => () => clearInterval(redirectRef.current), []);
|
||||
useEffect(() => () => { mountedRef.current = false; }, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (codeSent && codeInputRef.current) codeInputRef.current.focus();
|
||||
@@ -381,6 +385,7 @@ export default function RegisterCard({ lang = 'en' }) {
|
||||
}
|
||||
setSendingCode(true);
|
||||
const r = await callApi('/v1/auth/code', { email: trimmed });
|
||||
if (!mountedRef.current) return;
|
||||
setSendingCode(false);
|
||||
if (!r.ok) {
|
||||
setCodeErr(r.network ? t.netErr : (r.error && r.error[msgKey]) || t.netErr);
|
||||
@@ -403,6 +408,7 @@ export default function RegisterCard({ lang = 'en' }) {
|
||||
const body = { email: trimmedEmail, code, password };
|
||||
if (invite) body.invite_code = invite;
|
||||
const r = await callApi('/v1/auth/register', body);
|
||||
if (!mountedRef.current) return;
|
||||
setSubmitting(false);
|
||||
if (!r.ok) {
|
||||
setRegErr(r.network ? t.netErr : (r.error && r.error[msgKey]) || t.netErr);
|
||||
|
||||
Reference in New Issue
Block a user