如何在 Next.js 中安全地条件化加载邀请令牌并延迟渲染注册表单

5次阅读

如何在 Next.js 中安全地条件化加载邀请令牌并延迟渲染注册表单

本文详解如何在 next.js 页面中正确处理可选查询参数(如 Token),利用 useswr 的条件取数机制与 userouter.isready 避免 hook 调用违规,并实现「有令牌则等待验证完成再渲染,无令牌则立即展示标准表单」的健壮逻辑。

本文详解如何在 next.js 页面中正确处理可选查询参数(如 token),利用 useswr 的条件取数机制与 userouter.isready 避免 hook 调用违规,并实现「有令牌则等待验证完成再渲染,无令牌则立即展示标准表单」的健壮逻辑。

在 Next.js 动态路由场景中,常需根据 URL 查询参数(如 ?token=xxx)切换页面行为:无 token 时显示普通注册页;有 token 时需先校验其有效性,再预填充邮箱或提示无效。但若直接在 if 分支中调用 useSWR 等 Hook,将违反 React 的 Rules of Hooks,导致 “Rendered more hooks than during the previous render” 错误——因为条件执行会破坏 Hook 调用顺序的一致性。

正确解法是:始终调用 Hook,但通过 NULL 或布尔守卫控制是否发起请求。useSWR 官方明确支持该模式:当 key 为 null 时,自动跳过请求。

以下是优化后的完整实现:

import { useRouter } from 'next/router'; import useSWR from 'swr'; import axios from 'axios'; import { useAuth } from '@/hooks/useAuth'; // 假设你的自定义 hook import PageLoader from '@/components/PageLoader';  const Register = () => {   const router = useRouter();   const { query, isReady } = router;    const { register, isLoading: isAuthLoading } = useAuth({     middleware: 'guest',     redirectIfAuthenticated: '/dashboard',   });    const [firstName, setFirstName] = useState('');   const [lastName, setLastName] = useState('');   const [errors, setErrors] = useState<string[]>([]);    // ✅ 正确:useSWR 始终调用,但仅当路由就绪且存在 token 时才触发请求   const {     data: inviteData,     error: inviteError,     isLoading: loadingInviteData,   } = useSWR(     isReady && query.token ? `/api/retrieve-invite/${query.token}` : null,     (url) => axios.get(url).then((res) => res.data.data)   );    const submitForm = async (event: React.FormEvent) => {     event.preventDefault();     try {       await register({         first_name: firstName,         last_name: lastName,         email: inviteData?.email || '', // 若有效邀请,自动带入邮箱       });     } catch (err: any) {       setErrors(err.response?.data?.errors || ['提交失败,请重试']);     }   };    // ? 统一加载态:认证初始化、路由就绪、邀请校验三者任一未完成,均显示加载器   if (isAuthLoading || !isReady || loadingInviteData) {     return <PageLoader />;   }    // ❌ 无效 token 处理:API 返回错误或无数据   if (inviteError || (query.token && !inviteData)) {     return (       <div className="max-w-md mx-auto p-6">         <h2 className="text-xl font-bold text-red-600 mb-4">邀请链接无效</h2>         <p className="text-gray-600 mb-6">           该邀请码已过期、被使用或不存在。请确认链接正确,或返回<a href="/register" className="text-blue-500 underline">标准注册页</a>。         </p>       </div>     );   }    // ✅ 渲染表单:此时可安全使用 inviteData(如有)   return (     <div className="max-w-md mx-auto p-6">       <h1 className="text-2xl font-bold mb-6">         {query.token ? '受邀注册' : '创建账户'}       </h1>        {query.token && inviteData?.email && (         <div className="mb-4 p-3 bg-blue-50 text-blue-700 rounded text-sm">           ? 已检测到邀请,邮箱将预填为:<strong>{inviteData.email}</strong>         </div>       )}        <form onSubmit={submitForm}>         <div className="mb-4">           <label className="block text-sm font-medium text-gray-700 mb-1">姓名</label>           <input             type="text"             value={firstName}             onChange={(e) => setFirstName(e.target.value)}             className="w-full px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"             required           />         </div>          <div className="mb-4">           <label className="block text-sm font-medium text-gray-700 mb-1">姓氏</label>           <input             type="text"             value={lastName}             onChange={(e) => setLastName(e.target.value)}             className="w-full px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"             required           />         </div>          {/* 邀请场景下隐藏邮箱输入框,由 inviteData.email 自动注入 */}         {query.token && inviteData?.email && (           <input type="hidden" name="email" value={inviteData.email} />         )}          <button           type="submit"           disabled={loadingInviteData}           className="w-full py-2 px-4 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50"         >           {query.token ? '接受邀请并注册' : '立即注册'}         </button>          {errors.length > 0 && (           <div className="mt-4 text-red-500 text-sm">             {errors.map((err, i) => (               <div key={i}>{err}</div>             ))}           </div>         )}       </form>     </div>   ); };  export default Register;

关键要点总结

  • Hook 调用必须稳定:useSWR 不可在条件语句(if)、循环或嵌套函数中调用。应始终出现在组件顶层,通过 key 参数(传 null)控制是否激活请求。
  • isReady 是必要守卫:router.query 在服务端渲染(SSR)或首次客户端导航时可能为空或不完整,isReady 确保路由参数已解析完毕,避免 undefined 导致的请求异常。
  • 加载态合并更优雅:将 useAuth.isLoading、router.isReady 和 useSWR.isLoading 统一纳入初始加载判断,避免闪烁或竞态。
  • 错误处理前置:在渲染前检查 inviteError 或 !inviteData(当 token 存在但无响应数据),及时展示语义化错误信息,而非回退到空表单。
  • 安全预填充:仅当 inviteData.email 明确存在时才注入,避免 undefined 引发表单异常;隐藏域()确保后端能接收到预填值。

此方案兼顾性能、可维护性与用户体验,是 Next.js 中处理可选查询参数驱动 UI 的推荐实践。

text=ZqhQzanResources