The application contains 200+ form components across two patterns, showing gradual migration from Redux Form to React Final Form.
Aspect |
Redux Form (Legacy, 59.5%) |
React Final Form (Modern, 30.5%) |
VStepperForm (Multi-step, 10%) |
State Storage |
Redux store |
Local component state |
Shared across steps |
Performance |
Can cause unnecessary re-renders |
Optimized subscription model |
Step-based validation |
Complexity |
More boilerplate required |
Minimal boilerplate |
Step progression |
Persistence |
Persists across unmounts |
Local to component lifecycle |
Visual progress indicators |
Integration |
Deep Redux integration |
Isolated, no external dependencies |
Complex deployments |
Implementation Examples
| export const PolicyCreateForm: FC<PolicyCreateFormProps> = (props) => (
<FormContainer submitting={props.submitting}>
<NumberField name="limit_cost" validate={required} />
<SelectField name="actions" validate={required} />
</FormContainer>
);
|
| <Form
onSubmit={onSubmit}
render={({ handleSubmit, submitting, invalid }) => (
<form onSubmit={handleSubmit}>
<OrganizationGroup />
<NameGroup />
<SubmitButton submitting={submitting} disabled={invalid} />
</form>
)}
/>
|
Field Group Pattern
React Final Form uses reusable field groups for better organization:
| 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
| export const validateProjectName = (value, _, props) =>
checkDuplicate(value, props) || checkPattern(value);
|
Async Data Integration
| const { data, isLoading, error, refetch } = useQuery({
queryKey: ['CustomerProjects', selectedCustomer?.uuid],
queryFn: () => fetchCustomerProjects(selectedCustomer.uuid),
});
|
| const onSubmit = async (formData) => {
try {
await projectsCreate({ body: formData });
showSuccess(translate('Project created'));
closeDialog();
} catch (e) {
showErrorResponse(e, translate('Unable to create project'));
}
};
|
Migration Strategy
- New Components: Use React Final Form
- Legacy Components: Maintain Redux Form
- Hybrid Support: Common field components work with both
- Gradual Migration: Phase out Redux Form over time
Migration Detailed Guidelines
- Redux Form (Legacy): 119 forms (59.5%) - being phased out
- React Final Form (Modern): 61 forms (30.5%) - preferred for new development
- VStepperForm (Multi-step): 20 forms (10%) - complex deployments
- User/Auth: SigninForm, KeyCreateDialog (React Final Form)
- Projects: ProjectCreateDialog (React Final Form), team management (Redux Form)
- Resources: OpenStack/VMware/Azure provider configs (mostly Redux Form)
- Administration: Mixed - newer ones use React Final Form
- Marketplace: DeployForm (Redux Form), newer policy forms (React Final Form)
Post-Migration Cleanup
Essential Commands:
| yarn deps:unused # Remove unused dependencies
yarn lint:check # Verify code standards
yarn test path/to/tests # Validate functionality
yarn tsc --noEmit # Check TypeScript compilation
|
Cleanup Checklist:
Error Handling Migration
Issue: Avoid unhandled promise rejections when migrating error handling.
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
}
|
Component Migration Patterns
Key Changes in Migration
- Form State: Redux Form HOC → React Final Form
<Form>
component
- Initial Values:
useEffect
with change
action → initialValues
prop
- Error Handling: Redux actions →
useNotify
hook
- Component Structure:
FormContainer
wrapper → render prop pattern
Field Migration
| // ✅ Standard pattern (preferred)
<Field
component={StringField as any}
name="username"
validate={required}
/>
// ❌ Avoid creating typed adapters
const TypedNumberField = Field as React.ComponentType<any>;
|
Array Field Migration
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 | // Before: Redux Form
const comp = fields.get(i);
// After: React Final Form
const comp = fields.value[i];
// Setup with array mutators
<Form
mutators={{ ...arrayMutators }}
render={({ handleSubmit }) => (
<form onSubmit={handleSubmit}>
<PolicyCreateForm {...props} />
</form>
)}
/>
|
SDK Types Best Practices
- Use
import { type ComponentUsage } from 'waldur-js-client'
- Prefer SDK types over custom interfaces
- Handle type conversions:
parseFloat(component.usage)
Key Issue: React Final Form context boundaries - submit buttons must be inside <Form>
component.
Problem: useFormState
called outside form context:
| // ❌ Problematic structure
<ModalDialog footer={<SubmitButton />}>
<Form><ResourceForm /></Form>
</ModalDialog>
|
Solution: Move submit button inside form context:
| // ✅ Correct structure
<ModalDialog>
<Form>
<ResourceForm />
<div className="modal-footer">
<SubmitButton />
</div>
</Form>
</ModalDialog>
|
Migration Steps:
- Move submit button inside
<Form>
render function
- Use
modal-footer
class for styling consistency
- Remove
footer
prop from ModalDialog
- Test form state access (
useFormState
, useField
)
Two Types Available:
@waldur/form/FormGroup
- Redux Form wrapper with comprehensive state management
@waldur/marketplace/offerings/FormGroup
- Simple wrapper for labels/help text (preferred for React Final Form)
Replace Manual Label/Field Combinations
1
2
3
4
5
6
7
8
9
10
11
12 | // ❌ Before - 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>
// ✅ After - 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 | // ❌ Before - Manual tooltip implementation
<label>
{translate('Plan')}
<Tip label={translate('Help text')}>
<QuestionIcon />
</Tip>
</label>
// ✅ After - Built-in help prop
<FormGroup
label={translate('Plan')}
help={translate('Help text')}
>
<Field component={SelectField as any} name="period" />
</FormGroup>
|
Best Practices
- Props Effectiveness:
hideLabel
, spaceless
don't work with direct Field usage
- CSS Classes: Verify classes exist (avoid non-existent Tailwind classes like
space-y-4
)
- Translations: Make all text including prepositions translatable
- FormGroup Choice: Use marketplace FormGroup for React Final Form
Architecture Benefits
- Flexibility: Multiple form approaches for different use cases
- Consistency: Shared field components across form systems
- Performance: Modern forms use optimized re-rendering
- Maintainability: Clear separation between legacy and modern patterns
- Developer Experience: Reduced boilerplate in new forms