Skip to main content

Migrate ContextualSaveBar from Polaris React

Replace Polaris React ContextualSaveBar with the data-save-bar form attribute or the 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.


Anchor to Migrate a standard formMigrate 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

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>
);
}
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>
);
}

Preview

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.


Polaris ReactAutomatic save barMigration notes
Render when values are dirtydata-save-bar on formApp Bridge detects native form changes and controls visibility.
saveAction.onActionForm submit eventPrevent default submission only when app code handles the request.
discardAction.onActionForm reset eventUncontrolled native fields reset automatically. Reset app-owned state in onReset.
discardConfirmationModaldata-discard-confirmationAdd the attribute to the same form.
messageRemoveThe Shopify admin owns save-bar messaging. Keep task-specific instructions in the page.
Action descriptorsRemoveThe 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.


Anchor to Use programmatic control for app-owned stateUse 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.


Anchor to Handle save failuresHandle 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.


Anchor to Remove Frame hostingRemove 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.


  • 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.

Anchor to Remove Polaris ReactRemove 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.



Was this page helpful?