UNPKG

18 kBMarkdownView Raw
1# `@shopify/react-async`
2
3[![Build Status](https://travis-ci.org/Shopify/quilt.svg?branch=master)](https://travis-ci.org/Shopify/quilt)
4[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE.md) [![npm version](https://badge.fury.io/js/%40shopify%2Freact-async.svg)](https://badge.fury.io/js/%40shopify%2Freact-async.svg) [![npm bundle size (minified + gzip)](https://img.shields.io/bundlephobia/minzip/@shopify/react-async.svg)](https://img.shields.io/bundlephobia/minzip/@shopify/react-async.svg)
5
6Tools for creating powerful, asynchronously-loaded React components.
7
8## Installation
9
10```bash
11$ yarn add @shopify/react-async
12```
13
14## Usage
15
16### `createAsyncComponent()`
17
18`createAsyncComponent` is a function for creating components that are loaded asynchronously on initial mount. However, the resulting component does more than just help you split up your application along component lines; it also supports customized rendering for loading and errors, and creates additional hooks and components for smartly preloading the component and its dependencies. Best of all, in conjunction with the Babel and Webpack plugins provided by [`@shopify/async`](../async), you can easily extract the bundles needed to render your application during server side rendering.
19
20To start, import the `createAsyncComponent` function. The simplest use of this function requires just a `load` function, which returns a promise for a component:
21
22```tsx
23import {createAsyncComponent} from '@shopify/react-async';
24
25const MyComponent = createAsyncComponent({
26 load: () => import('./MyComponent'),
27});
28```
29
30This function returns a component that accepts the same props as the original one.
31
32#### Preload, prefetch, and keep fresh
33
34`createAsyncComponent` also adds a few static members that are themselves components: `Preload`, `Prefetch`, and `KeepFresh`. Likewise, it provides a set of hooks, which allow for more complex preloading use-cases.
35
36```tsx
37const MyComponent = createAsyncComponent({
38 load: () => import('./MyComponent'),
39});
40
41// All of these are available:
42<MyComponent />
43
44<MyComponent.Preload />
45<MyComponent.Prefetch />
46<MyComponent.KeepFresh />
47
48MyComponent.usePreload();
49MyComponent.usePrefetch();
50MyComponent.useKeepFresh();
51```
52
53By default, all of these special hooks and components will preload the assets for the asynchronously-imported components. However, you can provide additional logic to perform with the `usePreload`, `usePrefetch`, and `useKeepFresh` options to `createAsyncComponent`:
54
55```tsx
56const MyComponent = createAsyncComponent({
57 load: () => import('./MyComponent'),
58 usePrefetch: () => {
59 const networkCache = useContext(MyNetworkCache);
60 return () => networkCache.preload('/component/data/endpoint');
61 },
62});
63
64// Now prefetches the component assets, and performs the custom prefetch
65<MyComponent.Prefetch />;
66
67// Returns a function that will do all of the above, but allows you to run
68// it at a specific time
69const prefetch = MyComponent.usePrefetch();
70```
71
72While you can supply whatever logic you like for these, we recommend that you use them for the following purposes:
73
74- `Preload`: loading resources that will be used by the component
75- `Prefetch`: loading resources **and** data that will be used by the component
76- `KeepFresh`: loading resources and data that will be used by the component, and keeping data up to date
77
78If you want arguments for your `usePreload`, `usePrefetch`, or `useKeepFresh` hooks, simply specify them in the matching option for that component. These options must be some object type, and will then be used as expected arguments to the hook, and expected props for the component.
79
80```tsx
81const MyComponent = createAsyncComponent({
82 load: () => import('./MyComponent'),
83 usePrefetch: ({id}: {id: string}) => (
84 const networkCache = useContext(MyNetworkCache);
85 return () => networkCache.preload(`/data/for/${id}`);
86 ),
87});
88
89// This is a type error, we need an `id` option/ prop!
90<MyComponent.Prefetch />;
91MyComponent.usePrefetch();
92
93// Much better!
94<MyComponent.Prefetch id="123" />;
95MyComponent.usePrefetch({id: '123'});
96```
97
98This system is designed to work well with our [`@shopify/react-graphql` package](../react-graphql). Simply create an async GraphQL query using that library, and then `usePrefetch`, `usePreload`, and `useKeepFresh` that component alongside the React component itself:
99
100```tsx
101import {
102 createAsyncComponent,
103 // `usePreload`, `usePrefetch`, and `useKeepFresh` are convenience hooks
104 // that will just call `AsyncComponentType.useX`.
105 usePreload,
106 usePrefetch,
107 useKeepFresh,
108} from '@shopify/react-async';
109import {createAsyncQueryComponent} from '@shopify/react-graphql';
110
111const MyQuery = createAsyncQueryComponent({
112 load: () => import('./graphql/MyQuery.graphql'),
113});
114
115const MyComponent = createAsyncComponent({
116 load: () => import('./MyComponent'),
117 renderLoading: () => <Loading />,
118 // If you use `graphql-typescript-definitions` for generating types from your
119 // GraphQL documents, you'll be warned if there are required variables you aren’t
120 // providing here!
121 usePreload: () => usePreload(MyQuery),
122 usePrefetch: () => usePrefetch(MyQuery),
123 useKeepFresh: () => useKeepFresh(MyQuery),
124});
125```
126
127#### Deferring components
128
129By default, components are loaded as early as possible. This means that, if the library can load your component synchronously, it will try to do so. If that is not possible, it will instead load it in after the component is mounted. In some cases, a component may not be important enough to warrant being loaded early. This library exposes a few ways of "deferring" the loading of the component to an appropriate time.
130
131If a component should always be deferred in some way, you can pass a custom `defer` option to `createAsyncComponent`. This property should be a member of the `DeferTiming` enum, which currently allows you to force deferring the component until:
132
133- Component mount (`DeferTiming.Mount`; note that this will defer it until mount even if the component could have been resolved synchronously),
134- Browser idle (`DeferTiming.Idle`; if `window.requestIdleCallback` is not available, it will load on mount), or
135- Component is in the viewport (`DeferTiming.InViewport`; if `IntersectionObserver` is not available, it will load on mount)
136
137```tsx
138import {createAsyncComponent, DeferTiming} from '@shopify/react-async';
139
140// No deferring
141const MyComponent = createAsyncComponent({
142 load: () => import('./MyComponent'),
143});
144
145// Never load synchronously, always start load in mount
146const MyComponentOnMount = createAsyncComponent({
147 load: () => import('./MyComponent'),
148 defer: DeferTiming.Mount,
149});
150
151// Never load synchronously, always start load in requestIdleCallback
152const MyComponentOnIdle = createAsyncComponent({
153 load: () => import('./MyComponent'),
154 defer: DeferTiming.Idle,
155});
156
157// Never load synchronously, always start load in when any part of
158// the component is intersecting the viewport
159const MyComponentOnIdle = createAsyncComponent({
160 load: () => import('./MyComponent'),
161 defer: DeferTiming.InViewport,
162});
163```
164
165You can also pass a function to `defer`. This function, which will be called with the current props of the component, should return `true` when the component should begin loading. This makes it easy to implement components that have their visibility controlled by a property, like the Polaris Modal’s `open` prop:
166
167```tsx
168const MyModalComponent = createAsyncComponent({
169 load: () => import('./MyModalComponent'),
170 defer: ({open}) => open,
171});
172```
173
174#### Progressive hydration
175
176It can sometimes be useful to server render a component, but to wait to load its assets until later in the page lifecycle. This is particularly relevant for large, mostly static components, components that are very likely to be outside the viewport on load, and expensive components that would cause significant layout shifts if only rendered on the client. This library supports this pattern through the `deferHydration` option, and with the help from the [`@shopify/react-hydrate` package](../react-hydrate).
177
178> **Note:** for progressive hydration to work, you **must** render either a `HydrationTracker` component from `@shopify/react-hydrate` or an `HtmlUpdater` from `@shopify/react-html>=9.0.0` somewhere in your app.
179
180In defining your async component, simply set the `deferHydration` option to one of the `DeferTiming` enum values (or, as noted in the previous example, a function that accepts props and returns a boolean).
181
182```tsx
183const Expensive = createAsyncComponent({
184 load: () => import('./Expensive'),
185 deferHydration: DeferTiming.InViewport,
186});
187```
188
189The resulting component has special loading behavior that differs by environment:
190
191- On the server, the component renders synchronously, but **does not** mark assets as used.
192- On the client, when the app is undergoing hydration, the component persists its server-rendered markup, even though its assets are not yet available. When the condition specified by `deferHydration` is met, the assets will be loaded, and the component will be hydrated.
193- On the client, when the app has already undergone hydration, the component will begin loading on mount, and will show the result of calling `renderLoading`, just like any other async component.
194
195### `usePreload`, `usePrefetch`, and `useKeepFresh`
196
197These hooks are provided as conveniences for extracting functions from an async component that can be used to preload, prefetch, or keep fresh. The result is identical to calling `AsyncComponent.useX` directly, but works better with the tooling around hooks, which often does not understand the "static member as hook" pattern.
198
199```tsx
200import {createAsyncComponent, usePreload} from '@shopify/react-async`
201
202const Expensive = createAsyncComponent({
203 load: () => import('./Expensive'),
204 deferHydration: DeferTiming.InViewport,
205});
206
207function MyComponent({children}) {
208 const preload = usePreload(Expensive);
209 return <div onMouseEnter={preload}>{children}</div>;
210}
211```
212
213### `useAsync`
214
215The `useAsync` hook is a primitive that can be used by other libraries to create asynchronous components with different behaviors. One example is [`@shopify/react-graphql`](../react-graphql), where this hook is used to implement asynchronous GraphQL query components.
216
217This hook accepts two arguments:
218
219- `resolver`, an object matching the `Resolver` type from `@shopify/async`. This object is in charge of managing the loading of an asynchronous resource. It also controls the type of the result returned from the `useAsync` hook. The easiest way to construct one is to use the `createResolver` function from `@shopify/async`.
220- `options`, an optional object with any of the following properties:
221 - `assetTiming`: when the assets for this async asset will be used. Should be a member of the `AssetTiming` enum. This is used to register the asset as used, as documented [below](#useAsyncAsset). Defaults to `AssetTiming.Immediate`
222 - `immediate`: whether the hook should attempt to resolve the async asset synchronously (using `resolver.resolved`). Defaults to `true` if `assetTiming` is `Immediate`, and `false` otherwise.
223
224The `useAsync` hook returns an object containing details about the asynchronous asset. This object includes the following properties:
225
226- `id`: the ID of the asset, as specified by `resolver.id`.
227- `resolved`: `null` if the asset has not resolved, or had an error during resolution, and otherwise will be the unwrapped promise value returned by `resolver.resolve()`.
228- `loading`: whether the asset has been loaded yet.
229- `error`: an `Error` object, if calling `resolver.resolve()` rejected.
230- `load`: a function that can be called to begin the loading process.
231
232The following example demonstrates how to use the `useAsync` hook to implement the default `usePreload` provided to async components:
233
234```tsx
235import {createResolver} from '@shopify/async';
236import {useAsync, AssetTiming} from '@shopify/react-async';
237
238const resolver = createResolver({
239 id: () => require.resolveWeak('./MyComponent'),
240 load: () => import('./MyComponent'),
241});
242
243function usePreload() {
244 return useAsync(resolver, {
245 assetTiming: AssetTiming.NextPage,
246 }).load;
247}
248```
249
250### `PrefetchRoute` and `Prefetcher`
251
252The `PrefetchRoute` component allows you to use the asynchronous component you generated with `createAsyncComponent` and automatically render its `Prefetch` component when the user looks like they are going to navigate to a page that uses it. This component takes as its props the asynchronous component, a path pattern to look for (a string or `RegExp` that is compared against the target pathname), and an optional function that can map the URL to a set of props for your prefetch component.
253
254Consider this async component:
255
256```tsx
257const ProductDetails = createAsyncComponent({
258 load: () => import('./ProductDetails'),
259 usePrefetch: ({id}: {id: string}) => <PrefetchGraphQLQuery id={id} />,
260});
261```
262
263This component might be rendered when the URL matches `/products/:id`. If we want to prefetch this component (including its GraphQL query!) whenever the user is going to navigate to a matching URL, we would register this intent with the following `PrefetchRoute` component:
264
265```tsx
266<PrefetchRoute
267 path={/^\/products\/(\d+)$/}
268 render={url => {
269 const id = url.pathname.split('/').pop();
270 return <ProductDetails.Prefetch id={id} />;
271 }}
272/>
273```
274
275To make the routes actually prefetch, you will need to add the `Prefetcher` component somewhere in your app. This component should only ever be rendered once, and will need to be somewhere that has access to all the context the prefetched components may depend on (for example, if your prefetching includes prefetching GraphQL data with Apollo, you will need to put this component below your `ApolloProvider`).
276
277```tsx
278<Prefetcher />
279```
280
281And that’s it. While we reserve the right to change it, the basic process for determining merchant navigation intent is fairly simple. We listen for users mousing over or focusing in to elements with an `href` attribute (or, `data-href`, if you can’t use a real link) and, if the user doesn’t mouse/ focus out in some small amount of time, we prefetch all matching components. We also do the prefetch when the user begins their click on an element with an `href` attribute.
282
283### `AsyncAssetManager` and `AsyncAssetContext`
284
285`AsyncAssetManager` and `AsyncAssetContext` allow you to extract the asynchronous bundles that were required for your application. If you use the Babel plugin, every component created by `createAsyncComponent` will report its existence when rendered to an `AsyncAssetManager`.
286
287To make use of this feature, you will need to use [`react-effect`](../react-effect). It will automatically extract the information and clear extraneous bundles between tree traversals.
288
289To extract the used assets, you can call `AsyncAssetManager#used()`. This method accepts an `AssetTiming`, or an array of `AssetTiming`s, which specify which assets were actually used.
290
291```tsx
292import {extract} from '@shopify/react-effect/server';
293import {AsyncAssetManager, AsyncAssetContext} from '@shopify/react-async';
294
295const asyncAssetmanager = new AsyncAssetManager();
296
297await extract(<App />, {
298 decorate(app) {
299 return (
300 <AsyncAssetContext.Provider value={asyncAssetmanager}>
301 {app}
302 </AsyncAssetContext.Provider>
303 );
304 },
305});
306
307const assetSelectors = [...asyncAssetmanager.used(AssetTiming.Immediate)];
308```
309
310These asset selectors indicate an `id` for the asset, and whether scripts and/ or styles should be included for the passed asset timings. The IDs can be looked up in the manifest created by `@shopify/async`’s Webpack plugin. If you are using [`sewing-kit-koa`](../sewing-kit-koa), you can follow the instructions from that package to automatically collect the required JavaScript and CSS bundles.
311
312#### `useAsyncAsset()`
313
314Other libraries may need to register an async asset as being used. They can do so with the `useAsyncAsset` hook, which accepts an optional string ID, and an optional object containing `styles` and `scripts` fields with `AssetTiming` values.
315
316The `AssetTiming` enum values allow you to specify how high-priority an asset is, which can be used to determine how to load that asset on the initial render:
317
318- `AssetTiming.Immediate`: load the asset as early as possible, but definitely before the initial hydration of the React application.
319- `AssetTiming.CurrentPage`: the asset is not needed before hydration, but is very likely to be used for other content on the current page (used for deferred and progressively hydrated components).
320- `AssetTiming.NextPage`: the asset is not needed for the current page, but may be needed after navigation (used for preloading, prefetching, and keeping fresh).
321
322> **Note:** `useAsync` calls `useAsyncAsset` under the hood, so you likely do not need to call it directly.
323
324### `createAsyncContext()`
325
326Most of the time, it makes sense to split your application along component boundaries. However, you may also have a reason to split off a part of your app that is not a component. To accomplish this, `react-async` provides a `createAsyncContext()` function. This function also takes an object with a `load` property that is a promise for the value you are splitting. The returned object mimics the shape of `React.createContext()`, except that the `Provider` component does not need a value supplied:
327
328```tsx
329const ExpensiveFileContext = createAsyncContext({
330 load: () => import('./a-csv-for-some-reason.csv'),
331});
332
333// Somewhere in your app, create the provider:
334
335<ExpensiveFileContext.Provider>
336 {/* consuming code goes here */}
337</ExpensiveFileContext.Provider>;
338
339// and use the consumer to access the value:
340
341<ExpensiveFileContext.Consumer>
342 {file => (file ? <CsvViewer file={file} /> : null)}
343</ExpensiveFileContext.Consumer>;
344```
345
346The typing of the render prop for the `Consumer` component always includes `null`, which is used to represent that the async value has not yet loaded successfully.