Solving the Save Bar Loading Spinner Challenge in Shopify Embedded Apps (React Router 7)

Hey everyone, your friendly Shopify migration expert here! I just caught a really interesting question in the community from RayleighCode, and it's one that many of you building embedded apps, especially with the newer React Router 7 template, might run into. RayleighCode's challenge is super specific but hits on a common pain point: how do you get a loading spinner on that 'Save' button in your Save Bar when your app is doing its thing in the background? Especially when using the component.

The Save Bar Dilemma in Modern Shopify Apps

RayleighCode is building a Shopify embedded app using the latest React Router 7 template. The goal? To display a loading spinner right on the Save button of the Save Bar whenever a save request is processing. This is crucial for good user experience, letting your merchants know their changes are being saved and preventing accidental double-clicks.

Why the Usual Suspects Weren't Working

It sounds like RayleighCode already tried the obvious, and correct, approaches first:

  1. The component: This is a standard Polaris component designed for this purpose. However, RayleighCode found it "does not render or function correctly" inside .

  2. The App Bridge SaveBar API: This is the programmatic way to control the Save Bar, allowing you to set dirty states, loading states, and actions. Unfortunately, this also "does not work properly within in the new template."

This is a classic "what gives?" moment for developers when the official tools don't behave as expected in a specific environment like the within a new template. It points to potential context or initialization issues.

The data-save-bar Workaround and Its Limits

Because the preferred methods weren't cooperating, RayleighCode smartly pivoted to using the data-save-bar attribute on their form:

This is a great fallback! It successfully makes the Save Bar appear when the form detects unsaved changes. But here's the rub: data-save-bar is pretty hands-off. It's designed to just show the bar and handle basic form submission. It doesn't give you direct access to that 'Save' button to slap a loading spinner on it, disable it during an async operation, or control its state granularly. Shopify generates it, and it's a bit of a black box for direct manipulation.

Expert Guidance: Getting That Loading State Right

Given this situation, and acknowledging that the App Bridge API wasn't playing nice for RayleighCode, let's break down a few paths forward. The goal is to provide that crucial visual feedback to your users that something is happening.

Option 1: Re-evaluating the App Bridge SaveBar API (Recommended First Step)

First things first, my strong recommendation is to revisit the App Bridge SaveBar API. While you mentioned it wasn't working properly, sometimes it's a subtle context or initialization issue within the new React Router 7 setup. The SaveBar API is designed precisely for this kind of control, allowing you to setDirty, setLoading, setSaving, and setDisabled states directly. If we can get this working, it's the cleanest and most integrated solution.

Here are a few things to check:

  • App Bridge Instance: Make absolutely sure your App Bridge instance is correctly initialized and available within the component where you're trying to use saveBar. Are you sure the app object is correctly passed down or accessible via useAppBridge?

  • Component Context: Critically, ensure your component is actually inside the context but that the App Bridge instance itself is correctly initialized outside or at a higher level, allowing the embedded app to communicate with the host.

  • Lifecycle Management: Ensure you're activating and deactivating the Save Bar correctly in your component's lifecycle (e.g., using useEffect with a cleanup function).

You'd typically use something like this (conceptually, adjust for your React context and specific template setup):

import { useAppBridge } from '@shopify/app-bridge-react';
import { SaveBar } from '@shopify/app-bridge/actions';
import { useEffect, useState, useCallback } from 'react';

function MyForm() {
  const app = useAppBridge();
  const saveBar = SaveBar.create(app);

  const [isDirty, setIsDirty] = useState(false);
  const [isSaving, setIsSaving] = useState(false);

  useEffect(() => {
    if (isDirty) {
      saveBar.set({
        enabled: true,
        message: 'Unsaved changes',
        discardAction: {
          onAction: () => { /* handle discard */ setIsDirty(false); },
        },
        saveAction: {
          onAction: async () => {
            setIsSaving(true);
            saveBar.setLoading(true); // This is the key for the spinner!
            try {
              // Your async save logic here
              await new Promise(resolve => setTimeout(resolve, 2000)); // Simulate API call
              console.log('Saved!');
              setIsDirty(false); // Mark form as clean
            } catch (error) {
              console.error('Save failed:', error);
              // Show error toast
            } finally {
              setIsSaving(false);
              saveBar.setLoading(false); // Hide spinner
              saveBar.set({ enabled: false }); // Hide save bar if clean
            }
          },
        },
      });
    } else {
      saveBar.set({ enabled: false });
    }
    return () => saveBar.dispatch(SaveBar.Action.DEACTIVATE);
  }, [app, saveBar, isDirty]);

  // Handle form field changes to set isDirty(true)
  const handleInputChange = useCallback(() => {
    setIsDirty(true);
  }, []);

  return (
    
      
      
    
  );
}

The saveBar.setLoading(true) is exactly what you're looking for to activate that spinner! If this isn't working, it points to a deeper App Bridge initialization or context issue that might be specific to the component in your React Router 7 template. It might be worth checking the official documentation for that specific template or raising a more focused question on the community forums about App Bridge initialization within that exact template structure.

Option 2: The data-save-bar with a Global Loading Indicator (If App Bridge is Truly Blocked)

If, for whatever reason, the App Bridge SaveBar API absolutely refuses to cooperate within your setup, and you must use data-save-bar for its automatic dirty state detection, you're left with a slightly less elegant but functional workaround. Since you can't control the button directly, you'll need to intercept the form submission and show a global loading state over your app content.

Here's the pattern:

  1. Intercept Submission: Attach an onSubmit handler to your

    .

  2. Prevent Default: Inside your onSubmit handler, call event.preventDefault() to stop the default form submission that data-save-bar would normally trigger.

  3. Show Global Spinner: Use a Polaris Loading component or similar global spinner that covers your app's content. You'd manage its visibility with a React state, say isLoading.

  4. Perform Async Save: Execute your API call or data saving logic.

  5. Hide Global Spinner: Once the save is complete (or fails), set isLoading(false).

  6. Clean the Form: To hide the data-save-bar after a successful save, you'll need to programmatically 'clean' your form. This usually means resetting the form's state or the individual field values back to their original state, so the data-save-bar no longer detects 'dirty' changes.

import { useState, useCallback } from 'react';
import { Loading } from '@shopify/polaris'; // Assuming Polaris is installed

function MyFormWithDataSaveBar() {
  const [isLoading, setIsLoading] = useState(false);
  const [formData, setFormData] = useState({ name: '' });
  const originalFormData = { name: '' }; // Keep track of original state

  const handleSubmit = useCallback(async (event) => {
    event.preventDefault(); // Stop default form submission
    setIsLoading(true); // Show global spinner

    try {
      // Simulate API call
      await new Promise(resolve => setTimeout(resolve, 2000));
      console.log('Data saved:', formData);
      // After successful save, you need to 'clean' the form state
      // This will cause data-save-bar to disappear
      setFormData(originalFormData); 
    } catch (error) {
      console.error('Save failed:', error);
      // Handle error, maybe show a Polaris Toast
    } finally {
      setIsLoading(false); // Hide global spinner
    }
  }, [formData]);

  const handleInputChange = useCallback((event) => {
    setFormData(prev => ({ ...prev, [event.target.name]: event.target.value }));
  }, []);

  // Determine if form is dirty for data-save-bar
  // (data-save-bar handles this automatically, but for manual control,
  // you'd typically compare formData to originalFormData)

  return (
    
{isLoading && }
); }

This approach moves the visual feedback from on the button to over the entire app, which is a common pattern for longer operations. It ensures users know something is happening, even if it's not directly tied to the Save button itself. It's a solid backup if direct Save Bar control remains elusive.

Option 3: Custom Save Bar (The Advanced Alternative)

If you find yourself continually fighting against the platform's abstractions and need absolute pixel-perfect control over every aspect, a custom-built Save Bar might be your last resort. This means abandoning data-save-bar and the App Bridge SaveBar API entirely for this specific component. You'd build your own fixed-position component at the bottom of the screen, mimicking Shopify's styling with Polaris components (a Button with a loading prop, for example). You'd manage its visibility based on your form's dirty state manually. This gives you full control but comes with the overhead of maintaining its state and styling to match Shopify's UX.

RayleighCode, I hope this helps you navigate this particular challenge. My strong recommendation is to try and get the App Bridge SaveBar API working correctly first, as it's the most integrated and user-friendly approach in the long run. If you're still hitting walls with the React Router 7 template and , it might be worth digging into the specific template's documentation or even raising another, more focused question on the community forums about App Bridge initialization within that exact template structure. Good luck!

Share:

Use cases

Explore use cases

Agencies, store owners, enterprise — find the migration path that fits.

Explore use cases