I wondered if there are best practices for adding SaveBar to the app. I always think our solution is a little bit clunky. I think it’s okay to use for simple forms with managed SaveBar, but for complex structures, it will be hard to manage (maybe I’m missing something?). Example of complex structure:
widget
title
posts
0:
title - this might change
file_url
thumbnail - this might change
Our stack includes: react-router, redux-toolkit.
What we do:
- For tracking form changes, we’re using redux-toolkit listeners to subscribe to actions and check the difference between original value and the new value.
widgetMiddleware.startListening({
matcher: isAnyOf(
updateTitle,
updateProduct,
disableProduct
),
effect: async (action, listenerApi) => {
listenerApi.cancelActiveListeners();
const state = listenerApi.getState();
if (JSON.stringify(state.widget.widget) !== JSON.stringify(state.widget.originalWidget)) {
listenerApi.dispatch(setWidgetChanged(true));
} else {
listenerApi.dispatch(setWidgetChanged(false));
}
}
});
- To pass down SaveBar ID to the tree with Context and Provider. The whole page with SaveBar will be wrapped with this Provider
export const SaveBarProvider = ({ saveBarId, isChanged, children }) => {
return <SaveBarContext.Provider
value={{
saveBarId,
isChanged
}}>
{children}
</SaveBarContext.Provider>;
};
- Finally, we have a custom SaveBar component, which will control SaveBar from AppBridge
import { SaveBar as PolarisSaveBar } from "@shopify/app-bridge-react";
import { memo, useContext, useEffect } from "react";
import { useShopifyAppBridge } from "@/hooks/useShopifyAppBridge.jsx";
import { SaveBarContext } from "@/contexts/savebar.js";
export const SaveBar = memo(({
onSave,
onDiscard,
isLoading,
disabled,
isChanged,
alwaysOn = false
}) => {
const shopify = useShopifyAppBridge();
const saveBarContext = useContext(SaveBarContext);
useEffect(() => {
if (!saveBarContext) {
throw new Error("SaveBar context not found!");
}
if (!saveBarContext.saveBarId) {
throw new Error("saveBarID not found!");
}
if (alwaysOn) {
setTimeout(() => shopify.saveBar.show(saveBarContext.saveBarId), 100);
} else {
if (isChanged) {
shopify.saveBar.show(saveBarContext.saveBarId);
} else {
shopify.saveBar.hide(saveBarContext.saveBarId);
}
}
}, [alwaysOn, isChanged]);
if (!shopify) {
return null;
}
const saving = isLoading ? { loading: "" } : {};
return (
<PolarisSaveBar id={saveBarContext.saveBarId} discardConfirmation={""}>
<button
variant="primary"
disabled={disabled}
onClick={onSave}
{...saving}
/>
<button
onClick={onDiscard}
/>
</PolarisSaveBar>
);
});
- For back button, we have method like this
const handleBackButton = async () => {
if (isWidgetChanged) {
await window.shopify.saveBar.leaveConfirmation();
} else {
hideSaveBar(SAVE_BAR_ID);
}
navigate("/widgets");
};
While this works great, the main issues are navigation and form validation.
-
If navigation should be changed (back button clicked, link clicked), we need to check isChanged and call
await window.shopify.saveBar.leaveConfirmation();
-
If form is invalid and user clicks on Save button, we need to call
await window.shopify.saveBar.leaveConfirmation();
At some point, we have 3-5 leaveConfirmation calls inside the submit method and across other methods
What are the best practices for SaveBar, and is it possible to eliminate many leaveConfirmation calls?
I’d love to hear how you’re dealing with SaveBar