🎭《哈姆雷特》如果会写 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."

相关推荐
爱上妖精的尾巴16 分钟前
7-2 WPS JS宏 Object对象属性的查、改、增、删
前端·javascript·vue.js
小哀225 分钟前
2025年总结: 我还在往前走
前端·后端·全栈
0思必得027 分钟前
[Web自动化] Requests模块基本使用
运维·前端·python·自动化·html·web自动化
change_fate28 分钟前
vue模板数组不要直接使用reverse方法
前端·javascript·vue.js
一 乐29 分钟前
健康管理|基于springboot + vue健康管理系统(源码+数据库+文档)
java·前端·数据库·vue.js·spring boot·后端·学习
C_心欲无痕32 分钟前
nodejs - npm run原理
前端·npm·node.js
小笔学长1 小时前
调试技巧:使用 debugger 语句
javascript·前端开发·debugger调试·项目调试实战
千里马-horse1 小时前
TypedArrayOf
开发语言·javascript·c++·node.js·napi
小笔学长1 小时前
Webpack 配置优化:提高打包速度与质量
前端·项目实战·前端开发·webpack优化·打包性能优化
Hao_Harrision1 小时前
50天50个小项目 (React19 + Tailwindcss V4) ✨| NotesApp(便签笔记组件)
javascript