Skip to content

Form Migration Guide

The application contains 650+ form-related files across three patterns, showing ongoing migration from Redux Form to React Final Form.

Form Patterns Comparison

Aspect Redux Form (Legacy, ~68%) React Final Form (Modern, ~21%) VStepperForm (Multi-step, ~7%)
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

Redux Form (Legacy)

1
2
3
4
5
6
export const PolicyCreateForm: FC<PolicyCreateFormProps> = (props) => (
  <FormContainer submitting={props.submitting}>
    <NumberField name="limit_cost" validate={required} />
    <SelectField name="actions" validate={required} />
  </FormContainer>
);

React Final Form (Modern)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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

1
2
export const validateProjectName = (value, _, props) =>
  checkDuplicate(value, props) || checkPattern(value);

Async Data Integration

1
2
3
4
const { data, isLoading, error, refetch } = useQuery({
  queryKey: ['CustomerProjects', selectedCustomer?.uuid],
  queryFn: () => fetchCustomerProjects(selectedCustomer.uuid),
});

Modern Form Submission

1
2
3
4
5
6
7
8
9
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

Form Distribution (as of December 2025)

  • Redux Form (Legacy): ~467 files importing redux-form (~68%) - being phased out
  • React Final Form (Modern): ~146 files importing react-final-form (~21%) - preferred for new development
  • VStepperForm (Multi-step): ~50 files (~7%) - complex deployments
  • FormContainer usage: ~96 files - Redux Form wrapper components

Note: Many files import redux-form for shared field components (SelectField, StringField, etc.) and table filters, not just form definitions.

Key Forms by Category

  • User/Auth: SigninForm, KeyCreateDialog (React Final Form)
  • Projects: ProjectCreateDialog, team dialogs (React Final Form)
  • Resources: OpenStack/VMware/Azure deploy steps (VStepperForm with Redux Form fields)
  • Administration: Mixed - newer dialogs use React Final Form
  • Marketplace: Deploy flows use VStepperForm, policy forms use React Final Form
  • Table Filters: Predominantly Redux Form (high migration priority)
  • Proposals: Multi-step wizards using VStepperForm

Areas Needing Migration

  1. Table filters - Most still use Redux Form field components
  2. Resource action dialogs - Many use FormContainer pattern
  3. Invoice/Reporting filters - Redux Form based
  4. Customer management - Mixed, older dialogs need migration

Post-Migration Cleanup

Essential Commands:

1
2
3
4
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:

  • Remove unused Redux Form constants (FORM_ID)
  • Convert exported interfaces to internal if only used within component
  • Delete orphaned Redux Form files
  • Update imports to remove unused dependencies
  • Verify form context boundaries for React Final Form
  • Test modal structure and submit button access

Error Handling Migration

Issue: Avoid unhandled promise rejections when migrating error handling.

Solution: Don't re-throw errors after showErrorResponse():

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// ❌ 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

  1. Form State: Redux Form HOC → React Final Form <Form> component
  2. Initial Values: useEffect with change action → initialValues prop
  3. Error Handling: Redux actions → useNotify hook
  4. Component Structure: FormContainer wrapper → render prop pattern

Field Migration

1
2
3
4
5
6
7
8
9
// ✅ 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:

1
2
3
4
// ❌ Problematic structure
<ModalDialog footer={<SubmitButton />}>
  <Form><ResourceForm /></Form>
</ModalDialog>

Solution: Move submit button inside form context:

1
2
3
4
5
6
7
8
9
// ✅ Correct structure
<ModalDialog>
  <Form>
    <ResourceForm />
    <div className="modal-footer">
      <SubmitButton />
    </div>
  </Form>
</ModalDialog>

Migration Steps:

  1. Move submit button inside <Form> render function
  2. Use modal-footer class for styling consistency
  3. Remove footer prop from ModalDialog
  4. Test form state access (useFormState, useField)

FormGroup Components

Two Types Available:

  1. @waldur/form/FormGroup - Redux Form wrapper with comprehensive state management
  2. @waldur/marketplace/offerings/FormGroup - Simple wrapper for labels/help text (preferred for React Final Form)

FormGroup Migration Patterns

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>

Convert Manual Tooltips to Help Props

 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

  1. Props Effectiveness: hideLabel, spaceless don't work with direct Field usage
  2. CSS Classes: Verify classes exist (avoid non-existent Tailwind classes like space-y-4)
  3. Translations: Make all text including prepositions translatable
  4. 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