Mastering Shopify App Save Bars: Handling Loading States in React Router 7 Templates
Hey everyone,
As a Shopify expert and community enthusiast, I love diving into the real-world challenges developers face. Recently, a fantastic question from RayleighCode caught my attention, highlighting a common pain point: gracefully handling loading states within Shopify embedded apps, especially with the new React Router 7 template and the Save Bar.
The Save Bar Challenge in React Router 7 Apps
RayleighCode is building a Shopify embedded app using the latest React Router 7 Shopify app template. The goal is clear: show a loading spinner on the Save button while a save request is processing, providing essential user feedback.
Initial Attempts and Roadblocks
RayleighCode first tried the standard
<ui-save-bar>
<button variant="primary">Save</button>
<button>Discard</button>
</ui-save-bar>
However, this did not render or function correctly inside the
Next, the App Bridge SaveBar API was tested:
import { saveBar } from "@shopify/app-bridge/actions";
Unfortunately, this also did not work properly within
The data-save-bar Workaround and Its Core Limitation
Facing these issues, RayleighCode wisely pivoted to using a form with the data-save-bar attribute:
<form data-save-bar>
This successfully displayed the Save Bar when the form became "dirty." However, this solution introduced its own problem: the Save button is automatically generated by Shopify. This means you lose direct control, making it seemingly impossible to:
- access the Save button element
- set a
loadingstate - disable the button during an asynchronous save
This is the classic trade-off: convenience for control. While data-save-bar simplifies dirty state detection, it abstracts away the very elements needed for custom loading feedback.
Expert Guidance: Handling Loading States
So, given these constraints, how can we provide that crucial loading feedback? Since the thread didn't offer a direct "magic fix" for the auto-generated button, let's explore robust strategies for great UX.
Option 1: Leveraging Alternative Loading Indicators (Recommended for data-save-bar)
If you stick with data-save-bar, you can’t put a spinner directly on the Save button. Instead, shift your focus to other clear ways to signal that a save operation is underway. The goal is to prevent user confusion and double submissions.
Here are actionable steps:
-
Global Loading Overlay or Spinner: Trigger a full-screen or section-specific loading overlay with a spinner. This visually disables the form, clearly indicating processing.
- Maintain a
loadingstate in your component. - On form submission, set
loadingtotrue. - Conditionally render a Polaris
(perhaps within an or a custom `div` that covers your form) based on loading. - Set
loadingback tofalseafter your save operation completes.
- Maintain a
-
Toast Notifications: Use these for quick, non-intrusive updates.
- On submission, show a "Saving..." toast.
- Update or dismiss the toast with "Settings saved!" or an error message post-API call using Polaris
.
-
Disable Form Fields: While the Save button is out of reach, you can disable all other input fields in the form during the save process. This is a strong visual cue.
- Pass a
disabled={loading}prop to your Polaris input components (e.g.,, ) based on your loadingstate.
- Pass a
Option 2: Taking Full Control with a Custom Save Bar
If absolute control over the Save button's loading state is non-negotiable, you might need to bypass data-save-bar entirely. This means you'll manually implement the Save Bar's functionality.
Here’s how you’d approach it:
- Manual Dirty State Tracking: Implement your own state management to detect form changes, comparing initial vs. current values.
-
Render Your Own Save Bar: Create a sticky footer using Polaris components (like
or a custom wrapper) to house your own "Save" and "Discard" buttons. -
Implement Loading State on Custom Button: With your own Polaris , you gain full control. Use its
loadingprop:
Manage your<Button primary loading={isSaving}>isSavingstate:truewhen the API call starts,falsewhen it finishes. -
Handle Discard: Implement an
onClickfor your "Discard" button to reset the form.
This path offers maximum flexibility but requires more manual setup for dirty state management and Save Bar rendering.
RayleighCode's detailed question highlights a key area where Shopify's embedded app ecosystem is still evolving, particularly within the context of new templates. Staying informed about App Bridge and Polaris updates is always wise. For now, focusing on alternative loading feedback with data-save-bar or taking full control with a custom Save Bar are your most robust options. The goal is always a smooth, clear user experience, even when navigating platform-specific nuances. Thanks to RayleighCode for sparking this important discussion!