Frequently Asked Questions (FAQ)
Basic Questions
A: Schema FormX supports React 16.8+ versions, requiring Hooks support.
// Recommended React versions
"react": ">=16.8.0"
"react-dom": ">=16.8.0"
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} />;
}
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:
- deps declaration: Ensure the dependent field is in the
deps array
- Field name matching: Field names in
deps must exactly match the target field's name
- 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:
{
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 }
}
};
}
}
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:
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.
A: Use the defaultData property:
const defaultData = {
name: 'John',
age: 25,
agree: true
};
function MyForm() {
return (
<Form
schema={schema}
defaultData={defaultData}
/>
);
}
A: Supported types:
A: Refer to the following optimization strategies:
- Split form: Use Group splitting or split into multiple Forms
- Control dependency depth: Avoid more than 3-4 levels of cascading dependencies
- Cache data: Use Map to cache async request results
- Use
useMemo: Cache schema and layout objects
- Partial validation: Only validate necessary fields
See Performance Optimization guide.
A: Take the following measures:
- Check for circular dependencies: Ensure dependencies are unidirectional
- Use debouncing: Debounce frequently triggered linkages
- 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
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:
- Use conditional
required: Dynamically decide if required based on other field values
- Use custom
validator: Check in the validation function whether the field should participate
- 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:
// 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
A: Form initialization follows this order:
- Parse schema: Process
FieldSchema[] or GroupSchema[]
- Register components: Map
components to corresponding fields
- Initialize data: Use
defaultData or empty object for initial form data
- Apply layout: Set form layout based on
layout property
- 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:
- Merge dependency values: Combine all changed dependency field values into the
deps array
- Trigger
onDepsChange: Call the current field's onDepsChange callback
- 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:
- 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);
}}
/>
);
}
- 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.