如何在NextJS中用tRPC发起API请求避免无效Hook调用错误
问题描述
我尝试把用户输入的数据发送到tRPC API,但发起查询时遇到错误:仅能在函数组件内使用React Hooks。我知道不能在回调函数里调用tRPC的useQuery(因为它是React Hook),但不知道表单提交时该怎么发请求?
邮政编码组件代码
'use client'; import { type NextPage } from "next" import { NextPageButtonLink } from "../UI/NextPageButtonLink" import { api } from "../utils/api"; import { useState } from "react"; const ZipCode: NextPage = () => { const [zipcode, setZipCode] = useState(""); // 这里会在每次重渲染和输入时都发起请求,理想情况是只在用户点击提交时发送 // const postZipCodeResult = api.zipcode.postZipCode.useQuery({userId: "1", zipcode: zipcode}); const handleSubmit = () => { // 这里会抛出Hook位置无效的错误 const postZipCodeResult = api.zipcode.postZipCode.useQuery({userId: "1", zipcode: zipcode}); console.log("Posting result", postZipCodeResult) } return ( <div className="bg-[#3276AE] flex flex-col items-center h-screen"> <form onSubmit={handleSubmit}> <label htmlFor="zipcode">输入你的邮政编码: <input type="text" id="zipcode" name="zipcode" required value={zipcode} onChange={e => setZipCode(e.target.value)} /> </label> <button type="submit">提交</button> </form> <NextPageButtonLink pageName="survey" msg="点击此处开始人口统计调查。" /> </div> ) } export default ZipCode;
邮政编码页面代码
import dynamic from "next/dynamic"; const ZipCode = dynamic(() => import('../components/zipcode'), { SSR: false }) export default function ZipCodePage() { return (<ZipCode/>) }
邮政编码路由代码
import { z } from "zod"; import { createTRPCRouter, publicProcedure } from "../trpc"; export const zipcodeRouter = createTRPCRouter({ postZipCode: publicProcedure .input(z.object({ userId: z.string(), zipcode: z.string() })) .query(({ input }) => { return { zipcode: `User: ${input.userId} zipcode: ${input.zipcode}`, }; }), });
解决方案
1. 改用tRPC的useMutation(推荐)
提交数据的场景更适合用mutation而非query,先修改路由的方法类型:
// zipcode路由修改 postZipCode: publicProcedure .input(z.object({ userId: z.string(), zipcode: z.string() })) .mutation(({ input }) => { // 把query改成mutation return { zipcode: `User: ${input.userId} zipcode: ${input.zipcode}`, }; }),
然后在组件中初始化useMutation,并在提交回调中调用mutate方法:
'use client'; import { type NextPage } from "next" import { NextPageButtonLink } from "../UI/NextPageButtonLink" import { api } from "../utils/api"; import { useState } from "react"; const ZipCode: NextPage = () => { const [zipcode, setZipCode] = useState(""); // 初始化mutation,可配置成功/失败回调 const postZipCodeMutation = api.zipcode.postZipCode.useMutation({ onSuccess: (data) => { console.log("提交成功", data); setZipCode(""); // 清空输入框 // 这里可以加跳转、提示等后续操作 }, onError: (error) => { console.error("提交失败", error); } }); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); // 阻止表单默认刷新行为 // 调用mutate发送请求 postZipCodeMutation.mutate({ userId: "1", zipcode: zipcode }); } return ( <div className="bg-[#3276AE] flex flex-col items-center h-screen"> <form onSubmit={handleSubmit}> <label htmlFor="zipcode">输入你的邮政编码: <input type="text" id="zipcode" name="zipcode" required value={zipcode} onChange={e => setZipCode(e.target.value)} /> </label> {/* 根据mutation状态显示加载状态,禁用按钮防止重复提交 */} <button type="submit" disabled={postZipCodeMutation.isLoading}> {postZipCodeMutation.isLoading ? "提交中..." : "提交"} </button> </form> <NextPageButtonLink pageName="survey" msg="点击此处开始人口统计调查。" /> </div> ) } export default ZipCode;
2. 若坚持使用query(不推荐,语义不符)
如果不想修改路由类型,可以用tRPC的查询客户端手动发起请求:
'use client'; import { type NextPage } from "next" import { NextPageButtonLink } from "../UI/NextPageButtonLink" import { api } from "../utils/api"; import { useState } from "react"; const ZipCode: NextPage = () => { const [zipcode, setZipCode] = useState(""); const queryClient = api.useQueryClient(); // 获取查询客户端 const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); try { // 手动发起查询,需指定唯一的queryKey const result = await queryClient.fetchQuery({ queryKey: ["zipcode.postZipCode", { userId: "1", zipcode: zipcode }], queryFn: () => api.zipcode.postZipCode.query({ userId: "1", zipcode: zipcode }) }); console.log("查询结果", result); } catch (error) { console.error("查询失败", error); } } return ( <div className="bg-[#3276AE] flex flex-col items-center h-screen"> <form onSubmit={handleSubmit}> <label htmlFor="zipcode">输入你的邮政编码: <input type="text" id="zipcode" name="zipcode" required value={zipcode} onChange={e => setZipCode(e.target.value)} /> </label> <button type="submit">提交</button> </form> <NextPageButtonLink pageName="survey" msg="点击此处开始人口统计调查。" /> </div> ) } export default ZipCode;
关键注意事项
- React Hooks(包括tRPC的
useQuery/useMutation)必须在组件顶层作用域调用,不能放在回调、条件判断等嵌套逻辑里,这是React的强制规则。 - 语义上,
query用于获取数据,mutation用于修改/提交数据,建议遵循这个规范。 - 使用
useMutation时,可利用isLoading/isError/isSuccess等状态优化UI反馈,避免用户重复操作。
内容的提问来源于stack exchange,提问作者avenmia
相关产品推荐
相关产品推荐

