Forms
VEF form capabilities mainly come from @vef-framework-react/components, and the two main entry points are:
useForm()useFormContext()
It can be understood as an enterprise-oriented UI wrapper around TanStack Form. The full field-component list and every shared FormItemProps field live in Form; this page focuses on how a form is put together.
Typical Usage
import { Grid, useCodeSetOptionsSelect, useFormContext } from "@vef-framework-react/components";
import { z } from "@vef-framework-react/shared";
const validators = {
name: z.string("Required").min(2, "At least 2 characters"),
gender: z.string("Required")
};
export function UserForm() {
const { AppField } = useFormContext<{ name: string; gender: string }>();
const { gender } = useCodeSetOptionsSelect({
gender: "common.gender"
});
return (
<Grid columnGap="small">
<Grid.Item span={12}>
<AppField name="name" validators={{ onBlur: validators.name }}>
{field => <field.Input required label="Name" />}
</AppField>
</Grid.Item>
<Grid.Item span={12}>
<AppField name="gender" validators={{ onChange: validators.gender }}>
{field => <field.Select {...gender} required label="Gender" />}
</AppField>
</Grid.Item>
</Grid>
);
}
useForm() Creates the Form Instance
import { useForm } from "@vef-framework-react/components";
const {
Form,
AppField,
SubmitButton
} = useForm({
defaultValues: {
keyword: ""
},
onSubmit({ value }) {
console.log(value);
}
});
useFormContext() Works Well for Split Forms
When a form is split into separate components, a new form instance is usually not needed. AppField can be retrieved from context instead:
const { AppField } = useFormContext<FormValues>();
This is the pattern to reach for once a form grows beyond a single component — a search bar, a scene-specific form body, and a footer of actions can each pull AppField from the same form context instead of threading form state through props.
Label Layout: FormLayout
Every field wrapper reads its label placement from a shared form-layout context. The three fields — layout ("horizontal" by default), labelAlign ("right"), and labelWidth (100) — can be set once on the Form component and apply to every field inside:
<Form layout="vertical">
{/* every AppField renders its label above the control */}
</Form>
When the Form element is not yours to render — inside FormModal, FormDrawer, or a CRUD scene form — pass the same three fields through the formLayout prop instead (added in v2.12.0):
<FormDrawer formLayout={{ layout: "vertical" }} ... />
<CrudPage formLayout={{ layout: "vertical" }} ... />
Vertical labels are the usual choice for narrow drawers, where a 100px label column costs too much width. The FormLayout type is exported from @vef-framework-react/components.
Validation with z
import { z } from "@vef-framework-react/shared";
const validators = {
username: z.string("Required").min(2, "At least 2 characters").max(16, "At most 16 characters"),
email: z.email().nullish()
};
@vef-framework-react/shared re-exports a project-configured Zod instance, so validators stay consistent with the rest of the codebase without an extra dependency.
Feeding Data into Option Fields
VEF applications usually avoid fetching inside field components. Instead, hooks generate props that can be spread directly into controls, keeping data sourcing and field rendering as separate concerns.
Select
const roleSelectProps = useDataOptionsSelect({
filterable: true,
queryOptions: {
queryKey: [findRoleOptions.key],
queryFn: findRoleOptions
}
});
Tree Select
const deptTreeSelectProps = useDataOptionsTreeSelect({
filterable: true,
queryOptions: {
queryKey: [findDepartmentTree.key],
queryFn: findDepartmentTree
}
});
Code Sets
const { gender } = useCodeSetOptionsSelect({
gender: "common.gender"
});
// <field.Select {...gender} />
useCodeSetOptionsSelect(keys, options?) returns a { alias: SelectProps } map. Pass { filterable: true } (or per-key { key: "...", filterable: true } as the alias value) to enable pinyin search — see Code Sets for where the underlying data comes from and how keys become a typed union.
Dependent Fields & Linkage
Because the form layer is TanStack Form underneath, one field can react to another without extra machinery. Three mechanisms cover the common cases:
Subscribe— re-renders a slice of UI whenever a selected part of form state changes; use it to show, hide, disable, or re-require a field based on another field's value.listenersonAppField— runs a side effect when a field's value changes; use it to reset or derive dependent values.useFormStore()— the hook form ofSubscribe, for when a whole component needs the value.
Reacting to Another Field
Subscribe comes from the same place as AppField — useForm() or useFormContext(). Its selector decides what triggers a re-render, so typing in unrelated fields does not re-render the subscribed block:
const { AppField, Subscribe } = useFormContext<MenuFormValues>();
<AppField name="path">
{field => (
<Subscribe selector={state => state.values.type}>
{type => (
<field.Input
disabled={type === "button"}
label="Path"
required={type !== "button"}
/>
)}
</Subscribe>
)}
</AppField>
The same component works outside AppField too, for showing or hiding an entire block:
<Subscribe selector={state => state.values.orgId}>
{orgId => orgId && <DepartmentSection orgId={orgId} />}
</Subscribe>
Keep selectors narrow — state.values.type, not state.values — so the block only re-renders when the value it actually depends on changes.
Deriving and Resetting Values
AppField accepts a listeners prop. The onChange listener receives the new value and the fieldApi, whose form property reaches the rest of the form — form.resetField() puts a dependent field back to its default, and form.setFieldValue() writes a derived value (it also accepts an updater function prev => next):
<AppField
name="quantity"
listeners={{
onChange: ({ value, fieldApi: { form } }) => {
form.setFieldValue("total", (value ?? 0) * form.state.values.unitPrice);
}
}}
>
{field => <field.InputNumber required label="Quantity" />}
</AppField>
Unlike validators, listeners are for side effects — they never produce error messages.
Cascading Options
Cascading combines the two mechanisms: the controlling field resets its dependent on change, and the dependent field's options query keys off the controlling value.
function DepartmentField({ orgId }: { orgId?: string }) {
const { AppField } = useFormContext<StaffFormValues>();
const deptTreeSelectProps = useDataOptionsTreeSelect({
filterable: true,
queryOptions: {
queryKey: [findDepartmentTree.key, { orgId }],
queryFn: findDepartmentTree,
enabled: Boolean(orgId)
}
});
return (
<AppField name="deptId">
{field => <field.TreeSelect {...deptTreeSelectProps} label="Department" />}
</AppField>
);
}
The parent wires the two fields together:
<AppField
name="orgId"
listeners={{
onChange: ({ fieldApi: { form } }) => form.resetField("deptId")
}}
>
{field => <field.TreeSelect {...orgTreeSelectProps} required label="Organization" />}
</AppField>
<Subscribe selector={state => state.values.orgId}>
{orgId => <DepartmentField orgId={orgId} />}
</Subscribe>
Because orgId is part of the queryKey, picking a different organization refetches the department options automatically (enabled keeps the query idle until one is chosen), and the listeners reset prevents a stale deptId from surviving the switch.
Subscribing from Hook Code
When the value is needed in ordinary hook code rather than inside JSX, useFormStore(form.store, selector) has the same selector semantics as Subscribe:
import { useForm, useFormStore } from "@vef-framework-react/components";
const form = useForm({
defaultValues: { type: "menu", path: "" }
});
const type = useFormStore(form.store, state => state.values.type);
To wire an entirely custom control into a dependent field, see Custom Form Components.
Why the Split Matters
Keeping data sourcing (useDataOptionsSelect(), useCodeSetOptionsSelect(), ...) separate from field rendering (AppField) and layout (Grid) means:
- option-loading logic can be reused across a create form, an edit form, and a search bar
- a field component never needs to know whether its options come from a code set, a query, or a tree
- swapping a field's data source does not touch its layout or validation
For the full set of available field components (field.Input, field.Select, field.Bool, and the rest), see Form.