Solved

@shopify/app-bridge-react documentation

ellitt
Excursionist
18 0 10

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.

Accepted Solution (1)
Trish_Ta
Shopify Staff
58 13 27

This is an accepted solution.

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

Trish | Shopify 
 - Was my reply helpful? Click Like to let me know! 
 - Was your question answered? Mark it as an Accepted Solution
 - To learn more visit the Shopify Help Center or the Shopify Blog

View solution in original post

Replies 9 (9)

Trish_Ta
Shopify Staff
58 13 27

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>
  );
};

 

 

 

Trish | Shopify 
 - Was my reply helpful? Click Like to let me know! 
 - Was your question answered? Mark it as an Accepted Solution
 - To learn more visit the Shopify Help Center or the Shopify Blog

ellitt
Excursionist
18 0 10

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.

ellitt
Excursionist
18 0 10

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;

 

ellitt
Excursionist
18 0 10

Any updates?

Trish_Ta
Shopify Staff
58 13 27

This is an accepted solution.

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

Trish | Shopify 
 - Was my reply helpful? Click Like to let me know! 
 - Was your question answered? Mark it as an Accepted Solution
 - To learn more visit the Shopify Help Center or the Shopify Blog

ellitt
Excursionist
18 0 10

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!

pavindu
Tourist
11 0 6

This has not been still updated. Docs still say src prop is optional and there's no note other than this thread. Please improve the documentation, otherwise a lot of developers will be confused like me.

arhumali07
Tourist
9 0 0

i have tried your above code but i am getting this error

 

Cannot find module '@shopify/app-bridge-react'

 

any idea how can i solve this?

 

 

 

ellitt
Excursionist
18 0 10

Have you installed that package using npm or yarn?