跳到主要内容

React 最小闭环(第 1-2 周)

目标:跑通“发送 -> 流式输出 -> 中断 -> 错误兜底”的最小链路。

目录

学习目标

如果你是第一次做 AI 聊天应用,这一节的目标不是追求“功能多”,而是先把核心链路跑稳。
你可以把最小闭环理解成:用户输入后,系统能够连续返回内容,用户能主动停止,出现异常时不会卡死。

  • 会写 useStream Hook 管理流式状态
  • 会处理中断、重试、失败状态
  • 会做基础节流,避免每 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 次无明显卡顿
  • 停止按钮可立即生效
  • 网络异常时有明确提示并可重试