Noah
20fe058b15
fix(ui): 修复 React Compiler lint 命中并恢复规则为 error (#1711)
* fix(ui): 主题与侧栏偏好改用 useSyncExternalStore 读取,去掉 effect 内 setState
react-hooks/set-state-in-effect 命中 4 处,都是「挂载后在 effect 里读浏览器状态再 setState」:
- app/(app)/layout.tsx:117 侧栏折叠偏好、:136 主题模式:两者都是 localStorage 里的值。
新增 useLocalStorageValue(key),用 useSyncExternalStore 订阅:服务端与水合期快照固定为
null(与 SSR 输出一致,不会水合不一致),水合完成后 React 自己用真实值重渲染一次;
写入走 setter 并通知本页订阅者。localStorage 写失败时退回内存值,本页内仍可切换,
与原先 try/catch 后照常 setState 的行为一致。
- app/lib/utils/index.tsx:66 useSystemTheme:matchMedia 是外部状态,改为 useSyncExternalStore
订阅 change 事件,服务端快照 light,与原来 useState('light') 再 effect 校正的结果一致。
- app/ui/ThemeButton.tsx:30:原来点击后先置 switchTrigger,再在 effect 里读标志位切主题、
setMode、清标志位,图标也在 effect 里 setState。改为点击事件里直接算出下一模式并
applyThemeMode + setMode,图标由 mode 派生,不再有中间 state 与 effect;切换结果相同,
少一轮渲染。setMode 的类型收窄为 (mode: string) => void,调用方只传字符串。
useTheme 未改:它的首次跳过逻辑仍然成立(首帧主题由 <head> 内联脚本设置)。
* fix(ui): 日志页、空间配置页、账号列表与模板表单去掉 effect 内同步 setState
react-hooks/set-state-in-effect 其余 5 处:
- app/(app)/logviewer/page.tsx:133:connectWebSocket() 开头同步 setIsLoading(true)/setLogs([])
再建连接,effect 与「刷新」按钮共用。拆开:effect 只负责建立 WebSocket、把消息写进
state、cleanup 时关闭(上一条连接不再靠 ref 手动 close);清空日志 + 进入加载态移到触发
重连的事件里(切 Tab 的 onChange、刷新按钮),刷新通过 connectSeq 计数器让 effect 重跑。
首次挂载时两个 state 本来就是初始值,其余时序与原来一致;顺带删掉不再需要的
exhaustive-deps 禁用注释。
- app/(app)/dashboard/page.tsx:101:校验失败后先切 Tab / 平台,再在 effect 里滚到出错字段并
把 pendingField 清空。改为 flushSync 同步提交切面板的 state,随后直接 scrollIntoView +
focus(React 文档推荐的「更新后读 DOM」写法),去掉 pendingField 这个中间 state。
onSubmitFail 由 Semi 在 Promise.catch 里调用,不在渲染期,flushSync 合法。
- app/lib/use-streamers.ts:26 useBiliUsers:data 为空时同步 setList([])。改为返回值直接派生
(无账号 → 空数组常量),effect 里只保留异步补全;data 为空数组时 Promise.all 解析为 []
仍会把上一批结果清掉,之后再有账号不会先闪旧数据。
- app/ui/TemplateFields.tsx:183:挂载后从 formApi.getValue('dtime') 算滚轮初始选中值。
Semi Form 在构造函数里就把 initValues 放进 store,首次渲染即可读到,改为 useState 的
惰性初始值(换算逻辑抽成 delayFromDtime),结果与 effect 版本相同。
- app/(app)/components/BackgroundSetter.tsx:71:hasBg / opacity 挂载后从 localStorage 读。
改为 useState 惰性初始值;两个值只在面板展开后渲染,SSR 首帧输出不受影响。
* fix(ui): 修复渲染期不纯写法:卡片实时时长、平台插件查找、time_range 转换
- react-hooks/purity app/ui/StreamerCard.tsx:101:渲染期调 Date.now() 算「已直播多久」。
新增共享时钟 useNowSec()(app/lib/use-dashboard.ts):模块级 store 由定时器每秒推进,
组件用 useSyncExternalStore 读取,首个订阅者接入时校准一次。时长拆成 LiveDuration 子组件,
只有录制中的卡片挂载它,其余卡片不随时钟重渲染。显示值语义不变(开播至今,同一
formatDuration),刷新节奏从「随 10 s 轮询重渲染时更新」变为每秒更新。timeAgo 内部也用
Date.now() 但未被规则命中(在工具函数里),本次不动。
- react-hooks/static-components app/ui/OverrideModal.tsx:297:JSX 里用 IIFE 调 platformSetting()
拿到组件再渲染,规则把它当成「渲染期创建的组件」。实际是按 url 从模块级常量表
SupportedPlatforms 取组件。改为先找出匹配的键,再按键从常量表取值,让「同一引用」这一点
在代码上可见;匹配顺序与结果不变。
- react-hooks/immutability app/ui/TemplateModal.tsx:121:把 props.entity.time_range 由 JSON
字串就地改写成 Date[]。改为 useMemo 生成一份本地副本作为 Form 的 initValues,解析失败
保持原样并 console.error(同原逻辑),不再修改上层传下来的对象。
* chore(lint): 恢复 React Compiler 系列规则为 eslint-plugin-react-hooks 的默认 error
#1710 升级 eslint-plugin-react-hooks 7 时把 set-state-in-effect / static-components /
purity / immutability 四条降为 warn,留给专门 PR 处理。命中的 12 处已在前三个 commit 修完,
这里删掉覆盖,回到插件 recommended 的 error 级别。
* fix(ui): useTheme 不再回写 localStorage,mode 只由 useLocalStorageValue 的 setter 持久化
mode 改由 useSyncExternalStore 从 localStorage 读取后,同一个键有了两个写入方。
StrictMode 下 effect 会在挂载时执行两次,useTheme 的「跳过首次」只能挡住第一次,
第二次会用水合期的默认值 auto 把用户刚保存的主题覆盖掉,再由 store 读回 auto——
`next dev` 里每次刷新主题都会丢(生产构建没有双次 effect,不受影响)。
去掉 useTheme 里的 setItem:切换主题时 setter 已经写过同一个值;系统配色变化时原来会
把 auto 写进去,去掉后键保持缺省,读出来同样按 auto 处理,行为一致。
* fix(ui): 登录页在手机宽度下卡片超出视口、两侧被裁切
review 意见(#1711 下 ForgQi):手机尺寸下登录页两侧被裁切。
原因:三个输入框写死 style={{ width: 440 }},卡片宽度由它们撑成 552px(440 + 56×2 内边距),
.main 居中放置、.frame 又是 overflow: hidden,窄于 552 的屏幕上卡片两侧直接被裁掉。
改法:去掉输入框的固定宽度,卡片定宽 552px 并允许 max-width: 100% 收窄到视口,
.inputs 改为 align-items: stretch 让字段撑满卡片内容区(Semi 的 .semi-input-wrapper 默认 width: 100%),
≤480px 时内边距收到 32px 24px。桌面 1440 浅 / 深色截图与修改前像素 0 差异;390px 下卡片 370px、
输入框 322px、无横向溢出,注册模式(三个输入框)同样。
2026-09-22 08:27:12 +00:00
..
2026-09-21 13:46:15 +00:00
2026-09-22 08:27:12 +00:00
2026-09-21 13:46:15 +00:00
2026-09-21 13:46:15 +00:00
2026-09-21 13:46:15 +00:00
2026-09-21 13:46:15 +00:00