I understand that the contextual save bar shows up if I use the Form component in the admin extension. My problem is the other way round. Can I access the resource state from my code?
For example, on the draft order details page, if the client make changes to the draft order, I don’t want them to be able to click the button in my extension until the changes have been saved. I tried using document to find the contextual save bar element but document is not defined.
Hey @hmpws Could you clarify which “form” you’re referring to? If you mean the draft-order form that Shopify itself renders in the Admin (the one that shows Shopify’s built-in contextual save bar), my understanding is that third-party code can’t access that dirty/saved state at all.
Just wanted to clarify this to make sure I’m understanding correctly - happy to confirm expected behaviour on this for you.
Thank you. I was using the Form from admin extension. It is only an example to point out that the extension can influence the dirty state. Not entirely relevant.
Our extension modifies the resource (e.g. draft-order through graphql). It is a bad user experience if there are unsaved changes on the page, we are modifying the old resource. If we can access the resource dirty state, at least we can disable the interaction until that has been saved.
On a related note, after modifying the resource, we currently have no way of reloading the page to show the changes programmatically either.
Thanks for clarifying @hmpws. You’re correct - there’s currently no way to access the dirty state of Shopify’s native forms from within an admin extension. The contextual save bar integration is only available through the Form component, and extensions can’t access the DOM or programmatically reload the host page after GraphQL mutations.
I’m curious about your specific use case though - what kind of modifications is your extension making to draft orders, and why doesn’t the Form component approach work for you? Just wanted to understand your workflow a bit better so we can take a look at some alternative approaches or document this as a feature request for the team to consider. Hope to hear from you soon!
To give an example of the current workflow, we help merchants apply discounts (across multiple line items) with the click of a button:
Merchant choose the discount from the extension and click ‘Apply’ on our extension.
Our app pulls the draft order through graphQl and mutates the draft order.
We ask the merchant to manually refresh the page to see the updated draft order (because we can’t refresh the page).
If the merchant makes changes to the draft order before clicking ‘Save’ on the contextual bar (e.g. adding a new line item), all their changes are lost. Since we don’t have access to the dirty state, we can’t disable the ‘Apply’ button on our extension.
If we rely on the contextual save bar, can we be sure we are applying mutation after the merchant’s draft order changes have been made? A ‘Save’ button on the contextual bar is also not as intuitive as an ‘Apply’ button on our extension.
This is just an example. We have multiple apps, which apply changes to the resource and the mismatch between resource state can be troublesome.
Thanks @hmpws, really appreciate you clarifying your use case here. Just want to make sure I fully understand the blockers. If you were to use the Form component with CSB integration, would your main concern be that after your GQL mutation finishes (when the merchant clicks “Save”), you still can’t programmatically refresh the page to show the updated draft order?
Or are there other reasons why the Form/CSB approach doesn’t work for your use case - for example, needing the explicit “Apply” button for better UX?
Just trying to understand if the page refresh limitation is the primary blocker, or if there are multiple aspects of the current limitations affecting your implementation there. I just want to make sure I can be more specific in the feature request so we can get this looked at for you
I think there are essentially two issues here, either UX or functional.
UX:
User makes changes but not save the resource (e.g. draft order).
Our app extension does not know that, the user takes action with our extension and after the app mutates the resource, the Shopify state is now different from the resource currently displayed on the user’s browser. Whether they then save from the CSB or reload the page, it is different from what the user expects.
Proposed solution: disable interaction from our app when the resource is dirty
Functional:
If we incorporate our function to use CSB, Shopify saves changes and call onSubmit, ref: Form.
If we make a graphQl query within onSubmit, can we be sure the app get the most up to date ‘saved’ resource?
Assume the answer is ‘yes’, we subsequently mutate the resource (e.g. set line item discounts), the resource page is still not reloaded and the user thinks our app did not work!
Proposed solution: let us refresh the resource programmatically after we finish our mutation
To conclude, from our perspective, we need one solution or the other (preferablly both).
Thanks for following up on this @hmpws and clarifying.
My understanding is that when using Form with CSB, onSubmit fires after Shopify itself saves, but you’re correct that without page refresh, users won’t see the effects of your mutations.
I’m going to do some further digging into this on my end to definitively confirm we don’t have a workaround or option for you, but at minimum I’m more than happy to note both of your proposed solutions as a feature request for the team because this does seem like a bit of a gap for apps that need to modify resources.
In the meantime, the best workaround would be just warning users to save first and asking them to manually refresh after, which I know isn’t ideal for the merchant experience. I’ll close the loop with you here once I’ve got confirmation and to let you know I’ve set up that feature request on my end here. Speak with you as soon as I have more info to share.
Hey @hmpws - I was able to get some clarification on this for you after speaking with some folks internally.
To answer your question directly there: I can confirm that Form’s onSubmit fires when Save is clicked and onReset fires when Discard is clicked. However, we don’t offer programmatic access to the Form’s dirty state at the moment and we don’t currently have plans to offer this in the future, but I will definitely put through a feature request for our product team to consider this on your behalf.
We also can’t guarantee that an app’s query happens after Shopify’s mutation (due to how we respect concurrency of calls). If multiple apps are modifying the same draft order, their queries/mutations happen concurrently with no guaranteed order.
For your use case, we’d strongly recommend using an admin action extension instead. This approach should help solve both problems there (though not in the most ideal manner): the action automatically disables when the form is dirty (preventing lost changes), and it operates independently so the data is should always be accurate when merchants click the action button in the admin to trigger your app (or open up the process on separate UI page/modal.
This should help avoid all the state synchronization issues. The merchant saves their changes first, then clicks your action to apply discounts, so this could be a much cleaner workflow.
I know it likely means some refactoring, but would generally provide a much more reliable merchant experience. Happy to help if you have questions about implementing an admin action extension if needed!
Thanks for the follow-up. Our extension is actually an action at the moment but then we run into this dirty state problem! There are other apps where an app block makes more sense (so an action is not ideal). Looking forward to a cleaner ‘dirty’ state solution !
Hey @hmpws - thanks for the extra context here! That’s really odd about the admin actions not working to resolve the issue. My understanding is that they are supposed to automatically disable themselves when there are unsaved changes on the form.
Could you let me know which resource page you’re working with? For example, is this happening on draft order details or somewhere else? If you could grab a quick screenshot or recording of the action staying enabled when it shouldn’t, that’d be super helpful for troubleshooting if possible.
Also curious about why action extensions aren’t working for some of your use cases - what specific scenarios are you running into where you need app blocks instead? Want to make sure I capture all the details when I submit that feature request.
I’ll definitely be pushing for both features - programmatic access to the dirty state from extensions and the ability to refresh after mutations. Can’t promise any timelines, but I’ll make sure your use case gets documented properly. Hope to hear from you soon on the action issue above, more than happy to help with that here in this thread.
Thanks for quick reply. I tested this today on the draft order details page.
The behavior I saw was (using 2025.4.x of ui-extensions-react):
“admin.draft-order-details.action.render” is not disabled on resource dirty.
“admin.draft-order-details.action.link” is disabled on resource dirty (similar to the ‘Run Flow automation’ link).
As for the use case, in other app, we display information relate to the order with the app block (related to the payment on the order) and our app adds relevant information to the order (tags, notes, metafields, etc.). For the user, if something doesn’t require their action, we display a success banner. It just makes more sense from a workflow perspective than doing it through an admin action because the user would then have to open the app action up on every order to check if something needs to be done.
Hey @hmpws - thanks for testing this out and confirming the specific behaviour, really appreciated. I’m wondering if this may be something to flag internally now, as it does sound odd that action.render and action.link would behave differently with a dirty state.
To help me investigate this with the team, could you share your shop/app ID so we can try to replicate this? Or if you’re able to share your extension code in a ZIP file, I’m happy to set up a DM if you’d prefer not to share any private details in this thread (this might be the easiest way to troubleshoot directly and we won’t share that code with anyone externally).
I’ll work with the team internally to dig into this - the inconsistent behavior definitely seems like something we should look into. Thanks for the detailed testing and hope to speak soon.
Our production app is called Cost+, you can use the current app extension action to apply discount to a draft order. For testing, I created app extensions from the CLI (shopify app generate extension). Probably quicker for you to replicate!
With regards to refreshing the page, we and our merchants are noticing a strange issue with the displayed resource. It takes multiple refresh now after a discount has been applied for the new mutated state to be repulled. There must be some caching going on and the resource does not get revalidated?
Hey @hmpws - thanks very much for setting this up and the replication steps. I think I was able to replicate this:
Were you able to get it working in the past without having to ask users to refresh the page? Just wanted to confirm if it was always the case that the page needed to be refreshed. My next step will likely be to reach out to some product folks internally on my end here, but just wanted to make sure I’m replicating correctly and that it had been working without a manual page refresh at one point.
We have always asked the user to refresh the page.
It is interesting that the discount was applied after one refresh for you. We tend to need a few tries for the discount to show up. That started this whole rabbit hole of UX improvement for us!
Thanks @hmpws - I appreciate you sending that video my way. The fact that you need multiple refreshes for the discount to show does seem to be odd to me, happy to dig into this further.
Would you be comfortable sharing your extension code so I can investigate what might be causing this caching/refresh issue? Happy to set up a DM if you’d prefer to keep your code private - just let me know here in this thread and I’ll get that sorted for you.
We pushed some changes to our app today. Please see this new video.
Maybe it is a placebo but the main difference we found between needing one or multiple refreshes seems to be related to the close() action returned from the admin extension action. I wonder if there is some data revalidation triggered by it.