---
title: Migrate ContextualSaveBar from Polaris React
description: >-
  Replace the Polaris React ContextualSaveBar component with automatic form
  change tracking or the Save Bar API.
source_url:
  html: >-
    https://shopify.dev/docs/apps/build/app-home/migrate-from-polaris-react/contextual-save-bar
  md: >-
    https://shopify.dev/docs/apps/build/app-home/migrate-from-polaris-react/contextual-save-bar.md
api_name: app-home
---

# Migrate Contextual​Save​Bar from Polaris React

Replace Polaris React `ContextualSaveBar` with the [`data-save-bar`](https://shopify.dev/docs/api/app-home/app-bridge-web-components/save-bar) form attribute or the [Save Bar API](https://shopify.dev/docs/api/app-home/apis/user-interface-and-interactions/save-bar-api). Migrate dirty-state detection, save, discard, and navigation protection together so that users don't lose changes.

Use `data-save-bar` for a standard form. Use a programmatic `<ui-save-bar>` when dirty state comes from multiple sources or isn't represented by native form controls. Choose one approach for each form. Don't combine them on the same page.

***

## Migrate a standard form

The automatic pattern removes the conditionally rendered component and the React state used only to compare saved form values. Native form changes show the save bar, the `submit` event handles **Save**, and the `reset` event handles **Discard**.

## Migrating a product settings form

##### Polaris web components

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

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

function ProductSettings({
  initialTitle,
  onSave,
}: {
  initialTitle: string;
  onSave: (formData: FormData) => Promise<void>;
}) {
  async function handleSubmit(event: FormSubmitEvent) {
    event.preventDefault();
    const form = event.currentTarget;
    const formData = new FormData(form);
    const submittedTitle = String(formData.get('title') ?? '');
    await onSave(formData);

    const titleField = form.elements.namedItem('title') as
      | (Element & {value: string; defaultValue: string})
      | null;
    if (titleField) {
      const hasChangedSinceSubmit = titleField.value !== submittedTitle;
      titleField.defaultValue = submittedTitle;
      if (!hasChangedSinceSubmit) {
        form.reset();
      }
    }
  }

  return (
    <form
      data-save-bar
      data-discard-confirmation
      onSubmit={handleSubmit}
    >
      <s-section heading="Product settings">
        <s-text-field
          label="Title"
          name="title"
          defaultValue={initialTitle}
          autocomplete="off"
        />
      </s-section>
    </form>
  );
}
```

##### Polaris React

```tsx
import {ContextualSaveBar, Frame, TextField} from '@shopify/polaris';
import {useState} from 'react';

export function ProductSettings({
  initialTitle,
  onSave,
}: {
  initialTitle: string;
  onSave: (title: string) => Promise<void>;
}) {
  const [savedTitle, setSavedTitle] = useState(initialTitle);
  const [title, setTitle] = useState(initialTitle);
  const dirty = title !== savedTitle;

  async function handleSave() {
    await onSave(title);
    setSavedTitle(title);
  }

  return (
    <Frame>
      {dirty && (
        <ContextualSaveBar
          message="Unsaved changes"
          saveAction={{onAction: handleSave}}
          discardAction={{
            onAction: () => setTitle(savedTitle),
            discardConfirmationModal: true,
          }}
        />
      )}
      <TextField
        label="Title"
        value={title}
        onChange={setTitle}
        autoComplete="off"
      />
    </Frame>
  );
}
```

The migrated field has a `name`, so `FormData` includes its value. `data-discard-confirmation` adds confirmation before reset. Omit that attribute when discarding the form is low risk.

***

## Map form behavior

| Polaris React | Automatic save bar | Migration notes |
| - | - | - |
| Render when values are dirty | `data-save-bar` on `form` | App Bridge detects native form changes and controls visibility. |
| `saveAction.onAction` | Form `submit` event | Prevent default submission only when app code handles the request. |
| `discardAction.onAction` | Form `reset` event | Uncontrolled native fields reset automatically. Reset app-owned state in `onReset`. |
| `discardConfirmationModal` | `data-discard-confirmation` | Add the attribute to the same form. |
| `message` | Remove | The Shopify admin owns save-bar messaging. Keep task-specific instructions in the page. |
| Action descriptors | Remove | The automatic pattern supplies the standard Save and Discard actions. |

If a controlled React value changes without a native `input` or `change` event, then `data-save-bar` can't detect that update. Mirror the value to a named input and dispatch a bubbling native event, or use the programmatic Save Bar API.

***

## Use programmatic control for app-owned state

Use `<ui-save-bar id="...">` with `shopify.saveBar.show(id)` and `shopify.saveBar.hide(id)` when the workflow has custom dirty-state rules. Render native `button` children for Save and Discard, then keep their handlers, backend operation, and state reset together.

Call `shopify.saveBar.leaveConfirmation()` before programmatic navigation when unsaved changes might be present. Continue navigation only after the returned promise resolves.

Don't use both `data-save-bar` and programmatic show or hide calls for the same form. Each approach manages visibility independently.

***

## Handle save failures

Keep the save bar active when persistence fails. Display the error near the relevant form or field, preserve the user's values, and let the user retry. Don't clear dirty state or hide a programmatic save bar until the backend confirms success.

***

## Remove Frame hosting

Polaris React rendered `ContextualSaveBar` through `Frame`. App Bridge renders the replacement in the Shopify admin, so the form doesn't need a `Frame` ancestor. Remove `Frame` only after migrating its other consumers.

***

## Test the migration

* Change every field type, and confirm that the save bar appears.
* Save successfully, and confirm that the saved values become the new baseline.
* Fail a save, and confirm that values, errors, and the save bar remain available.
* Discard changes with and without confirmation, and verify controlled and uncontrolled values reset.
* Test links, browser history, redirects, and programmatic navigation with unsaved changes.

***

## Remove Polaris React

After every `ContextualSaveBar` call site is migrated, remove the component import, dirty-state helpers used only to control it, and its `Frame` dependency. Remove `@shopify/polaris` only after no other route in scope imports it.

***

## Related guidance

* [Save bar](https://shopify.dev/docs/api/app-home/app-bridge-web-components/save-bar)
* [Save Bar API](https://shopify.dev/docs/api/app-home/apis/user-interface-and-interactions/save-bar-api)
* [Migrate Frame from Polaris React](https://shopify.dev/docs/apps/build/app-home/migrate-from-polaris-react/frame)

***
