React 最小闭环(第 1-2 周)
目标:跑通“发送 -> 流式输出 -> 中断 -> 错误兜底”的最小链路。
目录
学习目标
如果你是第一次做 AI 聊天应用,这一节的目标不是追求“功能多”,而是先把核心链路跑稳。
你可以把最小闭环理解成:用户输入后,系统能够连续返回内容,用户能主动停止,出现异常时不会卡死。
- 会写
useStreamHook 管理流式状态 - 会处理中断、重试、失败状态
- 会做基础节流,避免每 token 重渲染
最小组件划分
这里建议拆组件,不是为了“代码优雅”,而是为了减少互相影响。
很多初学者会把输入框、请求逻辑、流式解析、错误提示都写在一个组件里,结果后续很难调试。
按职责拆分后,排障会简单很多。
ChatInput:输入与发送/停止按钮ChatOutput:流式文本展示useStream:请求、状态机、错误处理
关键实现
这段 useStream 代码的目标不是“写个 Hook”,而是把聊天流式交互的核心状态统一收口:
- 业务状态:当前是空闲、流式中、停止、失败还是完成
- UI 状态:当前展示文本是什么
- 控制句柄:是否还能主动中断请求
把这些集中在一个 Hook 的好处是:页面组件只关心渲染,不关心底层流读取细节。
type StreamState = "idle" | "streaming" | "stopped" | "failed" | "done";
export function useStream() {
const [state, setState] = useState<StreamState>("idle");
const [text, setText] = useState("");
const controllerRef = useRef<AbortController | null>(null);
async function send(message: string) {
setState("streaming");
setText("");
const controller = new AbortController();
controllerRef.current = controller;
// fetch + read stream...
}
function stop() {
controllerRef.current?.abort();
setState("stopped");
}
return { state, text, send, stop };
}
实战中建议你继续补两段逻辑:
send内补齐fetch + reader解析和异常处理- 增加
retry,让失败状态可一键重试
最终效果是:交互行为有明确状态边界,不会出现按钮可点但逻辑失效的混乱情况。
流式解析代码(可直接复用)
下面这段把服务端 SSE 文本安全拆分成事件,再映射到 delta/done/error:
export function parseSSEBuffer(raw: string) {
const events = raw.split("\n\n").filter(Boolean);
return events
.map((block) => block.split("\n").find((line) => line.startsWith("data:")))
.filter(Boolean)
.map((line) => JSON.parse((line as string).replace(/^data:\s*/, "")));
}
// 在 reader 循环中使用
let buffer = "";
while (reader) {
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const parts = buffer.split("\n\n");
buffer = parts.pop() || "";
for (const p of parts) {
const [evt] = parseSSEBuffer(`${p}\n\n`);
if (!evt) continue;
if (evt.type === "delta") setText((t) => t + evt.text);
if (evt.type === "done") setState("done");
if (evt.type === "error") setState("failed");
}
}
状态机图示
性能与体验底线
- 流式阶段先渲染纯文本,结束后再转 Markdown
- 使用 50-100ms 合并刷新
- 请求中禁用重复发送
性能优化代码片段
let temp = "";
let ticking = false;
function pushDelta(delta: string) {
temp += delta;
if (ticking) return;
ticking = true;
requestAnimationFrame(() => {
setText((prev) => prev + temp);
temp = "";
ticking = false;
});
}
验收标准
- 连续发送 5 次无明显卡顿
- 停止按钮可立即生效
- 网络异常时有明确提示并可重试