🎭《哈姆雷特》如果会写 React:useChat 自定义 Hook 的 AI 炼金术

🌌 1. 开场:一个异步请求的奥德赛

想象浏览器是一座剧院,舞台上的演员分别是:

角色 职责 道具
用户 提问 键盘
AI 回答 fetch
React 导演 hooks
我们 剧务 useChat

传统写法像即兴表演:

"台词、灯光、音效一起上!"------结果舞台塌了(内存泄漏、竞态、loading 闪烁)。
useChat 则像提前彩排好的百老汇,灯光、走位、提词器,一键到位。


🧰 2. 目录(舞台布置图)

  1. API 设计:演员的剧本
  2. 代码剖析:拆台、换幕、打光
  3. 内存与竞态:后台的幽灵
  4. 彩蛋:把 useChat 变成"时间旅行机"
  5. 一键启动:npm i @ai-chat/use-chat(假装我们有包)

🧩 3. API 设计:给哈姆雷特一把 TypeScript 的剑

ts 复制代码
// 用 JSDoc 冒充 TypeScript,浏览器也能看懂
/**
 * @param {Object} config
 * @param {string} config.api  - AI 服务端地址
 * @param {number} config.maxHistory - 记忆长度(防止 token 爆炸)
 * @returns {{
 *   messages: Array<Message>,
 *   send: (text: string) => Promise<void>,
 *   isLoading: boolean,
 *   abort: () => void
 * }}
 */

🎨 4. 代码剖析:把哈姆雷特拆成乐高

以下代码可直接丢进 Vite / CRA / Next.js 的 src/hooks/useChat.js

js 复制代码
import { useState, useRef, useEffect, useCallback } from 'react';

export default function useChat({ api, maxHistory = 20 }) {
  // 1. 舞台灯光:react 状态
  const [messages, setMessages] = useState([]);
  const [isLoading, setIsLoading] = useState(false);

  // 2. 打光师:控制 fetch 的 AbortController
  const abortRef = useRef(null);

  // 3. 提词器:真正发送消息的函数
  const send = useCallback(async (text) => {
    // 3-a 防止空字符串的小丑上场
    if (!text.trim()) return;

    // 3-b 临时演员:用户消息
    const userMsg = { role: 'user', content: text };
    setMessages(prev => [...prev, userMsg]);

    // 3-c 点亮"加载中"霓虹灯
    setIsLoading(true);

    // 3-d 新建 AbortController,给幽灵一个名字
    abortRef.current = new AbortController();

    try {
      const res = await fetch(api, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ prompt: text, history: messages.slice(-maxHistory) }),
        signal: abortRef.current.signal,
      });

      if (!res.ok) throw new Error(await res.text());

      const { reply } = await res.json();

      // 3-e 把 AI 放上舞台
      setMessages(prev => [...prev, { role: 'assistant', content: reply }]);
    } catch (err) {
      if (err.name !== 'AbortError') {
        // 真正的错误,亮起红灯
        console.error(err);
        setMessages(prev => [...prev, { role: 'error', content: err.message }]);
      }
    } finally {
      setIsLoading(false);
    }
  }, [api, messages, maxHistory]);

  // 4. 拉闸:观众起哄,演员退场
  const abort = useCallback(() => {
    abortRef.current?.abort();
    setIsLoading(false);
  }, []);

  // 5. 幕布落下时清理后台幽灵
  useEffect(() => {
    return () => abortRef.current?.abort();
  }, []);

  return { messages, send, isLoading, abort };
}

🧠 5. 内存 & 竞态:后台的幽灵

幽灵 出没地点 驱魔咒语
竞态 快速连发两次 send AbortController
内存泄漏 组件卸载仍在 fetch useEffect 清理
无限历史 token 爆炸 slice(-maxHistory)

⏳ 6. 彩蛋:时间旅行机(undo/redo)

js 复制代码
// 在 useChat 里再加两行
const [historyStack, setHistoryStack] = useState([]);
const undo = () => setMessages(historyStack.pop());

messages 每次深拷贝 push 进栈即可。

"Undo 是前端人的月光宝盒。"


🖼️ 7. 配图:舞台全景

graph TD A[用户敲字] -->|text| B(useChat.send) B --> C{fetch 请求} C -->|成功| D[AI 回复] C -->|失败/取消| E[错误/abort] D --> F[setMessages] E --> F F --> G[React 重渲染] style C fill:#f9f,stroke:#333 style G fill:#bbf,stroke:#333

🚀 8. 一键起飞

bash 复制代码
npm create vite@latest my-ai-chat --template react
cd my-ai-chat
# 把上面的 useChat.js 丢进 src/hooks/
npm run dev

🎭 尾声

useChat 像一支魔法羽毛笔,把异步、竞态、内存三大怪兽关进 React 的生命周期笼子。

今晚,让你的组件也念一段独白:

"To fetch, or not to fetch, that is no longer a question."

相关推荐
掘金017 分钟前
震惊!Vue3 竟能这样写?React 开发者狂喜的「Vue-React 缝合怪」封装指南
javascript·vue.js·react.js
Lsx_8 分钟前
分不清RAG 、Function Call、MCP、Agent?一文秒懂它们的区别和联系
前端·agent·mcp
毕了业就退休28 分钟前
websocket 的心跳机制你知道几种
前端·javascript·http
子林super29 分钟前
aiforcast集群单节点CPU使用率100%问题
前端
CF14年老兵31 分钟前
为什么 position: absolute 在 Flexbox 里会失效?
前端·css·trae
Juchecar32 分钟前
TypeScript 与 JavaScript 的关系及学习建议
javascript
JohnYan34 分钟前
Bun技术评估 - 25 Utils(实用工具)
javascript·后端·bun
xianxin_35 分钟前
CSS 选择器
前端
徐小夕36 分钟前
花3个月时间,写了一款协同文档编辑器
前端·vue.js·算法
Nicholas6843 分钟前
flutter滚动视图之ScrollDirection、ViewportOffset源码解析(一)
前端