UNPKG

12 kBMarkdownView Raw
1# `@shopify/app-bridge-host`
2
3App 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](../app-bridge/). This package is used by Shopify's web admin.
4
5[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE.md)
6
7## App Bridge Host architecture
8
9The 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.
10
11Functionality 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.
12
13Features 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.
14
15The `<HostProvider>` is not responsible for rendering the client app, by Iframe or other means.
16
17## Building an App Bridge host
18
19You can create your own App Bridge host using the `<HostProvider>` component.
20
21`<HostProvider>` requires three types of data: app configuration, functionality to provide to the client app, and an initial state for the store.
22
23### App configuration
24
25The `<HostProvider>` requires configuration information about the client app to be loaded:
26
27```js
28const config = {
29 apiKey: 'API key from Shopify Partner Dashboard',
30 appId: 'app id from GraphQL',
31 handle: 'my-app-handle',
32 shopId: 'shop id from GraphQL',
33 url: 'app url from Shopify Partner Dashboard',
34 name: 'app name',
35};
36```
37
38Note that we'll be referring to this sample config throughout the examples below.
39
40#### Providing functionality
41
42The `<HostProvider>` does not load any components by default. In order to provide a feature to an app, you must load the necessary component(s).
43
44You can find pre-defined host UI components inside the `@shopify/app-bridge-host/components` directory. You can also write your own components.
45
46#### Initial state
47
48In 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.
49
50The `<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.
51
52The `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`:
53
54```tsx
55import {HostProvider} from '@shopify/app-bridge-host';
56import {Loading} from '@shopify/app-bridge-host/components/Loading';
57import {Modal} from '@shopify/app-bridge-host/components/Modal';
58import {Navigation} from '@shopify/app-bridge-host/components/Navigation';
59
60import {Group} from '@shopify/app-bridge/actions';
61import {setFeaturesAvailable} from '@shopify/app-bridge-host/store';
62
63const initialState = {
64 features: setFeaturesAvailable(Group.Loading, Group.Modal, Group.Navigation),
65};
66
67function Host() {
68 return (
69 <HostProvider
70 config={config}
71 components={[Loading, Modal, Navigation]}
72 initialState={initialState}
73 />
74 );
75}
76```
77
78### Custom components
79
80`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.
81
82Your custom components can also be functional components that don't render UI, you just need to ensure it returns `null`:
83
84```tsx
85function nonUI() {
86 // do something non UI
87 return null;
88}
89```
90
91#### withFeature
92
93To 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`).
94
95This 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.
96
97Here is an example of creating a custom component that utilizes the App Bridge `Toast` feature, rendering the `Toast` component from Polaris.
98
99```tsx
100import {HostProvider, ComponentProps, withFeature} from '@shopify/app-bridge-host';
101import {
102 feature as toastFeature,
103 WithFeature,
104} from '@shopify/app-bridge-host/store/reducers/embeddedApp/toast';
105import {Toast} from '@shopify/polaris';
106import compose from '@shopify/react-compose';
107
108function CustomToastComponent(props: WithFeature) {
109 const {
110 actions,
111 store: {content},
112 } = props;
113
114 if (!content) {
115 return null;
116 }
117
118 const {duration, error, id, message} = content;
119 return (
120 <Toast
121 error={error}
122 duration={duration}
123 onDismiss={() => actions.clear({id: id})}
124 content={message}
125 />
126 );
127}
128
129const Toast = compose<ComponentProps>(withFeature(toastFeature))(CustomToastComponent);
130
131const initialState = {
132 features: setFeaturesAvailable(Group.Toast),
133};
134
135function Host() {
136 return <HostProvider config={config} initialState={initialState} components={[Toast]} />;
137}
138```
139
140#### useFeature
141
142Use 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`).
143
144The 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.
145
146One thing to note is that you need to do an `undefined` check before accessing the `store` to prevent errors.
147
148Here is an example of creating a custom component that utilizes the App Bridge `TitleBar` and `Menu` feature and uses the [`useRouterContext` hook](src/hooks/README.md#useRouterContext) to access the router:
149
150```tsx
151import {HostProvider, useRouterContext, useFeature} from '@shopify/app-bridge-host';
152import {feature as titleBarFeature} from '@shopify/app-bridge-host/store/reducers/embeddedApp/titleBar';
153import {feature as menuFeature} from '@shopify/app-bridge-host/store/reducers/embeddedApp/menu';
154import {Toast} from '@shopify/polaris';
155
156function TitleBarWithMenu() {
157 const {appRoot} = useRouterContext();
158 const [titleBarStore, titleBarActions] = useFeature(titleBarFeature);
159 const [menuStore, menuActions] = useFeature(menuFeature);
160
161 const items = menuStore?.navigationMenu?.items || [];
162
163 return (
164 <div>
165 <div>{titleBarStore?.title}</div>
166 <ul>
167 {items.map(({label, destination: {path}}) => {
168 const appUrl = `/admin/apps/${appRoot}`;
169 const href = `${appUrl}/${path}`;
170 return (
171 <li>
172 <a href={href}>{label}</a>
173 </li>
174 );
175 })}
176 </ul>
177 </div>
178 );
179}
180
181const initialState = {
182 features: setFeaturesAvailable(Group.TitleBar, Group.Menu),
183};
184
185function Host() {
186 return (
187 <HostProvider config={config} initialState={initialState} components={[TitleBarWithMenu]} />
188 );
189}
190```
191
192#### useFeature
193
194### Asynchronous components
195
196You can load host components asynchronously, ie using [@shopify/react-async](https://github.com/Shopify/quilt/tree/master/packages/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.
197
198```tsx
199import {createAsyncComponent, DeferTiming} from '@shopify/react-async';
200import {HostProvider} from '@shopify/app-bridge-host';
201
202const Loading = createAsyncComponent<ComponentProps>({
203 load: () =>
204 import(
205 /* webpackChunkName: 'AppBridgeLoading' */ '@shopify/app-bridge-host/components/Loading'
206 ),
207 defer: DeferTiming.Idle,
208 displayName: 'AppBridgeLoading',
209});
210
211function Host() {
212 return <HostProvider config={config} components={[Loading]} router={router} />;
213}
214```
215
216### Rendering the client app, with navigation
217
218Since `<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.
219
220If 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.
221
222The following example shows a simple router being passed into `<HostProvider>`, along with the `MainFrame` and `Navigation` components:
223
224```ts
225import {HostProvider} from '@shopify/app-bridge-host';
226import {MainFrame} from '@shopify/app-bridge-host/components/MainFrame';
227import {Navigation} from '@shopify/app-bridge-host/components/Navigation';
228
229const router = {
230 location: {
231 pathname: window.location.pathname,
232 search: window.location.search,
233 },
234 history: {
235 push(path: string) {
236 window.history.pushState('', null, path);
237 },
238 replace(path: string) {
239 window.history.replaceState('', null, path);
240 },
241 },
242};
243
244const initialState = {
245 features: setFeaturesAvailable(Group.Navigation),
246};
247
248function Host() {
249 return (
250 <HostProvider
251 config={config}
252 components={[MainFrame, Navigation]}
253 initialState={initialState}
254 router={router}
255 />
256 );
257}
258```
259
260Note 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`.
261
262### Communicating with the loaded app
263
264Certain 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.
265
266You can communicate with the client app by using the [`useHostContext` hook](src/hooks/README.md#useHostContext).
267
268The following example shows a Session Token component communicating with the client app:
269
270```tsx
271import {HostProvider, useHostContext, useFeature} from '@shopify/app-bridge-host';
272import {SessionToken} from '@shopify/app-bridge/actions';
273import {feature} from '@shopify/app-bridge-host/features/sessionToken';
274
275function SessionTokenComponent() {
276 const {app} = useHostContext();
277 const [store, actions] = useFeature(feature);
278
279 useEffect(() => {
280 return app.subscribe(SessionToken.ActionType.REQUEST, () =>
281 actions.respond({sessionToken: 'TEST-SESSION-TOKEN'}),
282 );
283 }, [actions, hostContext]);
284
285 return null;
286}
287
288const initialState = {
289 features: setFeaturesAvailable(Group.SessionToken),
290};
291
292function Host() {
293 return (
294 <HostProvider
295 config={config}
296 components={[SessionTokenComponent]}
297 initialState={initialState}
298 />
299 );
300}
301```