You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.01 16:46:09