Development discussions around Shopify APIs
Hi everyone, I created a Contextual Save Bar component for react. The Polaris docs recommend to use the app bridge bar and app-bridge-react doesn't contain a component for that. So I made one! Cheers.
import { useContext, useEffect, useState } from 'react' import { Context as ShopifyContext } from '@shopify/app-bridge-react' import { ContextualSaveBar } from '@shopify/app-bridge/actions' function useContextualSaveBar(save, discard) { const [shouldSave, setShouldSave] = useState(false); const [shouldDiscard, setShouldDiscard] = useState(false); useEffect(() => { if (shouldSave) { save[0](); setShouldSave(false); } }, [shouldSave, ...save]); useEffect(() => { if (shouldDiscard) { discard[0](); setShouldDiscard(false); } }, [shouldDiscard, ...discard]); return [() => setShouldSave(true), () => setShouldDiscard(true)]; } const SaveBar = ({ isShown, save, discard }) => { const [onSave, onDiscard] = useContextualSaveBar(save, discard); const app = useContext(ShopifyContext); const create = () => { const options = { message: 'Unsaved changes', saveAction: { disabled: false, loading: false, }, discardAction: { disabled: false, loading: false, discardConfirmationModal: true, }, }; return ContextualSaveBar.create(app, options); }; const [saveBar] = useState(create()); useEffect(() => { const saveUnsub = saveBar.subscribe( ContextualSaveBar.Action.SAVE, onSave ); const discardUnsub = saveBar.subscribe( ContextualSaveBar.Action.DISCARD, onDiscard ); return () => { saveUnsub(); discardUnsub(); }; }, []); if (isShown) { saveBar.dispatch(ContextualSaveBar.Action.SHOW); } else { saveBar.dispatch(ContextualSaveBar.Action.HIDE); } return null; } export default SaveBar
Use:
const [showSaveBar, setShowSaveBar] = useState(false); const onSave = () => {
doWork(stateA, stateB, stateC); }; const onDiscard = () => {
const {stateA, stateB, stateC} = initialState; // via useState(getInitialState())
setStateA(stateA);
setStateB(stateB);
setStateC(stateC); }; <SaveBar isShown={showSaveBar} save={[onSave, stateA, stateB, stateC]} discard={[onDiscard, initialState]} />
I made an update to this code. It adds the <Loading /> bar and disabling save.
import { useContext, useEffect, useState } from 'react' import { Context as ShopifyContext, Loading } from '@shopify/app-bridge-react' import { ContextualSaveBar } from '@shopify/app-bridge/actions' function useContextualSaveBar(save, discard) { const [shouldSave, setShouldSave] = useState(false); const [shouldDiscard, setShouldDiscard] = useState(false); useEffect(() => { if (shouldSave) { save[0](); setShouldSave(false); } }, [shouldSave, ...save]); useEffect(() => { if (shouldDiscard) { discard[0](); setShouldDiscard(false); } }, [shouldDiscard, ...discard]); return [() => setShouldSave(true), () => setShouldDiscard(true)]; } const SaveBar = ({ showBar, showSaveLoading, disableSave, save, discard }) => { const options = { saveAction: { disabled: disableSave, loading: showSaveLoading, }, discardAction: { disabled: false, loading: false, discardConfirmationModal: true, }, }; const [saveBar] = useState(ContextualSaveBar.create(useContext(ShopifyContext), options)); saveBar.set(options, true); const [onSave, onDiscard] = useContextualSaveBar(save, discard); useEffect(() => { const saveUnsub = saveBar.subscribe( ContextualSaveBar.Action.SAVE, onSave ); const discardUnsub = saveBar.subscribe( ContextualSaveBar.Action.DISCARD, onDiscard ); return () => { saveUnsub(); discardUnsub(); }; }, []); showBar ? saveBar.dispatch(ContextualSaveBar.Action.SHOW) : saveBar.dispatch(ContextualSaveBar.Action.HIDE); return showSaveLoading ? <Loading /> : null; } export default SaveBar
This is awesome! thank you for sharing!
Just added to my app. Works like a charm.
Yes, thank you! This is so much better than the out-of-the-box contextual save bar.
Here's a modified version in typescript, with a props interface that inherits from App Bridge:
import {useEffect, useState} from 'react';
import {Loading, useAppBridge} from '@shopify/app-bridge-react';
import {ContextualSaveBar} from '@shopify/app-bridge/actions';
import {Options} from '@shopify/app-bridge/actions/ContextualSaveBar';
const useContextualSaveBar = (
onReceiveSave: () => void,
onReceiveDiscard: () => void,
) => {
const [receivedSave, setReceivedSave] = useState(false);
const [receivedDiscard, setReceivedDiscard] = useState(false);
useEffect(() => {
if (receivedSave) {
onReceiveSave();
setReceivedSave(false);
}
}, [receivedSave]);
useEffect(() => {
if (receivedDiscard) {
onReceiveDiscard();
setReceivedDiscard(false);
}
}, [receivedDiscard]);
return [() => setReceivedSave(true), () => setReceivedDiscard(true)];
};
type SaveBarProps = {
onDiscard: () => void;
onSave: () => void;
payload: Options;
show: boolean;
};
export const SaveBar = ({onDiscard, onSave, payload, show}: SaveBarProps) => {
const app = useAppBridge();
const [saveBar] = useState(ContextualSaveBar.create(app, payload));
useEffect(() => {
if (show) saveBar.set(payload, true);
}, [payload]);
useEffect(() => {
saveBar.dispatch(ContextualSaveBar.Action[show ? 'SHOW' : 'HIDE']);
}, [show]);
const [onReceiveSave, onReceiveDiscard] = useContextualSaveBar(
onSave,
onDiscard,
);
useEffect(
() => saveBar.subscribe(ContextualSaveBar.Action.SAVE, onReceiveSave),
[],
);
useEffect(
() => saveBar.subscribe(ContextualSaveBar.Action.DISCARD, onReceiveDiscard),
[],
);
if (payload.saveAction?.loading || payload.discardAction?.loading) {
return <Loading />;
}
return null;
};
Here's how I'm using it:
// canWrite and canReset are booleans from my app; onWrite and onReset are callback functions
<SaveBar
payload={{
saveAction: {
disabled: !canWrite,
},
discardAction: {
disabled: !canReset,
discardConfirmationModal: true,
},
fullWidth: true,
}}
onSave={onWrite}
onDiscard={onReset}
show={canWrite || canReset}
/>
Thank you to everyone who participated in our AMA with Klaviyo. It was great to see so man...
By Jacqui May 30, 2023Photo by Marco Verch Sales channels on Shopify are various platforms where you can sell...
By Ollie May 25, 2023Summary of EventsBeginning in January of 2023, some merchants reported seeing a large amo...
By Trevor May 15, 2023