
本文详解如何在 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 的推荐实践。