Skip to content

Validation

CommandForm integrates seamlessly with the Arc command validation system to provide automatic validation feedback and error handling.

CommandForm automatically validates field inputs and displays errors based on:

  • Required Fields: Fields marked with required prop
  • Type Validation: Built-in HTML5 validation (email, URL, number ranges, etc.)
  • Command Validation Rules: Backend validation rules defined on your command
  • Custom Validation: Custom validators you define

CommandForm provides flexible control over when validation occurs through the validateOn prop:

By default, validation occurs when a field loses focus (blur event). This provides a balance between immediate feedback and not interrupting the user while typing:

<CommandForm command={RegisterUser} validateOn="blur">
<InputTextField<RegisterUser> value={c => c.email} type="email" title="Email" required />
<InputTextField<RegisterUser> value={c => c.password} type="password" title="Password" required />
</CommandForm>

When to use: Most forms - provides feedback after the user completes a field without being intrusive.

Validation runs immediately as the user types. This provides the fastest feedback but can be distracting:

<CommandForm command={RegisterUser} validateOn="change">
<InputTextField<RegisterUser> value={c => c.email} type="email" title="Email" required />
<InputTextField<RegisterUser> value={c => c.password} type="password" title="Password" required />
</CommandForm>

When to use: Forms where immediate validation is critical, like password strength meters or username availability checks.

Validation runs on both change and blur events:

<CommandForm command={RegisterUser} validateOn="both">
<InputTextField<RegisterUser> value={c => c.email} type="email" title="Email" required />
<InputTextField<RegisterUser> value={c => c.password} type="password" title="Password" required />
</CommandForm>

When to use: Forms where continuous validation is important for complex rules.

Control whether validation validates just the changed field or all fields:

By default, only the field that changed is validated. This is more efficient and provides focused feedback:

<CommandForm
command={CreateUser}
validateOn="blur"
>
<InputTextField<CreateUser> value={c => c.username} title="Username" required />
<InputTextField<CreateUser> value={c => c.email} type="email" title="Email" required />
</CommandForm>

In this example, when the username field loses focus, only username validation runs.

Set validateAllFieldsOnChange to true to validate the entire form when any field changes:

<CommandForm
command={CreateUser}
validateOn="blur"
validateAllFieldsOnChange={true}
>
<InputTextField<CreateUser> value={c => c.username} title="Username" required />
<InputTextField<CreateUser> value={c => c.email} type="email" title="Email" required />
<InputTextField<CreateUser> value={c => c.confirmEmail} type="email" title="Confirm Email" required />
</CommandForm>

When to use: Forms with interdependent fields where one field’s validity depends on another (e.g., password confirmation, start/end dates).

CommandForm always runs client validation silently on load, regardless of any other settings. This means isValid in the form context correctly reflects the real validity state from the very first render — before the user has interacted with any field.

This is useful for scenarios such as:

  • Disabling a submit button until the form is valid
  • Conditionally rendering actions based on form state
  • Knowing whether previously loaded data is valid before the user touches anything
function EditUserForm({ user }: { user: User }) {
return (
<CommandForm command={UpdateUser} currentValues={user}>
<InputTextField<UpdateUser> value={c => c.username} title="Username" required />
<InputTextField<UpdateUser> value={c => c.email} type="email" title="Email" required />
<SubmitButton /> {/* can use isValid from useCommandFormContext */}
</CommandForm>
);
}

The silent validation does not display any error messages. Errors are only rendered after the user has interacted with a field (governed by validateOn) or when validateOnInit is set to true.

Set validateOnInit to true to show validation error messages immediately when the form renders:

<CommandForm
command={CreateUser}
validateOnInit={true}
initialValues={{ username: '', email: '' }}
>
<InputTextField<CreateUser> value={c => c.username} title="Username" required />
<InputTextField<CreateUser> value={c => c.email} type="email" title="Email" required />
</CommandForm>

When to use:

  • Edit forms where existing data might have validation issues
  • Forms where you want to show all errors upfront
  • Step-by-step wizards showing validation state of upcoming steps

For automatic server-side validation as users type, see Auto Server Validation.

PropTypeDefaultDescription
validateOn'blur' | 'change' | 'both''blur'When to trigger validation
validateAllFieldsOnChangebooleanfalseValidate all fields or just the changed field
validateOnInitbooleanfalseShow validation error messages on form initialization (validation itself always runs silently on load)

Gentle validation (recommended for most forms):

<CommandForm command={T} validateOn="blur" />

Note: isValid in context is already accurate on first render due to silent validation on load, even though no errors are displayed yet.

Aggressive validation (real-time feedback):

<CommandForm command={T} validateOn="change" validateAllFieldsOnChange={true} />

Show all errors immediately:

<CommandForm command={T} validateOn="blur" validateOnInit={true} />

Mark fields as required using the required prop:

<CommandForm command={RegisterUser}>
<InputTextField<RegisterUser> value={c => c.email} type="email" title="Email" required />
<InputTextField<RegisterUser> value={c => c.password} type="password" title="Password" required />
<CheckboxField<RegisterUser> value={c => c.agreeToTerms} title="Terms" label="I agree" required />
</CommandForm>

Required fields:

  • Show visual indicator when invalid
  • Prevent form submission when empty
  • Display error messages when validation fails

By default, CommandForm displays error messages below each invalid field:

// Errors shown automatically for invalid/required fields
<CommandForm command={CreateAccount}>
<InputTextField<CreateAccount> value={c => c.username} title="Username" required />
{/* Error appears here if username is empty or invalid */}
<InputTextField<CreateAccount> value={c => c.email} type="email" title="Email" required />
{/* Error appears here if email is invalid format */}
</CommandForm>

Disable automatic errors to implement custom error rendering:

<CommandForm command={CreateAccount} showErrors={false}>
<InputTextField<CreateAccount> value={c => c.username} title="Username" required />
{/* No automatic error rendering */}
</CommandForm>

See Customization for custom error rendering patterns.

Field components leverage HTML5 validation attributes:

<CommandForm command={UpdateProfile}>
{/* Email format validation */}
<InputTextField<UpdateProfile>
value={c => c.email}
type="email"
title="Email"
required
/>
{/* URL format validation */}
<InputTextField<UpdateProfile>
value={c => c.website}
type="url"
title="Website"
placeholder="https://example.com"
/>
{/* Number range validation */}
<NumberField<UpdateProfile>
value={c => c.age}
title="Age"
min={18}
max={120}
required
/>
{/* Pattern matching */}
<InputTextField<UpdateProfile>
value={c => c.phone}
type="tel"
title="Phone Number"
/>
</CommandForm>

CommandForm automatically propagates validation results from backend command handlers:

[Command]
public record CreateUser(string Email, string Username)
{
public Result<ValidationResult, UserRegistered> Handle()
{
// Backend validation
if (Username.Length < 3)
{
return ValidationResult.Error("Username must be at least 3 characters");
}
if (!Email.Contains('@'))
{
return ValidationResult.Error("Email must be a valid address");
}
return new UserRegistered(Email, Username);
}
}
<CommandForm command={CreateUser}>
<InputTextField<CreateUser> value={c => c.email} type="email" title="Email" required />
<InputTextField<CreateUser> value={c => c.username} title="Username" required />
</CommandForm>

When the form is submitted:

  1. Frontend validation runs first (required, type checking)
  2. Command is sent to backend if frontend validation passes
  3. Backend validation rules execute
  4. Validation errors are returned and displayed in the form
  5. The form remains interactive for corrections

Use the useCommandFormContext hook to access validation state programmatically:

import { useCommandFormContext } from '@cratis/arc.react/commands';
function MyForm() {
const { getFieldError, commandResult } = useCommandFormContext();
const emailError = getFieldError('email');
const hasAnyErrors = commandResult?.validationResults && commandResult.validationResults.length > 0;
return (
<CommandForm command={CreateAccount} showErrors={false}>
<InputTextField<CreateAccount> value={c => c.email} type="email" title="Email" required />
{/* Check for specific field errors */}
{emailError && (
<div className="error">
{emailError}
</div>
)}
<InputTextField<CreateAccount> value={c => c.username} title="Username" required />
{/* Check for any errors */}
{hasAnyErrors && (
<div className="error-summary">
Please fix the errors above before submitting.
</div>
)}
</CommandForm>
);
}

Validate as users interact with the form using the useCommandInstance hook:

import { useCommandInstance } from '@cratis/arc.react/commands';
import { useEffect } from 'react';
function MyForm() {
const command = useCommandInstance(CreateAccount);
const [canSubmit, setCanSubmit] = useState(false);
useEffect(() => {
// Validate whenever command changes
const validate = async () => {
const result = await command.validate();
setCanSubmit(result.isValid);
};
if (command.hasChanges) {
validate();
}
}, [command.hasChanges]);
return (
<CommandForm command={CreateAccount}>
<InputTextField<CreateAccount> value={c => c.email} type="email" title="Email" required />
<InputTextField<CreateAccount> value={c => c.username} title="Username" required />
<button type="submit" disabled={!canSubmit}>
Create Account
</button>
</CommandForm>
);
}

Validate individual fields on blur for immediate feedback:

function RegistrationForm() {
const command = useCommandInstance(RegisterUser);
const [emailError, setEmailError] = useState<string>();
const handleEmailBlur = async () => {
// Validate just the email field
const result = await command.validate();
if (result.hasErrors('email')) {
setEmailError(result.getErrorsFor('email')[0]);
} else {
setEmailError(undefined);
}
};
return (
<CommandForm command={RegisterUser} showErrors={false}>
<InputTextField<RegisterUser>
value={c => c.email}
type="email"
title="Email"
required
onBlur={handleEmailBlur}
/>
{emailError && <div className="error">{emailError}</div>}
</CommandForm>
);
}

The command validate() method returns a CommandResult with:

interface CommandResult {
isSuccess: boolean;
isValid: boolean;
isAuthorized: boolean;
validationResults: ValidationResult[];
errors: Record<string, string[]>;
hasErrors(property?: string): boolean;
getErrorsFor(property: string): string[];
}
const result = await command.validate();
if (!result.isValid) {
console.log('Validation failed');
console.log('All errors:', result.errors);
console.log('Email errors:', result.getErrorsFor('email'));
}
if (!result.isAuthorized) {
console.log('User not authorized to execute this command');
}
  1. Use Required Fields: Mark essential fields with required for client-side validation
  2. Type-Specific Fields: Use appropriate input types (email, url, number) for built-in validation
  3. Backend Validation: Always validate on the server for security and data integrity
  4. Progressive Feedback: Consider validating on field blur for better UX
  5. Clear Messages: Provide clear, actionable error messages
  6. Accessible Errors: Ensure error messages are associated with their fields for screen readers
  7. Visual Feedback: Use consistent visual styling for invalid fields

For comprehensive details on the command validation system: