Waldur HomePort exclusively uses React Final Form for standard forms and dialogs, and a custom VStepperForm component for complex multi-step wizards.
| Aspect |
React Final Form (Standard) |
VStepperForm (Multi-step) |
| State Storage |
Local component state |
Shared across steps |
| Performance |
Optimized subscription model |
Step-based validation |
| Complexity |
Minimal boilerplate |
Step progression |
| Persistence |
Local to component lifecycle |
Visual progress indicators |
| Integration |
Isolated, no external dependencies |
Complex deployments |
Implementation Examples
The standard form implementation uses a <Form> component that provides the handleSubmit method to its render prop.
1
2
3
4
5
6
7
8
9
10
11
12
13 | import { Form } from 'react-final-form';
import { SubmitButton } from '@/form';
<Form
onSubmit={onSubmit}
render={({ handleSubmit, submitting, invalid }) => (
<form onSubmit={handleSubmit}>
<OrganizationGroup />
<NameGroup />
<SubmitButton submitting={submitting} disabled={invalid} />
</form>
)}
/>
|
Field Group Pattern
We use reusable field groups for better organization, separating the structure from the actual logic:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 | import { Field } from 'react-final-form';
import { StringField } from '@/form';
import { FormGroup } from '@/marketplace/offerings/FormGroup';
import { translate } from '@/i18n';
export const NameGroup = ({ customer }) => (
<FormGroup label={translate('Project name')} required>
<Field
component={StringField as any}
name="name"
validate={validateProjectName}
customer={customer}
/>
</FormGroup>
);
|
Benefits: Separation of concerns, reusability, maintainability, testability.
Key Patterns & Best Practices
Validation
Validation functions are pure functions that return an error string if invalid, or undefined if valid.
| export const validateProjectName = (value, _, props) =>
checkDuplicate(value, props) || checkPattern(value);
|
Async Data Integration
Use React Query inside forms to load asynchronous data:
| const { data, isLoading, error, refetch } = useQuery({
queryKey: ['CustomerProjects', selectedCustomer?.uuid],
queryFn: () => fetchCustomerProjects(selectedCustomer.uuid),
});
|
When writing the submission handler for modal dialogs, use useManagedMutation or handle success/error states natively.
| const onSubmit = async (formData) => {
try {
await projectsCreate({ body: formData });
showSuccess(translate('Project created'));
closeDialog();
} catch (e) {
showErrorResponse(e, translate('Unable to create project'));
}
};
|
Error Handling
Issue: Avoid unhandled promise rejections when reporting API errors.
Solution: Don't re-throw errors after showErrorResponse():
| // ❌ Before (problematic)
catch (e) {
showErrorResponse(e, translate('Unable to create key.'));
throw e; // Causes unhandled promise rejection
}
// ✅ After (correct)
catch (e) {
showErrorResponse(e, translate('Unable to create key.'));
// Error handled, no re-throw needed
}
|
Key Issue: React Final Form context boundaries - submit buttons must be inside the <Form> component.
Solution: Move submit buttons inside the form context and provide a custom footer structure.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 | // ✅ Correct structure
import { ModalDialog } from '@/modal/ModalDialog';
<Form
onSubmit={onSubmit}
render={({ handleSubmit, submitting, invalid }) => (
<form onSubmit={handleSubmit}>
<ModalDialog
title={translate('New Project')}
footer={
<SubmitButton submitting={submitting} disabled={invalid} />
}
>
<ResourceForm />
</ModalDialog>
</form>
)}
/>
|
We have multiple FormGroup components, but you should favor the standard wrapper:
@/marketplace/offerings/FormGroup - Clean wrapper for labels/help text.
Replace Manual Label/Field Combinations
1
2
3
4
5
6
7
8
9
10
11
12 | // ❌ Avoid repetitive manual structure
<div className="mb-7">
<label className="form-label fw-bolder text-dark fs-6 required">
{translate('Username')}
</label>
<Field component={StringField as any} name="username" />
</div>
// ✅ Use clean FormGroup wrapper
<FormGroup label={translate('Username')} required>
<Field component={StringField as any} name="username" />
</FormGroup>
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 | // ❌ Manual tooltip implementation
<label>
{translate('Plan')}
<Tip label={translate('Help text')}>
<QuestionIcon />
</Tip>
</label>
// ✅ Built-in help prop
<FormGroup
label={translate('Plan')}
help={translate('Help text')}
>
<Field component={SelectField as any} name="period" />
</FormGroup>
|
If you want to prevent users from accidentally closing a modal dialog when a form has unsaved changes, use the DirtyStateReporter drop-in component anywhere inside your <Form> element:
1
2
3
4
5
6
7
8
9
10
11
12
13 | import { DirtyStateReporter } from '@/core/DirtyFormContext';
<Form
onSubmit={onSubmit}
render={({ handleSubmit }) => (
<form onSubmit={handleSubmit}>
<DirtyStateReporter />
<FormGroup label={translate('Name')}>
<Field component={StringField as any} name="name" />
</FormGroup>
</form>
)}
/>
|