@shopify/app-bridge-react documentation

Solved
Tourist
5 0 1

Is there any actual documentation for how @shopify/app-bridge-react is supposed to be used? It seems like it's been half baked and released without a lot of supporting information.

 

Currently, I'm trying to get a Modal component to render, but any time I add the component and reload the app, I get loads of error messages like:

 

AppBridgeError {name: "AppBridgeError", message: "APP::ERROR::INVALID_OPTIONS: `type_error_expected_string thrown for path: ['message'] and value: `undefined`", action: undefined, type: "APP::ERROR::INVALID_OPTIONS", stack: "AppBridgeError: APP::ERROR::INVALID_OPTIONS: `type…rown for path: ['message'] and value: `undefined`"}

And:

 

Uncaught TypeError: Cannot read property 'unsubscribe' of undefined
    at Modal.componentWillUnmount (Modal.js:93)
    at callComponentWillUnmountWithTimer (react-dom.development.js:21896)
    at HTMLUnknownElement.callCallback (react-dom.development.js:336)
    at HTMLUnknownElement.sentryWrapped (helpers.ts:85)
    at Object.invokeGuardedCallbackDev (react-dom.development.js:385)
    at invokeGuardedCallback (react-dom.development.js:440)
    at safelyCallComponentWillUnmount (react-dom.development.js:21903)
    at commitUnmount (react-dom.development.js:22392)
    at commitNestedUnmounts (react-dom.development.js:22486)
    at unmountHostComponents (react-dom.development.js:22810)
    at commitDeletion (react-dom.development.js:22896)
    at commitMutationEffects (react-dom.development.js:25323)
    at HTMLUnknownElement.callCallback (react-dom.development.js:336)
    at HTMLUnknownElement.sentryWrapped (helpers.ts:85)
    at Object.invokeGuardedCallbackDev (react-dom.development.js:385)
    at invokeGuardedCallback (react-dom.development.js:440)
    at commitRootImpl (react-dom.development.js:25050)
    at unstable_runWithPriority (scheduler.development.js:697)
    at runWithPriority$2 (react-dom.development.js:12149)
    at commitRoot (react-dom.development.js:24922)
    at finishSyncRender (react-dom.development.js:24329)
    at performSyncWorkOnRoot (react-dom.development.js:24307)
    at react-dom.development.js:12199
    at unstable_runWithPriority (scheduler.development.js:697)
    at runWithPriority$2 (react-dom.development.js:12149)
    at flushSyncCallbackQueueImpl (react-dom.development.js:12194)
    at flushSyncCallbackQueue (react-dom.development.js:12182)
    at scheduleUpdateOnFiber (react-dom.development.js:23709)
    at dispatchAction (react-dom.development.js:17076)
    at Object.next (QueryData.ts:282)
    at notifySubscription (Observable.js:135)
    at onNotify (Observable.js:179)
    at SubscriptionObserver.next (Observable.js:235)
    at ObservableQuery.ts:701
    at Array.forEach (<anonymous>)
    at iterateObserversSafely (ObservableQuery.ts:701)
    at Object.next (ObservableQuery.ts:662)
    at invoke (QueryManager.ts:518)
    at QueryManager.ts:644
    at QueryManager.ts:1091
    at Set.forEach (<anonymous>)
    at QueryManager.ts:1087
    at Map.forEach (<anonymous>)
    at QueryManager.broadcastQueries (QueryManager.ts:1085)
    at QueryManager.ts:1237
    at Object.next (Observable.js:322)
    at notifySubscription (Observable.js:135)
    at onNotify (Observable.js:179)
    at SubscriptionObserver.next (Observable.js:235)
    at observables.ts:12

The Provider is sitting at the root of the project and looks like this:

 

import React, { useContext } from "react";
import {
  Provider as AppBridgeProvider,
  Loading,
  TitleBar,
  Context,
  Modal
} from "@shopify/app-bridge-react";
import apolloClient from "./apollo";
import { ApolloProvider } from "react-apollo";

import enTranslations from "@shopify/polaris/locales/en.json";
import { AppProvider } from "@shopify/polaris";
import Router from "./Router";

const App: React.FC = () => {
  const shopOrigin =
    document.getElementById("shopify-app-init")!.dataset.shopOrigin || "";

  const config = {
    apiKey: "***************************8969",
    shopOrigin: shopOrigin
  };

  return (
      <ApolloProvider client={apolloClient}>
        <AppProvider i18n={enTranslations}>
          <AppBridgeProvider config={config}>
            <Router></Router>
          </AppBridgeProvider>
        </AppProvider>
      </ApolloProvider>
  );
};

export default App;

And in the Router component:

 

import React from "react";
import { BrowserRouter } from "react-router-dom";
import Routes from "./Routes";
import { Context } from "@shopify/app-bridge-react";

export default function Router() {
  return (
    <BrowserRouter>
      <Context.Consumer>
        {app => {
          // Do something with App Bridge `app` instance...
          if (!app) {
            return null;
          }

          return <Routes bridgeApp={app}></Routes>;
        }}
      </Context.Consumer>
    </BrowserRouter>
  );
}

The way the documentation for the Modal component is written, it seems as if you can just drop it in and have it work, but I haven't had any luck getting it to happen so far.

1 Like
Highlighted
Shopify Staff
Shopify Staff
26 7 10

Hi ellitt,

 

Can you post a complete example that includes your usage of the Modal? The stack trace shows the modal `componentWillUnmount` being called, which is very strange.

 

Could you also try to test out a minimal example with just the Modal to make sure App Bridge is working with your app?

 

 

function App() {
  const config = {
    apiKey: "YOUR_API_KEY",
    shopOrigin: "YOUR_SHOP_ORIGIN".
  };

  return (
      <ApolloProvider client={apolloClient}>
        <AppProvider i18n={enTranslations}>
          <AppBridgeProvider config={config}>
            <Modal title="My modal" message="Hello world!" open />
          </AppBridgeProvider>
        </AppProvider>
      </ApolloProvider>
  );
};

 

 

 

0 Likes
Highlighted
Tourist
5 0 1

That minimal implementation did render a modal correctly.

 

Here's a trail of how the modal is being used:

 

Dashboard.tsx component

import React, { useState } from "react";
import { Layout, Page } from "@shopify/polaris";
import { Modal } from "@shopify/app-bridge-react";

export default function Dashboard() {
  const [showDownloadModal, setShowDownloadModal] = useState(false);

  return (
    <>
      <Page narrowWidth>
        <Layout>
          <div>Hello world!</div>
        </Layout>
      </Page>
      <Modal
        open={showDownloadModal}
        title="Download"
        onClose={() => setShowDownloadModal(false)}
      >
        <div className="flex justify-around m-5">
          <p>iOS</p>
          <p>Android</p>
        </div>
      </Modal>
    </>
  );
}

 

 

 

Here's the Routes.tsx component, where Dashboard is used

 

import React from "react";
import { Switch, Route, withRouter } from "react-router";
import Dashboard from "./screens/Dashboard";

export default withRouter(() => {
  return (
    <Switch>
      <Route path="/" render={() => <Dashboard></Dashboard>}></Route>
    </Switch>
  );
});

 

 

Router.tsx component, where Routes is used

 

import React from "react";
import { BrowserRouter } from "react-router-dom";
import Routes from "./Routes";
import { Context } from "@shopify/app-bridge-react";

export default function Router() {
  return (
    <BrowserRouter>
      <Context.Consumer>
        {app => {
          // console.log(app);
          // Do something with App Bridge `app` instance...
          if (!app) {
            return null;
          }

          return <Routes></Routes>;
        }}
      </Context.Consumer>
    </BrowserRouter>
  );
}

 

 

And App.tsx, where Router is used

 

import React from "react";
import { Provider as AppBridgeProvider } from "@shopify/app-bridge-react";
import apolloClient from "./apollo";
import { ApolloProvider } from "react-apollo";

import enTranslations from "@shopify/polaris/locales/en.json";
import { AppProvider } from "@shopify/polaris";
import Router from "./Router";

const App: React.FC = () => {
  const shopOrigin = (window as any).shopifyShopOrigin;
  const apiKey = (window as any).shopifyApiKey;
  const config = {
    apiKey,
    shopOrigin
  };

  return (
    <ApolloProvider client={apolloClient}>
      <AppProvider i18n={enTranslations}>
        <AppBridgeProvider config={config}>
          <Router></Router>
        </AppBridgeProvider>
      </AppProvider>
    </ApolloProvider>
  );
};

export default App;

 

 

Those together end up with the long list of errors from the original post.

0 Likes
Highlighted
Tourist
5 0 1

It seems like it's something to do with the message prop being missing and/or trying to put child nodes inside the modal.

 

This works: 

 

import React from "react";
import {
  Provider as AppBridgeProvider,
  Modal
} from "@shopify/app-bridge-react";
import apolloClient from "./apollo";
import { ApolloProvider } from "react-apollo";

import enTranslations from "@shopify/polaris/locales/en.json";
import { AppProvider } from "@shopify/polaris";

const App: React.FC = () => {
  const shopOrigin = (window as any).shopifyShopOrigin;
  const apiKey = (window as any).shopifyApiKey;
  const config = {
    apiKey,
    shopOrigin
  };

  return (
    <ApolloProvider client={apolloClient}>
      <AppProvider i18n={enTranslations}>
        <AppBridgeProvider config={config}>
          <Modal open={true} title="test" message="test"></Modal>
        </AppBridgeProvider>
      </AppProvider>
    </ApolloProvider>
  );
};

export default App;

 

 

This does not work:

import React from "react";
import {
  Provider as AppBridgeProvider,
  Modal
} from "@shopify/app-bridge-react";
import apolloClient from "./apollo";
import { ApolloProvider } from "react-apollo";

import enTranslations from "@shopify/polaris/locales/en.json";
import { AppProvider } from "@shopify/polaris";

const App: React.FC = () => {
  const shopOrigin = (window as any).shopifyShopOrigin;
  const apiKey = (window as any).shopifyApiKey;
  const config = {
    apiKey,
    shopOrigin
  };

  return (
    <ApolloProvider client={apolloClient}>
      <AppProvider i18n={enTranslations}>
        <AppBridgeProvider config={config}>
          <Modal open={true} title="test"></Modal>
        </AppBridgeProvider>
      </AppProvider>
    </ApolloProvider>
  );
};

export default App;

And this does "work" in the sense that it shows up on the page, but it doesn't actually render the <span>child test</span>.

import React from "react";
import {
  Provider as AppBridgeProvider,
  Modal
} from "@shopify/app-bridge-react";
import apolloClient from "./apollo";
import { ApolloProvider } from "react-apollo";

import enTranslations from "@shopify/polaris/locales/en.json";
import { AppProvider } from "@shopify/polaris";

const App: React.FC = () => {
  const shopOrigin = (window as any).shopifyShopOrigin;
  const apiKey = (window as any).shopifyApiKey;
  const config = {
    apiKey,
    shopOrigin
  };

  return (
    <ApolloProvider client={apolloClient}>
      <AppProvider i18n={enTranslations}>
        <AppBridgeProvider config={config}>
          <Modal open={true} title="test" message="test">
            <span>child test</span>
          </Modal>
        </AppBridgeProvider>
      </AppProvider>
    </ApolloProvider>
  );
};

export default App;

 

0 Likes
Highlighted
Tourist
5 0 1

Any updates?

0 Likes
Highlighted

Success.

Shopify Staff
Shopify Staff
26 7 10

Hi ellitt,

 

The App Bridge Modal requires either a `message`  or a `src`.  The modal will not render if neither are provided.

Unfortunately, it also does not support rendering arbitrary children inside the modal. This is because the modal is rendered in Shopify Admin and there is currently no way to re-construct DOM nodes specified by the app inside Shopify Admin. If you need dynamic content inside the modal, the only way to do that is to create a route in your app that renders only the modal content. You can then set the modal `src` to point to that route.

 

Here's a simplified example:

 

import React from "react";
import {Switch, Route} from 'react-router';
import {Provider as AppBridgeProvider, Modal} from '@shopify/app-bridge-react';

function Main() {
  return <Modal title="My modal" src="/myModal" open />;
}

function MyModal() {
  return (
    <div>
      <h1>My Modal</h1>
      <div>My content</div>
    </div>
  );
}

function App() {
  const config = {
    apiKey: 'YOUR_API_KEY',
    shopOrigin: 'YOUR_SHOP_ORIGIN',
  };

  return (
    <AppBridgeProvider config={config}>
      <Switch>
        <Route path="/" render={() => <Main />}></Route>
        <Route path="/myModal" render={() => <MyModal />}></Route>
      </Switch>
    </AppBridgeProvider>
  );
}

 

Let me know if this makes sense. I've added an issue to our backlog to clarify the requirements/limitations in the Help docs for the App Bridge Modal.

 

Trish

0 Likes
Highlighted
Tourist
5 0 1

I see. Updating those docs and correcting the TypeScript typings would be very helpful since children is presented as a usable prop and message is an optional prop.

 

Thanks for looking into it!

0 Likes