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