---
title: Migrate Form from Polaris React
description: >-
  Replace Polaris React Form with a native form that preserves submission,
  validation, FormData, and optional save-bar behavior.
source_url:
  html: 'https://shopify.dev/docs/apps/build/app-home/migrate-from-polaris-react/form'
  md: >-
    https://shopify.dev/docs/apps/build/app-home/migrate-from-polaris-react/form.md
api_name: app-home
---

# Migrate Form from Polaris React

Replace Polaris React `Form` with the native `form` element. Polaris web-component fields are form-associated, so named fields participate in `FormData`, submit, and reset behavior.

If the app renders controlled Polaris web-component fields through React, upgrade to React 19 first. React 18 doesn't provide the custom-element property and event behavior this example relies on. If you can't upgrade yet, keep the controlled Polaris React fields during this migration slice.

***

## Migrate a settings form

The following example submits immediately with a native submit button and shows pending and form-level error states. Add `data-save-bar` only when the page also needs unsaved-change protection.

## Migrating a product settings form

##### Polaris web components

```tsx
import {useState, type FormEvent} from 'react';

type FormSubmitEvent =
  | FormEvent<HTMLFormElement>
  | (SubmitEvent & {currentTarget: HTMLFormElement});

export function ProductSettings({initialTitle, saveProduct}) {
  const [pending, setPending] = useState(false);
  const [submitError, setSubmitError] = useState('');

  async function handleSubmit(event: FormSubmitEvent) {
    event.preventDefault();
    if (pending) return;

    setPending(true);
    setSubmitError('');

    try {
      await saveProduct(new FormData(event.currentTarget));
      shopify.toast.show('Product saved');
    } catch {
      setSubmitError("Product couldn't be saved. Check the fields and try again.");
    } finally {
      setPending(false);
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <s-section heading="Product settings">
        <s-stack gap="base">
          {submitError && (
            <s-banner tone="critical" heading="Fix the following error">
              {submitError}
            </s-banner>
          )}
          <s-text-field
            label="Title"
            name="title"
            defaultValue={initialTitle}
            autocomplete="off"
            required
          />
          <s-button type="submit" variant="primary" loading={pending}>
            Save product
          </s-button>
        </s-stack>
      </s-section>
    </form>
  );
}
```

##### Polaris React

```tsx
import {Form, TextField} from '@shopify/polaris';

export function ProductSettings({title, setTitle, saveProduct}) {
  return (
    <Form
      noValidate
      preventDefault
      onSubmit={() => saveProduct({title})}
    >
      <TextField
        label="Title"
        name="title"
        value={title}
        onChange={setTitle}
        autoComplete="off"
      />
      <button type="submit">Save</button>
    </Form>
  );
}
```

***

## Replace Form properties

| Polaris React | Native form | Migration notes |
| - | - | - |
| `onSubmit` | `onSubmit` | Read named controls with `new FormData(event.currentTarget)`. |
| `preventDefault` | `event.preventDefault()` inside `onSubmit` | Use it only when app code handles persistence instead of native navigation. |
| `action` and `method` | `action` and `method` | Keep native submission when the server endpoint owns the workflow. |
| `acceptCharset`, `encType`, `name`, and `target` | Same native attributes | Preserve them only when the endpoint depends on them. |
| `autoComplete` boolean | `autoComplete="on"` or `autoComplete="off"` | Prefer field-specific autocomplete tokens when available. |
| `noValidate` | `noValidate` | If set, render actionable field errors from server and client validation. |
| `implicitSubmit` | Native Enter-key submission | Keep standard behavior unless the old form intentionally disabled it for a documented product rule. |
| `children` | Native and Polaris web-component form controls | Give every submitted control a stable `name`. |

Don't add `preventDefault()` and then forget to submit. The handler must set pending state, call the backend, render field or form errors, and confirm success.

***

## Handle values and validation

Use uncontrolled fields with `defaultValue` when the browser and `FormData` can own the draft. Use controlled `value` only when the UI must react to every change. Don't mix a controlled value with a second object that becomes the submission source.

Put a single-field validation message on that field's `error` property. Use `s-banner` for a submission failure that affects the complete form. After a failed submit, preserve entered values and focus the first invalid field or the form-level error summary.

When the server returns errors, map them by stable field name. Don't rely on translated labels as error keys.

***

## Add unsaved-change protection when needed

Add `data-save-bar` to the native form when merchants can leave with unsaved changes. The form's submit event handles **Save**, and reset handles **Discard**. Add `data-discard-confirmation` when discarding is risky.

Use the programmatic [Save Bar API](https://shopify.dev/docs/api/app-home/apis/user-interface-and-interactions/save-bar-api) instead when dirty state isn't represented by native form controls. Don't use automatic and programmatic save-bar control for the same form.

***

## Test the migration

* Submit with the primary button and Enter from each eligible field.
* Verify `FormData` contains every named value, including web-component fields, choices, and dates.
* Exercise client and server validation, pending state, duplicate submission, failure, retry, and success.
* Reset or discard controlled and uncontrolled fields.
* Navigate away with clean and dirty state when the form uses a save bar.

***

## Remove Polaris React

After every form is migrated, remove `Form`, callback adapters used only for Polaris value signatures, and duplicate form-state helpers. Remove `@shopify/polaris` only after no other route in scope imports it.

***

## Related guidance

* [Migrate ContextualSaveBar from Polaris React](https://shopify.dev/docs/apps/build/app-home/migrate-from-polaris-react/contextual-save-bar)
* [FormLayout migration](https://shopify.dev/docs/apps/build/app-home/migrate-from-polaris-react/form-layout)
* [Save bar](https://shopify.dev/docs/api/app-home/app-bridge-web-components/save-bar)

***
