如何将Zod Schema传递给客户端组件以动态设置输入验证
解决Next.js客户端组件传递Zod Schema的序列化错误
错误原因
Next.js App Router中,标记'use client'的客户端组件仅接收可序列化的props(如字符串、数字、JSON对象等)。ZodSchema实例包含函数(如parse、safeParse)和非序列化的内部状态,直接作为props传递会触发Functions cannot be passed directly to Client Components错误。
可行解决方案
方案1:传递序列化的Schema配置,客户端重建Schema
利用Zod的toJSON()和fromJSON()方法,将Schema序列化为JSON对象传递,在客户端组件内重建Schema进行验证。
父组件(页面/容器)代码
import { z } from 'zod'; import InputField from './InputField'; // 定义用户名验证Schema const usernameSchema = z.string().min(3, '用户名至少3个字符').max(20, '用户名不能超过20个字符'); // 序列化为JSON const usernameSchemaJson = usernameSchema.toJSON(); // 定义密码验证Schema const passwordSchema = z.string().min(8, '密码至少8位').regex(/[A-Z]/, '密码需包含大写字母'); const passwordSchemaJson = passwordSchema.toJSON(); export default function AuthPage() { return ( <div> <InputField label="用户名" placeholder="请输入用户名" validationJson={usernameSchemaJson} /> <InputField label="密码" placeholder="请输入密码" validationJson={passwordSchemaJson} type="password" /> </div> ); }
客户端InputField组件代码
'use client'; import { z, ZodSchema } from 'zod'; import { useState } from 'react'; export type InputFieldProps = { label: string; disabled?: boolean; placeholder?: string; icon?: React.ReactNode; width?: string; type?: 'text' | 'password'; validationJson?: ReturnType<ZodSchema['toJSON']>; }; export default function InputField({ label, validationJson, type = 'text', ...rest }: InputFieldProps) { const [value, setValue] = useState(''); const [errorMessage, setErrorMessage] = useState(''); // 从JSON重建Zod Schema const validationSchema = validationJson ? z.ZodSchema.fromJSON(validationJson) : undefined; const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { const inputValue = e.target.value; setValue(inputValue); if (validationSchema) { const result = validationSchema.safeParse(inputValue); setErrorMessage(result.success ? '' : result.error.issues[0].message); } }; return ( <div style={{ width: rest.width || '320px', marginBottom: '16px' }}> <label style={{ display: 'block', marginBottom: '4px' }}>{label}</label> <input type={type} value={value} onChange={handleChange} disabled={rest.disabled} placeholder={rest.placeholder} style={{ width: '100%', padding: '8px', border: errorMessage ? '1px solid red' : '1px solid #ccc', borderRadius: '4px' }} /> {errorMessage && <p style={{ color: 'red', margin: '4px 0 0 0' }}>{errorMessage}</p>} {!errorMessage && value && <p style={{ color: 'green', margin: '4px 0 0 0' }}>验证通过</p>} </div> ); }
方案2:传递验证类型标识,组件内部匹配对应Schema
如果验证规则是固定的几种(如用户名、密码、邮箱),可以传递字符串类型的标识,在客户端组件内部预定义对应Schema,避免传递Schema实例。
组件类型定义修改
export type InputFieldProps = { // ... 原有props validationType?: 'username' | 'password' | 'email'; };
客户端组件内部处理
'use client'; import { z } from 'zod'; import { useState } from 'react'; // 预定义所有验证Schema const schemaMap = { username: z.string().min(3, '用户名至少3个字符').max(20, '用户名不能超过20个字符'), password: z.string().min(8, '密码至少8位').regex(/[A-Z]/, '密码需包含大写字母'), email: z.string().email('请输入合法邮箱地址') }; export default function InputField({ label, validationType, ...rest }: InputFieldProps) { const [value, setValue] = useState(''); const [errorMessage, setErrorMessage] = useState(''); const validationSchema = validationType ? schemaMap[validationType] : undefined; // ... 其余逻辑同方案1的handleChange和渲染部分 }
父组件调用
<InputField label="用户名" validationType="username" /> <InputField label="密码" validationType="password" type="password" />
方案3:父组件为客户端组件时,直接传递验证函数
如果父组件本身也是'use client'标记的客户端组件,可以直接传递一个验证函数(函数在客户端定义,无需序列化)。
父组件代码(标记'use client')
'use client'; import { z } from 'zod'; import InputField from './InputField'; // 定义用户名验证函数 const validateUsername = (value: string): string => { const result = z.string().min(3, '用户名至少3个字符').safeParse(value); return result.success ? '' : result.error.issues[0].message; }; export default function AuthPage() { return ( <InputField label="用户名" validate={validateUsername} /> ); }
客户端组件类型与逻辑
'use client'; import { useState } from 'react'; export type InputFieldProps = { // ... 原有props validate?: (value: string) => string; // 验证函数,返回错误信息或空字符串 }; export default function InputField({ label, validate, ...rest }: InputFieldProps) { const [value, setValue] = useState(''); const [errorMessage, setErrorMessage] = useState(''); const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { const inputValue = e.target.value; setValue(inputValue); if (validate) { setErrorMessage(validate(inputValue)); } }; // ... 渲染部分同前 }
总结
- 若需要高度动态的自定义Schema,优先选择方案1(序列化JSON重建Schema);
- 若验证规则固定,方案2更简洁易维护;
- 若父组件已是客户端组件,方案3实现成本最低。
内容的提问来源于stack exchange,提问作者Tehcooky
相关产品推荐
相关产品推荐

