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 |
|
6 | Tools 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 |
|
20 | To 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
|
23 | import {createAsyncComponent} from '@shopify/react-async';
|
24 |
|
25 | const MyComponent = createAsyncComponent({
|
26 | load: () => import('./MyComponent'),
|
27 | });
|
28 | ```
|
29 |
|
30 | This 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
|
37 | const 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 |
|
48 | MyComponent.usePreload();
|
49 | MyComponent.usePrefetch();
|
50 | MyComponent.useKeepFresh();
|
51 | ```
|
52 |
|
53 | By 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
|
56 | const 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
|
69 | const prefetch = MyComponent.usePrefetch();
|
70 | ```
|
71 |
|
72 | While 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 |
|
78 | If 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
|
81 | const 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 />;
|
91 | MyComponent.usePrefetch();
|
92 |
|
93 | // Much better!
|
94 | <MyComponent.Prefetch id="123" />;
|
95 | MyComponent.usePrefetch({id: '123'});
|
96 | ```
|
97 |
|
98 | This 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
|
101 | import {
|
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';
|
109 | import {createAsyncQueryComponent} from '@shopify/react-graphql';
|
110 |
|
111 | const MyQuery = createAsyncQueryComponent({
|
112 | load: () => import('./graphql/MyQuery.graphql'),
|
113 | });
|
114 |
|
115 | const 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 |
|
129 | By 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 |
|
131 | If 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
|
138 | import {createAsyncComponent, DeferTiming} from '@shopify/react-async';
|
139 |
|
140 | // No deferring
|
141 | const MyComponent = createAsyncComponent({
|
142 | load: () => import('./MyComponent'),
|
143 | });
|
144 |
|
145 | // Never load synchronously, always start load in mount
|
146 | const MyComponentOnMount = createAsyncComponent({
|
147 | load: () => import('./MyComponent'),
|
148 | defer: DeferTiming.Mount,
|
149 | });
|
150 |
|
151 | // Never load synchronously, always start load in requestIdleCallback
|
152 | const 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
|
159 | const MyComponentOnIdle = createAsyncComponent({
|
160 | load: () => import('./MyComponent'),
|
161 | defer: DeferTiming.InViewport,
|
162 | });
|
163 | ```
|
164 |
|
165 | You 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
|
168 | const MyModalComponent = createAsyncComponent({
|
169 | load: () => import('./MyModalComponent'),
|
170 | defer: ({open}) => open,
|
171 | });
|
172 | ```
|
173 |
|
174 | #### Progressive hydration
|
175 |
|
176 | It 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 |
|
180 | In 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
|
183 | const Expensive = createAsyncComponent({
|
184 | load: () => import('./Expensive'),
|
185 | deferHydration: DeferTiming.InViewport,
|
186 | });
|
187 | ```
|
188 |
|
189 | The 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 |
|
197 | These 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
|
200 | import {createAsyncComponent, usePreload} from '@shopify/react-async`
|
201 |
|
202 | const Expensive = createAsyncComponent({
|
203 | load: () => import('./Expensive'),
|
204 | deferHydration: DeferTiming.InViewport,
|
205 | });
|
206 |
|
207 | function MyComponent({children}) {
|
208 | const preload = usePreload(Expensive);
|
209 | return <div onMouseEnter={preload}>{children}</div>;
|
210 | }
|
211 | ```
|
212 |
|
213 | ### `useAsync`
|
214 |
|
215 | The `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 |
|
217 | This 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 |
|
224 | The `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 |
|
232 | The following example demonstrates how to use the `useAsync` hook to implement the default `usePreload` provided to async components:
|
233 |
|
234 | ```tsx
|
235 | import {createResolver} from '@shopify/async';
|
236 | import {useAsync, AssetTiming} from '@shopify/react-async';
|
237 |
|
238 | const resolver = createResolver({
|
239 | id: () => require.resolveWeak('./MyComponent'),
|
240 | load: () => import('./MyComponent'),
|
241 | });
|
242 |
|
243 | function usePreload() {
|
244 | return useAsync(resolver, {
|
245 | assetTiming: AssetTiming.NextPage,
|
246 | }).load;
|
247 | }
|
248 | ```
|
249 |
|
250 | ### `PrefetchRoute` and `Prefetcher`
|
251 |
|
252 | The `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 |
|
254 | Consider this async component:
|
255 |
|
256 | ```tsx
|
257 | const ProductDetails = createAsyncComponent({
|
258 | load: () => import('./ProductDetails'),
|
259 | usePrefetch: ({id}: {id: string}) => <PrefetchGraphQLQuery id={id} />,
|
260 | });
|
261 | ```
|
262 |
|
263 | This 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 |
|
275 | To 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 |
|
281 | And 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 |
|
287 | To 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 |
|
289 | To 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
|
292 | import {extract} from '@shopify/react-effect/server';
|
293 | import {AsyncAssetManager, AsyncAssetContext} from '@shopify/react-async';
|
294 |
|
295 | const asyncAssetmanager = new AsyncAssetManager();
|
296 |
|
297 | await extract(<App />, {
|
298 | decorate(app) {
|
299 | return (
|
300 | <AsyncAssetContext.Provider value={asyncAssetmanager}>
|
301 | {app}
|
302 | </AsyncAssetContext.Provider>
|
303 | );
|
304 | },
|
305 | });
|
306 |
|
307 | const assetSelectors = [...asyncAssetmanager.used(AssetTiming.Immediate)];
|
308 | ```
|
309 |
|
310 | These 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 |
|
314 | Other 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 |
|
316 | The `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 |
|
326 | Most 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
|
329 | const 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 |
|
346 | The 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.
|