常见问题 (FAQ)

基础问题

Q1: Schema FormX 支持哪些 React 版本?

A: Schema FormX 支持 React 16.8+ 版本,需要支持 Hooks 特性。

// 推荐的 React 版本
"react": ">=16.8.0"
"react-dom": ">=16.8.0"

Q2: 如何安装使用 Schema FormX?

A: 使用 npm 或 pnpm 安装:

# npm
npm install @schema-formx/react

# pnpm
pnpm add @schema-formx/react

基本使用:

import { Form } from '@schema-formx/react';
import type { FieldSchema } from '@schema-formx/react';

const schema: FieldSchema[] = [
  { type: 'string', name: 'name', label: '姓名' }
];

function App() {
  return <Form schema={schema} />;
}

Q3: Schema FormX 是否支持 TypeScript?

A: 是的,Schema FormX 使用 TypeScript 编写,提供完整的类型定义。

import type { 
  FieldSchema, 
  GroupSchema, 
  FormInstance, 
  Layout 
} from '@schema-formx/react';

验证相关

Q4: 如何实现远程校验(如检查用户名是否已存在)?

A: 使用异步验证函数:

{
  type: 'string',
  name: 'username',
  label: '用户名',
  required: true,
  validator: async ({ value, label }) => {
    if (!value) return `${label}不能为空`;
    
    try {
      const response = await fetch(`/api/check-username?username=${value}`);
      const { exists } = await response.json();
      
      if (exists) {
        return `${label}已存在`;
      }
      return undefined;
    } catch (error) {
      return '网络错误,请稍后重试';
    }
  }
}

Q5: 为什么 boolean 类型的必填字段显示验证错误?

A: boolean 类型的 false 不被视为空值,请确认是否真的设置了 required: true。如果协议同意只需要勾选表态,可以不设置 required

// 如果需要用户必须同意
{
  type: 'boolean',
  name: 'agree',
  label: '我同意用户协议',
  required: true  // 空值(null/undefined)会触发错误
}

// 如果只需要记录用户选择(同意或不同意都不影响提交)
{
  type: 'boolean',
  name: 'agree',
  label: '我同意用户协议'
  // 不设置 required
}

Q6: 如何自定义验证提示信息?

A: 使用自定义验证函数并返回特定的错误信息:

{
  type: 'string',
  name: 'password',
  label: '密码',
  required: true,
  validator: ({ value, label }) => {
    if (!value) return `请输入${label}`;
    if (value.length < 8) return `${label}至少8个字符`;
    if (!/[A-Z]/.test(value)) return `${label}需包含大写字母`;
    return undefined;
  }
}

依赖联动相关

Q7: 依赖联动为什么没有触发?

A: 请检查以下几点:

  1. deps 声明:确保依赖字段在 deps 数组中
  2. 字段名称匹配deps 中的字段名必须与目标字段的 name 完全一致
  3. 依赖字段存在:确保依赖的字段已存在于 schema 中
// ❌ 错误:依赖字段名称不匹配
{
  name: 'city',
  deps: ['country']  // 但 schema 中字段名是 'Country'(大小写不一致)
}

// ✅ 正确:确保名称完全匹配
{
  name: 'city',
  deps: ['country']  // schema 中字段名也是 'country'
}

Q8: 如何实现多级联动(如省→市→区)?

A: 通过链式依赖自动级联:

const schema = [
  { type: 'string', name: 'province', label: '省份' },
  {
    type: 'string',
    name: 'city',
    label: '城市',
    deps: ['province'],
    onDepsChange: async ({ deps, schema }) => {
      const cities = await getCities(deps[0]);
      return {
        patch: { city: '' },  // 清空城市值
        schema: { ...schema, props: { options: cities } }
      };
    }
  },
  {
    type: 'string',
    name: 'district',
    label: '区县',
    deps: ['city'],
    onDepsChange: async ({ deps, schema }) => {
      const districts = await getDistricts(deps[0]);
      return {
        schema: { ...schema, props: { options: districts } }
      };
    }
  }
];

Q9: 循环依赖是什么?如何避免?

A: 循环依赖指 A 依赖 B,B 依赖 A 的情况,会导致无限递归。

问题现象:控制台报错 over maxTimes

避免方法

  • 确保依赖是单向的
  • 使用 triggerCount 判断是否进入死循环
// ❌ 错误
{
  name: 'fieldA',
  deps: ['fieldB'],
  onDepsChange: () => ({ patch: { fieldA: '...' } })
}
{
  name: 'fieldB',
  deps: ['fieldA'],
  onDepsChange: () => ({ patch: { fieldB: '...' } })
}

// ✅ 正确:单向依赖
{
  name: 'fieldA'
}
{
  name: 'fieldB',
  deps: ['fieldA'],
  onDepsChange: () => ({ patch: { fieldB: '...' } })
}

Q10: patchschema 返回值有什么区别?

A:

返回值作用说明
patch浅合并到表单数据适合更新其他字段的值
schema完整替换当前字段 schema适合更新当前字段的配置
{
  name: 'city',
  deps: ['province'],
  onDepsChange: ({ deps, schema }) => {
    return {
      patch: { city: '' },  // 清空 city(更新其他字段的数据)
      schema: {             // 更新 city 字段自身的 schema
        ...schema,
        props: { options: cities }
      }
    };
  }
}

分组表单相关

Q11: 如何使用分组表单(Group)?

A: 使用 GroupSchema 数组作为 schema:

import { Form } from '@schema-formx/react';
import type { GroupSchema } from '@schema-formx/react';

const schema: GroupSchema[] = [
  {
    key: 'basic',
    props: { title: '基本信息' },
    fields: [
      { type: 'string', name: 'name', label: '姓名' },
      { type: 'string', name: 'email', label: '邮箱' }
    ]
  }
];

function MyForm() {
  return <Form schema={schema} />;
}

Q12: 如何自定义分组的容器样式?

A: 通过覆盖 DefaultGroup 组件,从 props 中读取标题:

const CustomGroup = ({ title, children, className, style }) => (
  <div className={className} style={{
    ...style,
    border: '2px solid #1890ff',
    borderRadius: '8px',
    padding: '16px'
  }}>
    <h3 className="group-title">{title}</h3>
    <div className="group-content">{children}</div>
  </div>
);

const InputComponent = ({ value, onChange, ...props }) => (
  <input
    value={value || ''}
    onChange={(e) => onChange(e.target.value)}
    style={{ width: '100%', padding: '8px', border: '1px solid #ddd', borderRadius: '4px' }}
    {...props}
  />
);

const components = {
  DefaultControl: InputComponent,
  DefaultGroup: CustomGroup
};

function MyForm() {
  return <Form schema={schema} components={components} />;
}

自定义组件相关

Q13: 如何注册自定义组件?

A: 通过 components 属性注册:

import { Form } from '@schema-formx/react';

// 自定义颜色选择器
const ColorPicker = ({ value, onChange }: any) => (
  <input 
    type="color"
    value={value || '#000000'}
    onChange={(e) => onChange(e.target.value)}
  />
);

const InputComponent = ({ value, onChange, ...props }) => (
  <input
    value={value || ''}
    onChange={(e) => onChange(e.target.value)}
    style={{ width: '100%', padding: '8px', border: '1px solid #ddd', borderRadius: '4px' }}
    {...props}
  />
);

const components = {
  DefaultControl: InputComponent,  // 默认组件
  ColorPicker: ColorPicker        // 自定义组件
};

const schema = [
  {
    type: 'string',
    name: 'themeColor',
    label: '主题色',
    component: 'ColorPicker'  // 引用注册的组件
  }
];

function MyForm() {
  return <Form schema={schema} components={components} />;
}

Q14: 自定义组件需要接收哪些 props?

A: 自定义组件会自动接收以下 props:

Prop类型说明
valueany当前字段值
onChange(value: any) => void值变化回调(注意:参数需直接传递控件的最终值,而非原生 Event 对象)
idstring字段名(与 schema.name 一致)
其他 propsRecord<string, unknown>通过 schema.props 传入的自定义属性
const MyComponent = ({ value, onChange, ...restProps }: any) => (
  <div>
    <input 
      value={value || ''}
      onChange={(e) => onChange(e.target.value)}
      style={{ padding: '8px', border: '1px solid #ccc' }}
      {...restProps}  // 包含 schema.props 中的属性
    />
  </div>
);

提示:如果需要在自定义组件中显示验证错误,可以通过 onChange 回调更新值后,在组件外部使用 validate() 获取错误信息。错误提示由 Form 组件统一管理并显示在控件下方的 span 元素中。

表单数据相关

Q15: 如何设置表单默认值?

A: 使用 defaultData 属性:

const defaultData = {
  name: '张三',
  age: 25,
  agree: true
};

function MyForm() {
  return (
    <Form 
      schema={schema}
      defaultData={defaultData}
    />
  );
}

Q16: 表单数据类型有哪些?

A: 支持以下类型:

类型默认值空值示例
string''null, undefined, ''
numbernullnull, undefined, ''
booleannullnull, undefined, ''
object{}null, undefined, {}
array[]null, undefined, []

性能相关

Q17: 大型表单性能很差怎么办?

A: 参考以下优化策略:

  1. 拆分表单:使用 Group 拆分或拆分为多个 Form
  2. 控制依赖深度:避免超过 3-4 层的级联依赖
  3. 缓存数据:使用 Map 缓存异步请求结果
  4. 使用 useMemo:缓存 schema 和 layout 对象
  5. 局部验证:只验证需要的字段

详见性能优化指南。

Q18: 依赖联动触发太快导致性能问题?

A: 可以采取以下措施:

  1. 检查是否循环依赖:确保依赖是单向的
  2. 使用防抖:对频繁触发的联动进行防抖
  3. 限制触发次数:Schema FormX 内置最大 20 次限制,超过会自动停止
// 使用 triggerCount 判断是否进入死循环
{
  deps: ['field1'],
  onDepsChange: ({ totalTriggerCount }) => {
    if (totalTriggerCount > 10) {
      console.warn('触发次数过多');
      return {};
    }
    // 正常逻辑
  }
}

其他问题

Q19: 如何获取表单实例方法?

A: 使用 FormInstance 类型和 ref

import { useRef } from 'react';
import type { FormInstance } from '@schema-formx/react';

function MyForm() {
  const formRef = useRef<FormInstance>(null);

  const handleSubmit = async () => {
    // 获取表单数据
    const data = formRef.current.getData();
    
    // 设置表单数据
    formRef.current.setData({ name: '新名字' });
    
    // 验证表单
    const { errors } = await formRef.current.validate();
    
    // 检查是否修改过
    const isDirty = formRef.current.isDirty();
    
    // 重置表单
    formRef.current.reset();
  };

  return <Form ref={formRef} schema={schema} />;
}

Q20: 与 antd/element-plus 集成使用?

A: 需要注册对应的组件:

// antd
import { Input } from 'antd';

const components = {
  DefaultControl: Input
};

// element-plus
import { ElInput } from 'element-plus';

const components = {
  DefaultControl: ElInput
};

然后按各 UI 库的 props 传入 schema.props

{
  type: 'string',
  name: 'username',
  label: '用户名',
  props: {
    // antd props
    placeholder: '请输入',
    maxLength: 50,
    showCount: true
  }
}

Q21: hidden: true 的字段还会参与验证吗?

A: hidden: true 仅在视觉上隐藏字段(设置 display: none),字段仍然会参与 required 验证。如果隐藏字段设置了 required: true,提交时仍会触发验证错误。

如需跳过隐藏字段的验证,可以:

  1. 使用条件 required:根据其他字段值动态决定是否必填
  2. 使用自定义 validator:在验证函数中判断字段是否应参与验证
  3. 使用 setData 清空隐藏字段值后再验证
// 使用自定义验证跳过隐藏字段
{
  type: 'string',
  name: 'conditionalField',
  label: '条件字段',
  hidden: someCondition,
  validator: ({ value, formData }) => {
    // 仅在可见时验证
    if (!someCondition && !value) {
      return '此字段必填';
    }
    return undefined;
  }
}

Q22: setDataonChange 有什么区别?

A:

特性setDataonChange
触发方式手动调用(通过 ref)字段值变化时自动触发
数据更新完全替换表单数据逐个字段更新
触发时机调用时立即生效字段 onChange 时立即生效
依赖联动触发所有字段的依赖联动触发依赖联动
onChange 回调不会触发触发
验证不会触发触发
使用场景批量设置数据、回填表单响应用户输入
// setData:完全替换,会触发依赖联动,但不触发 onChange 回调和验证
formRef.current.setData({ name: '新名字', age: 25 });

// onChange:用户输入时自动触发,会触发联动、验证和 onChange 回调
// 用户在 name 输入框输入 "新名字" → 触发联动 → 触发验证 → 触发 onChange

Q23: 表单初始化的顺序是什么?

A: 表单初始化顺序如下:

  1. 解析 schema:处理 FieldSchema[]GroupSchema[]
  2. 注册组件:将 components 映射到对应字段
  3. 初始化数据:使用 defaultData 或空对象初始化表单数据
  4. 应用布局:根据 layout 属性设置表单布局
  5. 触发首次渲染:渲染表单字段和初始状态

注意:初始化时不会触发 onChange 回调,也不会触发依赖联动。依赖联动在用户交互时自动触发,调用 setDatareset 时也会触发。验证仅在用户交互或手动调用 validate() 时触发。

Q24: 多个依赖同时变化时如何处理?

A: 当多个依赖字段同时变化时,Schema FormX 会按以下顺序处理:

  1. 合并依赖值:将所有变化的依赖字段值合并到 deps 数组中
  2. 触发 onDepsChange:调用当前字段的 onDepsChange 回调
  3. 应用返回值:将 patchschema 应用到表单

如果多个字段的依赖相互影响,可能会触发多次联动。Schema FormX 内置了最大 20 次触发限制,超过后会自动停止并打印警告。

// 处理多个依赖
{
  name: 'result',
  deps: ['fieldA', 'fieldB'],
  onDepsChange: ({ deps: [fieldA, fieldB], schema }) => {
    // fieldA 和 fieldB 同时变化时,deps 包含最新值
    return {
      schema: {
        ...schema,
        props: { disabled: !fieldA || !fieldB }
      }
    };
  }
}

Q25: 如何与 Redux/Zustand 等状态管理库集成?

A: Schema FormX 是独立的表单状态管理方案,通常不需要额外的状态管理库。但如果需要与外部状态同步,可以:

  1. 使用 onChange 同步到外部状态
import { useStore } from 'zustand';

function MyForm() {
  const updateFormData = useStore((state) => state.updateFormData);
  
  return (
    <Form 
      schema={schema}
      onChange={({ data }) => {
        // 同步到 Zustand store
        updateFormData(data);
      }}
    />
  );
}
  1. 使用 setData 从外部状态回填
import { useEffect, useRef } from 'react';
import { useStore } from 'zustand';

function MyForm() {
  const formRef = useRef(null);
  const savedData = useStore((state) => state.formData);
  
  useEffect(() => {
    if (savedData && formRef.current) {
      formRef.current.setData(savedData);
    }
  }, [savedData]);
  
  return <Form ref={formRef} schema={schema} />;
}

提示:Schema FormX 内部使用 Object.freeze 保护表单数据,外部状态管理库无法直接修改表单数据,必须通过 setData 方法。