Frequently Asked Questions (FAQ)

Basic Questions

Q1: What React versions does Schema FormX support?

A: Schema FormX supports React 16.8+ versions, requiring Hooks support.

// Recommended React versions
"react": ">=16.8.0"
"react-dom": ">=16.8.0"

Q2: How to install and use Schema FormX?

A: Install using npm or pnpm:

# npm
npm install @schema-formx/react

# pnpm
pnpm add @schema-formx/react

Basic usage:

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

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

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

Q3: Does Schema FormX support TypeScript?

A: Yes, Schema FormX is written in TypeScript and provides complete type definitions.

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

Q4: How to implement remote validation (e.g., check if username already exists)?

A: Use async validation functions:

{
  type: 'string',
  name: 'username',
  label: 'Username',
  required: true,
  validator: async ({ value, label }) => {
    if (!value) return `${label} is required`;
    
    try {
      const response = await fetch(`/api/check-username?username=${value}`);
      const { exists } = await response.json();
      
      if (exists) {
        return `${label} already exists`;
      }
      return undefined;
    } catch (error) {
      return 'Network error, please try again later';
    }
  }
}

Q5: Why does a required boolean field show a validation error?

A: false for boolean type is NOT considered empty. If agreement only needs to be checked, you can avoid setting required: true:

// If user must agree
{
  type: 'boolean',
  name: 'agree',
  label: 'I agree to the user agreement',
  required: true  // Empty values (null/undefined) will trigger error
}

// If just recording user's choice (no impact on submission)
{
  type: 'boolean',
  name: 'agree',
  label: 'I agree to the user agreement'
  // Don't set required
}

Q6: How to customize validation error messages?

A: Use custom validation functions and return specific error messages:

{
  type: 'string',
  name: 'password',
  label: 'Password',
  required: true,
  validator: ({ value, label }) => {
    if (!value) return `Please enter ${label}`;
    if (value.length < 8) return `${label} must be at least 8 characters`;
    if (!/[A-Z]/.test(value)) return `${label} must contain uppercase letters`;
    return undefined;
  }
}

Q7: Why is dependency linkage not triggering?

A: Please check the following:

  1. deps declaration: Ensure the dependent field is in the deps array
  2. Field name matching: Field names in deps must exactly match the target field's name
  3. Dependent field exists: Ensure the dependent field already exists in schema
// ❌ Wrong: Dependency field name mismatch
{
  name: 'city',
  deps: ['country']  // But schema field name is 'Country' (case mismatch)
}

// ✅ Correct: Ensure names match exactly
{
  name: 'city',
  deps: ['country']  // Schema field name is also 'country'
}

Q8: How to implement multi-level linkage (e.g., Province→City→District)?

A: Through chain dependencies, automatically cascading:

const schema = [
  { type: 'string', name: 'province', label: 'Province' },
  {
    type: 'string',
    name: 'city',
    label: 'City',
    deps: ['province'],
    onDepsChange: async ({ deps, schema }) => {
      const cities = await getCities(deps[0]);
      return {
        patch: { city: '' },  // Clear city value
        schema: { ...schema, props: { options: cities } }
      };
    }
  },
  {
    type: 'string',
    name: 'district',
    label: 'District',
    deps: ['city'],
    onDepsChange: async ({ deps, schema }) => {
      const districts = await getDistricts(deps[0]);
      return {
        schema: { ...schema, props: { options: districts } }
      };
    }
  }
];

Q9: What is circular dependency? How to avoid it?

A: Circular dependency occurs when A depends on B, and B depends on A, causing infinite recursion.

Symptoms: Console error over maxTimes

Avoidance Methods:

  • Ensure dependencies are unidirectional
  • Use triggerCount to detect infinite loops
// ❌ Wrong
{
  name: 'fieldA',
  deps: ['fieldB'],
  onDepsChange: () => ({ patch: { fieldA: '...' } })
}
{
  name: 'fieldB',
  deps: ['fieldA'],
  onDepsChange: () => ({ patch: { fieldB: '...' } })
}

// ✅ Correct: Unidirectional dependency
{
  name: 'fieldA'
}
{
  name: 'fieldB',
  deps: ['fieldA'],
  onDepsChange: () => ({ patch: { fieldB: '...' } })
}

Q10: What's the difference between patch and schema return values?

A:

Return ValueFunctionDescription
patchShallow merge into form dataSuitable for updating other fields' values
schemaCompletely replace current field schemaSuitable for updating current field's configuration
{
  name: 'city',
  deps: ['province'],
  onDepsChange: ({ deps, schema }) => {
    return {
      patch: { city: '' },  // Clear city (update other field's data)
      schema: {             // Update city field's own schema
        ...schema,
        props: { options: cities }
      }
    };
  }
}

Q11: How to use Group forms (Group)?

A: Use GroupSchema array as schema:

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

const schema: GroupSchema[] = [
  {
    key: 'basic',
    props: { title: 'Basic Info' },
    fields: [
      { type: 'string', name: 'name', label: 'Name' },
      { type: 'string', name: 'email', label: 'Email' }
    ]
  }
];

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

Q12: How to customize Group container styles?

A: By overriding the DefaultGroup component:

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,  // Default component
    DefaultGroup: CustomGroup
  };

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

Q13: How to register custom components?

A: Register via the components property:

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

// Custom color picker
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,  // Default component
  ColorPicker: ColorPicker        // Custom component
};

const schema = [
  {
    type: 'string',
    name: 'themeColor',
    label: 'Theme Color',
    component: 'ColorPicker'  // Reference registered component
  }
];

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

Q14: What props does a custom component need to receive?

A: Custom components automatically receive the following props:

PropTypeDescription
valueanyCurrent field value
onChange(value: any) => voidValue change callback (pass the final value directly, not a native Event object)
idstringField name (same as schema.name)
Other propsRecord<string, unknown>Custom attributes passed via 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}  // Contains props from schema.props
    />
  </div>
);

Note: Validation error display is managed by the Form component. Error messages are shown in a span element below the control. If you need custom error display, use validate() externally to get error information.

Q15: How to set form default values?

A: Use the defaultData property:

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

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

Q16: What are the form data types?

A: Supported types:

TypeDefault ValueEmpty Value Example
string''null, undefined, ''
numbernullnull, undefined, ''
booleannullnull, undefined, ''
object{}null, undefined, {}
array[]null, undefined, []

Q17: Large form has poor performance, what to do?

A: Refer to the following optimization strategies:

  1. Split form: Use Group splitting or split into multiple Forms
  2. Control dependency depth: Avoid more than 3-4 levels of cascading dependencies
  3. Cache data: Use Map to cache async request results
  4. Use useMemo: Cache schema and layout objects
  5. Partial validation: Only validate necessary fields

See Performance Optimization guide.

Q18: Dependency linkage triggers too fast, causing performance issues?

A: Take the following measures:

  1. Check for circular dependencies: Ensure dependencies are unidirectional
  2. Use debouncing: Debounce frequently triggered linkages
  3. Limit trigger count: Schema FormX has built-in maximum 20 times limit, automatically stops when exceeded
// Use triggerCount to detect infinite loops
{
  deps: ['field1'],
  onDepsChange: ({ totalTriggerCount }) => {
    if (totalTriggerCount > 10) {
      console.warn('Too many triggers');
      return {};
    }
    // Normal logic
  }
}

Other Questions

Q19: How to access form instance methods?

A: Use FormInstance type and ref:

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

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

  const handleSubmit = async () => {
    // Get form data
    const data = formRef.current.getData();
    
    // Set form data
    formRef.current.setData({ name: 'New Name' });
    
    // Validate form
    const { errors } = await formRef.current.validate();
    
    // Check if modified
    const isDirty = formRef.current.isDirty();
    
    // Reset form
    formRef.current.reset();
  };

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

Q20: How to integrate with antd/element-plus?

A: Register the corresponding components:

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

const components = {
  DefaultControl: Input
};

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

const components = {
  DefaultControl: ElInput
};

Then pass UI library specific props via schema.props:

{
  type: 'string',
  name: 'username',
  label: 'Username',
  props: {
    // antd props
    placeholder: 'Please enter',
    maxLength: 50,
    showCount: true
  }
}

Q21: Do hidden fields (hidden: true) still participate in validation?

A: Yes. hidden: true only visually hides the field (display: none). The field still participates in required validation. If a hidden field has required: true, it will still trigger validation errors on submit.

To skip hidden field validation:

  1. Use conditional required: Dynamically decide if required based on other field values
  2. Use custom validator: Check in the validation function whether the field should participate
  3. Use setData to clear hidden field values before validation
// Skip validation for hidden fields using custom validator
{
  type: 'string',
  name: 'conditionalField',
  label: 'Conditional Field',
  hidden: someCondition,
  validator: ({ value, formData }) => {
    // Only validate when visible
    if (!someCondition && !value) {
      return 'This field is required';
    }
    return undefined;
  }
}

Q22: What's the difference between setData and onChange?

A:

FeaturesetDataonChange
TriggerManual call (via ref)Automatic on field value change
Data updateComplete replacement of form dataPer-field incremental update
TimingImmediate on callImmediate on field onChange
Dependency LinkageDOES trigger for all fieldsDOES trigger
onChange callbackDoes NOT triggerDOES trigger
ValidationDoes NOT triggerDOES trigger
Use caseBulk data setting, form prefillResponding to user input
// setData: complete replacement, triggers dependency linkage but NOT onChange callback or validation
formRef.current.setData({ name: 'New Name', age: 25 });

// onChange: automatic on user input, triggers linkage, validation, and onChange callback
// User types "New Name" in name input → linkage triggers → validation runs → onChange fires

Q23: What is the form initialization order?

A: Form initialization follows this order:

  1. Parse schema: Process FieldSchema[] or GroupSchema[]
  2. Register components: Map components to corresponding fields
  3. Initialize data: Use defaultData or empty object for initial form data
  4. Apply layout: Set form layout based on layout property
  5. First render: Render form fields and initial state

Note: Initialization does NOT trigger onChange callback or dependency linkage. Dependency linkage triggers automatically on user interaction, and also when calling setData or reset. Validation only triggers on user interaction or when manually calling validate().

Q24: How are concurrent dependency changes handled?

A: When multiple dependency fields change simultaneously, Schema FormX processes them as follows:

  1. Merge dependency values: Combine all changed dependency field values into the deps array
  2. Trigger onDepsChange: Call the current field's onDepsChange callback
  3. Apply return values: Apply patch and schema to the form

If multiple fields' dependencies interact, multiple linkage rounds may trigger. Schema FormX has a built-in maximum 20 trigger limit, after which it automatically stops and prints a warning.

// Handle multiple dependencies
{
  name: 'result',
  deps: ['fieldA', 'fieldB'],
  onDepsChange: ({ deps: [fieldA, fieldB], schema }) => {
    // When fieldA and fieldB change simultaneously, deps contains latest values
    return {
      schema: {
        ...schema,
        props: { disabled: !fieldA || !fieldB }
      }
    };
  }
}

Q25: How to integrate with Redux/Zustand state management libraries?

A: Schema FormX is a standalone form state management solution and typically doesn't need additional state management. However, to sync with external state:

  1. Sync to external state using onChange:
import { useStore } from 'zustand';

function MyForm() {
  const updateFormData = useStore((state) => state.updateFormData);
  
  return (
    <Form 
      schema={schema}
      onChange={({ data }) => {
        // Sync to Zustand store
        updateFormData(data);
      }}
    />
  );
}
  1. Prefill from external state using 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} />;
}

Note: Schema FormX internally uses Object.freeze to protect form data. External state management libraries cannot directly modify form data — you must use the setData method.