Contextual Save Bar - React component

chrisandrewca
Shopify Partner
9 0 15

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]} />

 

Replies 4 (4)
chrisandrewca
Shopify Partner
9 0 15

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
JoshHighland
Shopify Expert
89 2 45

This is awesome! thank you for sharing!

Just added to my app. Works like a charm.

sleyhane
Excursionist
18 2 7

Yes, thank you! This is so much better than the out-of-the-box contextual save bar.

Isaac_Bowen
Shopify Partner
77 0 24

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}
/>

 

I made Locksmith and Mechanic. 🙂