@shopify/app-bridge-host
v6.0.0
Published
App Bridge Host contains components and middleware to be consumed by the app's host, as well as the host itself. The middleware and `Frame` component are responsible for facilitating communication between the client and host, and used to act on actions se
Downloads
278,038
Maintainers
Keywords
Readme
@shopify/app-bridge-host
App Bridge Host contains components and middleware to be consumed by the app's host, as well as the host itself. The middleware and Frame
component are responsible for facilitating communication between the client and host, and used to act on actions sent from the App Bridge client. This package is used by Shopify's web admin.
App Bridge Host architecture
The App Bridge host uses a cross-platform, modular architecture. The host has several responsibilities. First, the host brokers communication between contexts (ie between the app and Shopify Admin). Second, the host maintains a central store representing the current state of all App Bridge features, which is exposed to the client app. Third, the host provides functionality to the client app.
Functionality is exposed to the client app via App Bridge actions. When an action is dispatched using App Bridge, the host evaluates the action against the relevant reducers, which make changes to the central store of app state. The state is then passed to UI components, which render functionality based on the state.
Features and UI components are treated separately in App Bridge. A feature consists of an action set and reducers, and the associated UI component consumes the resulting state. Most UI components have an associated feature, but this is not required.
The <HostProvider>
is not responsible for rendering the client app, by Iframe or other means.
Building an App Bridge host
You can create your own App Bridge host using the <HostProvider>
component.
<HostProvider>
requires three types of data: app configuration, functionality to provide to the client app, and an initial state for the store.
App configuration
The <HostProvider>
requires configuration information about the client app to be loaded:
const config = {
apiKey: 'API key from Shopify Partner Dashboard',
appId: 'app id from GraphQL',
handle: 'my-app-handle',
shopId: 'shop id from GraphQL',
url: 'app url from Shopify Partner Dashboard',
name: 'app name',
};
Note that we'll be referring to this sample config throughout the examples below.
Providing functionality
The <HostProvider>
does not load any components by default. In order to provide a feature to an app, you must load the necessary component(s).
You can find pre-defined host UI components inside the @shopify/app-bridge-host/components
directory. You can also write your own components.
Initial state
In App Bridge, features are gated using a permission model, which lives in the store. All feature permissions default to false
. To provide a feature, you must also set the relevant permissions. If you don’t, the client app will not be permitted to use the feature, even if the component is available. Most components are associated with a single feature, but this is not a requirement.
The <HostProvider>
accepts an initial state for the store. This allows a host to pre-populate the store with information the app can immediately access, such as feature permissions.
The setFeaturesAvailable
utility can be used to build the initialState.features
object. The following example shows a host with several components, and the corresponding feature availability set in initialState
:
import {HostProvider} from '@shopify/app-bridge-host';
import Loading from '@shopify/app-bridge-host/components/Loading';
import Modal from '@shopify/app-bridge-host/components/Modal';
import Navigation from '@shopify/app-bridge-host/components/Navigation';
import {Group} from '@shopify/app-bridge/actions';
import {setFeaturesAvailable} from '@shopify/app-bridge-host/store';
const initialState = {
features: setFeaturesAvailable(Group.Loading, Group.Modal, Group.Navigation),
};
function Host() {
return (
<HostProvider
config={config}
components={[Loading, Modal, Navigation]}
initialState={initialState}
/>
);
}
Custom components
HostProvider
can render any type of React component; it’s not limited to the components in this package. You can use either the withFeature
decorator or the useFeature
hook to connect a custom component to an App Bridge feature.
Your custom components can also be functional components that don't render UI, you just need to ensure it returns null
:
function nonUI() {
// do something non UI
return null;
}
withFeature
To connect a component to the App Bridge host, wrap it using the withFeature
decorator. This decorator provides the component with access to the store
and actions
for a specified App Bridge feature (remember to set the corresponding feature permissions in initialState
).
This decorator only renders your custom component when the store
for the specified feature is not undefined
. This means you do not have to do an undefined
check before accessing the store.
Here is an example of creating a custom component that utilizes the App Bridge Toast
feature, rendering the Toast
component from Polaris.
import {HostProvider, ComponentProps, withFeature} from '@shopify/app-bridge-host';
import {
feature as toastFeature,
WithFeature,
} from '@shopify/app-bridge-host/store/reducers/embeddedApp/toast';
import {Toast} from '@shopify/polaris-internal';
import compose from '@shopify/react-compose';
function CustomToastComponent(props: WithFeature) {
const {
actions,
store: {content},
} = props;
if (!content) {
return null;
}
const {duration, error, id, message, action} = content;
return (
<Toast
error={error}
duration={duration}
onDismiss={() => actions.clear({id: id})}
content={message}
action={action}
/>
);
}
const Toast = compose<ComponentProps>(withFeature(toastFeature))(CustomToastComponent);
const initialState = {
features: setFeaturesAvailable(Group.Toast),
};
function Host() {
return <HostProvider config={config} initialState={initialState} components={[Toast]} />;
}
useFeature
Use the useFeature
hook to connect your component to the App Bridge host. This hook returns an array with the store
and actions
for a specified App Bridge feature (remember to set the corresponding feature permissions in initialState
).
The hook is useful when you want to use one component to handle multiple related features. For example, a single component can be used to render the Menu and Title Bar features.
One thing to note is that you need to do an undefined
check before accessing the store
to prevent errors.
Here is an example of creating a custom component that utilizes the App Bridge TitleBar
and Menu
feature and uses the useRouterContext
hook to access the router:
import {HostProvider, useRouterContext, useFeature} from '@shopify/app-bridge-host';
import {feature as titleBarFeature} from '@shopify/app-bridge-host/store/reducers/embeddedApp/titleBar';
import {feature as menuFeature} from '@shopify/app-bridge-host/store/reducers/embeddedApp/menu';
import {Toast} from '@shopify/polaris-internal';
function TitleBarWithMenu() {
const {appRoot} = useRouterContext();
const [titleBarStore, titleBarActions] = useFeature(titleBarFeature);
const [menuStore, menuActions] = useFeature(menuFeature);
const items = menuStore?.navigationMenu?.items || [];
return (
<div>
<div>{titleBarStore?.title}</div>
<ul>
{items.map(({label, destination: {path}}) => {
const appUrl = `/admin/apps/${appRoot}`;
const href = `${appUrl}/${path}`;
return (
<li>
<a href={href}>{label}</a>
</li>
);
})}
</ul>
</div>
);
}
const initialState = {
features: setFeaturesAvailable(Group.TitleBar, Group.Menu),
};
function Host() {
return (
<HostProvider config={config} initialState={initialState} components={[TitleBarWithMenu]} />
);
}
useFeature
Asynchronous components
You can load host components asynchronously, ie using @shopify/react-async. The <HostProvider>
handles adding the feature's reducer to the Redux store. Actions that are dispatched by the app before the feature is available is automatically queued and resolved once the feature's component is loaded.
import {createAsyncComponent, DeferTiming} from '@shopify/react-async';
import {HostProvider} from '@shopify/app-bridge-host';
const Loading = createAsyncComponent<ComponentProps>({
load: () =>
import(
/* webpackChunkName: 'AppBridgeLoading' */ '@shopify/app-bridge-host/components/Loading'
),
defer: DeferTiming.Idle,
displayName: 'AppBridgeLoading',
});
function Host() {
return <HostProvider config={config} components={[Loading]} router={router} />;
}
Rendering the client app, with navigation
Since <HostProvider>
is not responsible for rendering the client app, one of the components
must handle this task. The Apps section in Shopify Admin uses the MainFrame
component, which additionally requires a router context to provide navigation to the client app.
If you want to provide navigation capabilities to your app, you will need to include the Navigation
component and provide a router to the <HostProvider>
. The router should keep the app’s current location in sync with the host page’s current location, and manage updating the location when the route changes.
The following example shows a simple router being passed into <HostProvider>
, along with the MainFrame
and Navigation
components:
import {HostProvider} from '@shopify/app-bridge-host';
import MainFrame from '@shopify/app-bridge-host/components/MainFrame';
import Navigation from '@shopify/app-bridge-host/components/Navigation';
const router = {
location: {
pathname: window.location.pathname,
search: window.location.search,
},
history: {
push(path: string) {
window.history.pushState('', null, path);
},
replace(path: string) {
window.history.replaceState('', null, path);
},
},
};
const initialState = {
features: setFeaturesAvailable(Group.Navigation),
};
function Host() {
return (
<HostProvider
config={config}
components={[MainFrame, Navigation]}
initialState={initialState}
router={router}
/>
);
}
Note that since MainFrame
only renders the app itself and does not provide features to the app, there is no related initialState
. Navigation
, however, provides a feature to the app. To allow the app to use that feature, it is made made available in initialState
.
Communicating with the loaded app
Certain App Bridge feature requires subscribing to actions dispatched by the app. For example, the Auth Code or Session Token features both respond to a request action from the app.
You can communicate with the client app by using the useHostContext
hook.
The following example shows a Session Token component communicating with the client app:
import {HostProvider, useHostContext, useFeature} from '@shopify/app-bridge-host';
import {SessionToken} from '@shopify/app-bridge/actions';
import {feature} from '@shopify/app-bridge-host/features/sessionToken';
function SessionTokenComponent() {
const {app} = useHostContext();
const [store, actions] = useFeature(feature);
useEffect(() => {
return app.subscribe(SessionToken.Action.REQUEST, () =>
actions.respond({sessionToken: 'TEST-SESSION-TOKEN'}),
);
}, [actions, hostContext]);
return null;
}
const initialState = {
features: setFeaturesAvailable(Group.SessionToken),
};
function Host() {
return (
<HostProvider
config={config}
components={[SessionTokenComponent]}
initialState={initialState}
/>
);
}
Migrating from 2.x.x to 3.0.0
There is one breaking change in version 3.0.0.
- React moved to
peerDependencies
React moved to peerDependencies
In version 2.x.x, react
was installed as a direct dependency of this package. This can result in duplicate versions of the package being installed in consuming applications.
In version 3.0.0, react
has been moved to being a peer dependency. This will prevent multiple versions of the package being installed in consuming applications. If a consuming application doesn't currently have react
installed as a dependency, a version compliant with the peer dependency range will need to be added (i.e. ^17.0.2
).