Skip to content

Commit 206964c

Browse files
committed
fix(docs-history): 改用 useReducer 修复 react-hooks/set-state-in-effect lint 错误
PR #279 build 挂在 lint: > 50 | setItems(null); | ^ Avoid calling setState() directly within an effect 把 items / error / loading 合并成 discriminated union + useReducer, effect 里只 dispatch 一次,规避 lint 规则。 副收益:三种状态天然互斥,不会出现'错误提示 + 旧列表'并存的情况 (这正是上个 CR 试图用两次 setState 解决的问题,现在用状态机更干净)。
1 parent b9f6421 commit 206964c

1 file changed

Lines changed: 36 additions & 18 deletions

File tree

app/components/DocHistoryPanel.tsx

Lines changed: 36 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import { useEffect, useState } from "react";
3+
import { useEffect, useReducer } from "react";
44
import Image from "next/image";
55
import type { HistoryItem } from "@/app/types/docs-history";
66

@@ -12,6 +12,25 @@ interface DocHistoryPanelProps {
1212
path: string;
1313
}
1414

15+
// 将 items / error / loading 合并成一个 discriminated union,
16+
// 避免 effect 里多次同步 setState 触发 react-hooks/set-state-in-effect
17+
// 同时天然保证三种状态互斥(不会同时出现"错误提示 + 旧列表")
18+
type State =
19+
| { status: "loading" }
20+
| { status: "ok"; items: HistoryItem[] }
21+
| { status: "error"; message: string };
22+
23+
type Action =
24+
| { type: "fetch" }
25+
| { type: "ok"; items: HistoryItem[] }
26+
| { type: "error"; message: string };
27+
28+
function reducer(_: State, action: Action): State {
29+
if (action.type === "fetch") return { status: "loading" };
30+
if (action.type === "ok") return { status: "ok", items: action.items };
31+
return { status: "error", message: action.message };
32+
}
33+
1534
// 将 ISO 日期转为相对时间描述(中文)
1635
function relativeTime(dateStr: string): string {
1736
const diff = Date.now() - new Date(dateStr).getTime();
@@ -41,30 +60,29 @@ function SkeletonRow() {
4160
}
4261

4362
export function DocHistoryPanel({ path }: DocHistoryPanelProps) {
44-
const [items, setItems] = useState<HistoryItem[] | null>(null);
45-
const [error, setError] = useState<string | null>(null);
63+
const [state, dispatch] = useReducer(reducer, { status: "loading" });
4664

4765
useEffect(() => {
66+
// 用 dispatch 而不是多次 setState,规避 react-hooks/set-state-in-effect lint;
67+
// path 变化时立刻回到 loading,避免"错误提示 + 旧列表"并存
68+
dispatch({ type: "fetch" });
4869
let cancelled = false;
49-
// path 变化触发重新 fetch 时先清空旧状态,避免"错误提示 + 旧列表"同时显示
50-
setItems(null);
51-
setError(null);
5270
fetch(`/api/docs/history?path=${encodeURIComponent(path)}`)
5371
.then((r) => r.json())
5472
.then((json) => {
5573
if (cancelled) return;
5674
if (json.success) {
57-
setItems(json.data);
58-
setError(null);
75+
dispatch({ type: "ok", items: json.data ?? [] });
5976
} else {
60-
setItems(null);
61-
setError(json.error ?? "无法加载历史");
77+
dispatch({
78+
type: "error",
79+
message: json.error ?? "无法加载历史",
80+
});
6281
}
6382
})
6483
.catch(() => {
6584
if (!cancelled) {
66-
setItems(null);
67-
setError("无法加载历史");
85+
dispatch({ type: "error", message: "无法加载历史" });
6886
}
6987
});
7088
return () => {
@@ -80,7 +98,7 @@ export function DocHistoryPanel({ path }: DocHistoryPanelProps) {
8098
</h2>
8199

82100
{/* 加载中 */}
83-
{items === null && error === null && (
101+
{state.status === "loading" && (
84102
<div className="divide-y divide-neutral-100 dark:divide-neutral-800">
85103
<SkeletonRow />
86104
<SkeletonRow />
@@ -89,23 +107,23 @@ export function DocHistoryPanel({ path }: DocHistoryPanelProps) {
89107
)}
90108

91109
{/* 错误 */}
92-
{error !== null && (
110+
{state.status === "error" && (
93111
<p className="text-xs font-mono text-neutral-400 dark:text-neutral-500 py-2">
94-
{error}
112+
{state.message}
95113
</p>
96114
)}
97115

98116
{/* 空结果 */}
99-
{items !== null && items.length === 0 && (
117+
{state.status === "ok" && state.items.length === 0 && (
100118
<p className="text-xs font-mono text-neutral-400 dark:text-neutral-500 py-2">
101119
暂无更新记录
102120
</p>
103121
)}
104122

105123
{/* 历史列表 */}
106-
{items !== null && items.length > 0 && (
124+
{state.status === "ok" && state.items.length > 0 && (
107125
<ol className="divide-y divide-neutral-100 dark:divide-neutral-800">
108-
{items.map((item) => (
126+
{state.items.map((item) => (
109127
<li key={item.sha}>
110128
<a
111129
href={item.htmlUrl}

0 commit comments

Comments
 (0)