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

相关推荐
Amumu1213812 分钟前
Js:正则表达式(一)
开发语言·javascript·正则表达式
小江的记录本22 分钟前
【Linux】《Linux常用命令汇总表》
linux·运维·服务器·前端·windows·后端·macos
无人机9011 小时前
Delphi 网络编程实战:TIdTCPClient 与 TIdTCPServer 类深度解析
java·开发语言·前端
lUie INGA2 小时前
rust web框架actix和axum比较
前端·人工智能·rust
OPHKVPS2 小时前
VoidStealer新型窃密攻击:首例利用硬件断点绕过Chrome ABE防护,精准窃取v20_master_key
前端·chrome
月光宝盒造梦师2 小时前
Ant Design Ellipsis 中的判断逻辑 isEleEllipsis 方法非常消耗性能
javascript·react·优化
gechunlian883 小时前
SpringBoot3+Springdoc:v3api-docs可以访问,html无法访问的解决方法
前端·html
驾驭人生3 小时前
ASP.NET Core 实现 SSE 服务器推送|生产级实战教程(含跨域 / Nginx / 前端完整代码)
服务器·前端·nginx
酉鬼女又兒3 小时前
零基础快速入门前端ES6 核心特性详解:Set 数据结构与对象增强写法(可用于备赛蓝桥杯Web应用开发)
开发语言·前端·javascript·职场和发展·蓝桥杯·es6
慧一居士3 小时前
Vue项目中,子组件调用父组件方法示例,以及如何传值示例,对比使用插槽和不使用插槽区别
前端·vue.js