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}
/>
User | RANK |
---|---|
8 | |
7 | |
3 | |
3 | |
3 |
Connect your PayPal account to allow your customers to checkout using the PayPal gateway a...
ByYour online store speed can enhance your store’s discoverability, boost conversion rates a...
ByShopping is at our fingertips with mobile devices. Is your theme optimized to be user-frie...
By