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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.18 18:09:52