1 | <div align="center">
|
2 | <h1>IMPORTED COMPONENT ✂</h1>
|
3 | <h2>Code splitting which always works<sup>*</sup></h2>
|
4 | <br/>
|
5 | <img src="./assets/imported-logo.png" alt="imported components" width="409" align="center">
|
6 | <br/>
|
7 | <br/>
|
8 | SSR-friendly code splitting compatible with any platform.
|
9 | <br/>
|
10 | Deliver a better experience within a single import.
|
11 | <br/>
|
12 | <br/>
|
13 |
|
14 | <a href="https://www.npmjs.com/package/react-imported-component">
|
15 | <img src="https://img.shields.io/npm/v/react-imported-component.svg?style=flat-square" />
|
16 | </a>
|
17 |
|
18 | <a href="https://travis-ci.org/github/theKashey/react-imported-component">
|
19 | <img src="https://travis-ci.org/theKashey/react-imported-component.svg" />
|
20 | </a>
|
21 |
|
22 | <a href="https://www.npmjs.com/package/react-imported-component">
|
23 | <img src="https://img.shields.io/npm/dm/react-imported-component.svg" alt="npm downloads">
|
24 | </a>
|
25 |
|
26 | <a href="https://bundlephobia.com/result?p=react-imported-component">
|
27 | <img src="https://img.shields.io/bundlephobia/minzip/react-imported-component.svg" alt="bundle size">
|
28 | </a>
|
29 |
|
30 | <img src="https://badges.greenkeeper.io/theKashey/react-imported-component.svg" />
|
31 |
|
32 | <br/>
|
33 | </div>
|
34 |
|
35 | > <sup>\*</sup> It's really will never let you down. All credits to your bundler.
|
36 |
|
37 | 👉 [Usage](#usage) | [API](#api) | [Setup](#setup) | [SSR](#ssr) | [CCS](#css) [Concurrent loading](#concurrent-loading) | [Webpack/Parcel](#bundler-integration)
|
38 |
|
39 | | Library | Suspense | SSR | Hooks | Library | Non-modules | import(`./${value}`) | babel-macro | webpack only |
|
40 | | ------------------- | :------: | :-: | :---: | :-----: | :---------: | :------------------: | :---------: | :----------: |
|
41 | | React.lazy | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | 😹 | no-ssr |
|
42 | | react-loadable | ✅ | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | 😿 |
|
43 | | @loadable/component | ✅ | ✅ | ❌ | ✅ | ❌ | ✅ | ❌ | 😿 |
|
44 | | imported-component | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | 😸 |
|
45 |
|
46 | > Read more about [what this table displays](#comparisonLegend)
|
47 |
|
48 | Key features:
|
49 |
|
50 | - 1️⃣ Single source of truth - your **bundler drives** everything
|
51 | - 📖 **library** level code **splitting**
|
52 | - 🧙️ Hybrid and **Prerendering** compatible
|
53 | - 💡 **TypeScript** bindings
|
54 | - ⚛️ **React.Lazy** underneath (if hot module updates are disabled)
|
55 | - 🌟 Async on client, sync on server. Supports **Suspense** (even on server side)
|
56 | - 📦 could work with **any bundler** - webpack, rollup, parcel or puppeteer - it does not matter
|
57 | - 🤹♂️ working as well with any `import` you may provide
|
58 |
|
59 | Other features:
|
60 |
|
61 | - 🔥 Hot-Module-Replacement/React-Hot-Loader friendly
|
62 | - ⛓️ support forwardRef
|
63 | - ⚛️ React 16/Async/Hooks ready
|
64 | - 🛠 HOC, Component, Hooks API
|
65 | - 🐳 stream rendering support
|
66 | - 👥 partial hydration out of the box
|
67 | - 📦 and yes - this is the only **parcel-bundler compatible** SSR-friendly React code splitting library, as well as a perfect solution for **Create React App**
|
68 |
|
69 | 👍 Better than [React.Lazy](https://reactjs.org/docs/code-splitting.html#reactlazy):
|
70 |
|
71 | - It **IS** Lazy, just with some stuff around<sup>\*</sup>
|
72 | - SSR, Prerendering and Preloading support
|
73 | - With or without Suspense, and easier Error cases support
|
74 |
|
75 | 👍 Better than others:
|
76 |
|
77 | - Not bound to webpack
|
78 | - Easy way to use per-browser(modern/legacy) bundles - you down have to mess with actual browser support
|
79 | - Strong typing
|
80 | - Working with any imports, even native, external or derived ones.
|
81 |
|
82 | 👌 Client-side module resolution
|
83 |
|
84 | - Loads chunks only after the `main one`, as long as loader code is bundled inside the main chunk, so it should be loaded first.
|
85 | - Not an issue with the `progressive hydration`, and might provide a better UX via feature detection.
|
86 | - Provides 👨🔬 technological workaround - [see here](#concurrent-loading)
|
87 |
|
88 | 📦 Optional bundler integration for the best experience
|
89 |
|
90 | - prefetching backed by webpack `stat.json` and `asset.json`
|
91 | - `parcel-manifest.json` support
|
92 |
|
93 | 👯♀️Works better in pair
|
94 |
|
95 | - [react-prerendered-component](https://github.com/theKashey/react-prerendered-component) for prerendering, partial hydration and react fragment caching
|
96 | - [used-style](https://github.com/theKashey/used-styles) for CSS and critical CSS extraction
|
97 | - [devolution](https://github.com/theKashey/devolution) for shipping legacy/modern bundles
|
98 | - [webpack-imported](https://github.com/theKashey/webpack-imported) for deep `webpack` integration, the only bundler dependent tool in this list.
|
99 |
|
100 | <a name="usage"/>
|
101 |
|
102 | # Usage
|
103 |
|
104 | ## Server side
|
105 |
|
106 | Just a proper setup and a bit of magic
|
107 |
|
108 | ## Client side
|
109 |
|
110 | ### Component
|
111 |
|
112 | `imported` provides 2 common ways to define a component, which are more different inside than outside
|
113 |
|
114 | - using `pre-lazy` API.
|
115 |
|
116 | ```javascript
|
117 | import importedComponent from 'react-imported-component';
|
118 | const Component = importedComponent( () => import('./Component'));
|
119 |
|
120 | const Component = importedComponent( () => import('./Component'), {
|
121 | LoadingComponent: Spinner, // what to display during the loading
|
122 | ErrorComponent: FatalError // what to display in case of error
|
123 | });
|
124 |
|
125 | Component.preload(); // force preload
|
126 |
|
127 | // render it
|
128 | <Component... />
|
129 | ```
|
130 |
|
131 | - using `lazy` API. It's almost the same `React.lazy` outside, and exactly the same inside.
|
132 |
|
133 | ```js
|
134 | import { lazy, LazyBoundary } from 'react-imported-component';
|
135 | const Component = lazy(() => import('./Component'));
|
136 |
|
137 | const ClientSideOnly = () => (
|
138 | <Suspense>
|
139 | <Component />
|
140 | </Suspense>
|
141 | );
|
142 |
|
143 | // or let's make it SSR friendly
|
144 | const ServerSideFriendly = () => (
|
145 | <LazyBoundary>
|
146 | {' '}
|
147 | // LazyBoundary is Suspense* on the client, and "nothing" on the server
|
148 | <Component />
|
149 | </LazyBoundary>
|
150 | );
|
151 | ```
|
152 |
|
153 | `LazyBoundary` is a `Suspense` on Client Side, and `React.Fragment` on Server Side. Don't forget - "dynamic" imports are sync on a server.
|
154 |
|
155 | Example: [React.lazy vs Imported-component](https://codesandbox.io/s/wkl95r0qw8)
|
156 |
|
157 | ### Hook
|
158 |
|
159 | However, you may not load only components - you may load anything
|
160 |
|
161 | ```js
|
162 | import {useImported} from 'react-imported-component'
|
163 |
|
164 | const MyCalendarComponent = () => {
|
165 | const {
|
166 | imported: moment,
|
167 | loading
|
168 | } = useImported(() => import("moment"));
|
169 |
|
170 | return loading ? "..." : <span>today is {moment(Date.now).format()}</span>
|
171 | }
|
172 |
|
173 | // or we could make it a bit more interesting...
|
174 |
|
175 | const MyCalendarComponent = () => {
|
176 | const {
|
177 | imported: format = x => "---", // default value is used while importing library
|
178 | } = useImported(
|
179 | () => import("moment"),
|
180 | moment => x => moment(x).format // masking everything behind
|
181 | );
|
182 |
|
183 | return <span>today is {format(Date.now())</span>
|
184 | }
|
185 | ```
|
186 |
|
187 | What you could load using `useImported`? Everything - `imported` itself is using it to import components.
|
188 |
|
189 | > `useImported` is an excellent example for loading translations, which are usually a simple json, in a _trackable_ way.
|
190 |
|
191 | > 💡 did you know that there is another hook based solution to load _"something might might need"_? The [use-sidecar](https://github.com/theKashey/use-sidecar) pattern.
|
192 |
|
193 | 🤔 Keep in mind - **everything here is using `useImported`**, and **you can build whatever you need** using just it.
|
194 |
|
195 | ### Module
|
196 |
|
197 | A slim helper to help handle `modules`, you might require using `useImported` in a component way
|
198 |
|
199 | ```js
|
200 | import { importedModule, ImportedModule } from 'react-imported-component';
|
201 |
|
202 | const Moment = importedModule(() => import('moment'));
|
203 |
|
204 | <Moment fallback="long time ago">
|
205 | {(momentjs /* default imports are auto-imported*/) => momentjs(date).fromNow()}
|
206 | </Moment>;
|
207 | ```
|
208 |
|
209 | > Yes, this example was taken from [loadable.lib](https://www.smooth-code.com/open-source/loadable-components/docs/library-splitting/)
|
210 |
|
211 | Can I also use a ref, populated when the library is loaded? No, you cant. Use `useImported` for any special case like this.
|
212 |
|
213 | Plus, there is a Component helper:
|
214 |
|
215 | ```js
|
216 | <ImportedModule
|
217 | // import is just a _trackable_ promise - do what ever you want
|
218 | import={() => import('moment').then(({momentDefault})=> momentDefault(date).fromNow()}
|
219 | fallback="long time ago"
|
220 | >
|
221 | {(fromNow) => fromNow()}
|
222 | </ImportedModule>
|
223 | ```
|
224 |
|
225 | > ImportedModule will throw a promise to the nearest Suspense boundary if no `fallback` provided.
|
226 |
|
227 | ## Babel macro
|
228 |
|
229 | If you could not use babel plugin, but do have `babel-plugin-macro` (like CRA) - consider using macro API:
|
230 |
|
231 | ```js
|
232 | import { imported, lazy, useImported } from 'react-imported-component/macro';
|
233 | // notice - there is no default import here
|
234 | ```
|
235 |
|
236 | ### Indirect usage
|
237 |
|
238 | Just importing `react-imported-component/macro` would enable babel transformation for the current file.
|
239 | If you have `imported` definition in one file, and use it from another - just `import "react-imported-component/macro"` in that another file. See [#142](https://github.com/theKashey/react-imported-component/issues/142)
|
240 |
|
241 | <a name="api"/>
|
242 |
|
243 | # API
|
244 |
|
245 | > Don't forget - there are TS typings provided.
|
246 |
|
247 | ### Code splitting components
|
248 |
|
249 | > import {\*} from 'react-imported-component';
|
250 |
|
251 | ##### importedComponent
|
252 |
|
253 | - `importedComponent(importFunction, [options]): ComponentLoader` - main API, default export, HOC to create imported component.
|
254 |
|
255 | - `importFunction` - function which resolves with Component to be imported.
|
256 | - `options` - optional settings
|
257 | - `options.async` - activates react suspense support. Will throw a Promise in a Loading State - use it with Suspense in a same way you use **React.lazy**. See [working with Suspense](working-with-suspense)
|
258 | - `options.LoadingComponent` - component to be shown in Loading state
|
259 | - `options.ErrorComponent` - component to be shown in Error state. Will re-throw error if ErrorComponent is not set. Use ErrorBoundary to catch it.
|
260 | - `options.onError` - function to consume the error, if one will thrown. Will rethrow a real error if not set.
|
261 | - `options.exportPicker` - function to pick `not default` export from a `importFunction`
|
262 | - `options.render(Component, state, props)`- function to render the result. Could be used to tune the rendering.
|
263 |
|
264 | - [static] `.preload` - static method to preload components.
|
265 |
|
266 | ##### lazy
|
267 |
|
268 | - `lazy(importFunction)` - helper to mimic **React.lazy** behavior
|
269 |
|
270 | ##### useImported
|
271 |
|
272 | - `useImported(importFunction, [exportPicker], [options])` - code splitting hook
|
273 |
|
274 | - `importFunction` - a function which resolves to `default` or `wildcard` import(T | {default:T})
|
275 | - `[exportPicker]` - function to pick "T" from the import
|
276 | - `[options]` - options to the hook
|
277 |
|
278 | - `[options.import]` - controls import. Hooks would be executed only if this is not false
|
279 | - `[options.track]` - ability to disable server-side usage tracking.
|
280 |
|
281 | `useImported` returns complex object(ImportedShape):
|
282 |
|
283 | - `imported` - the imported resource
|
284 | - `error` - error (if present)
|
285 | - `loading` - is it loading right now?
|
286 | - `loadable` - the underlying `Loadable` object
|
287 | - `retry` - retry action (in case of error)
|
288 |
|
289 | Hints:
|
290 |
|
291 | - use `options.import=false` to perform conditional import - `importFunction` would not be used if this option set to `false.
|
292 | - use `options.track=true` to perform SSR only import - to usage would be tracked if this option set to `false.
|
293 |
|
294 | ##### ImportedController
|
295 |
|
296 | - `<ImportedController>` - a controller for Suspense Hydration. **Compulsory** for async/lazy usecases
|
297 |
|
298 | ##### Misc
|
299 |
|
300 | There is also API method, unique for imported-component, which could be useful on the client side
|
301 |
|
302 | - `addPreloader(fn):fn` - adds a function, result of which would be _awaited_ when any component is loaded. Returns cancel method.
|
303 |
|
304 | ### Server side API
|
305 |
|
306 | > import {\*} from 'react-imported-component/server';
|
307 |
|
308 | - `whenComponentsReady():Promise` - will be resolved, when all components are loaded. Usually on the next "Promise" tick.
|
309 | - `drainHydrateMarks([stream])` - returns the currently used marks, and clears the list.
|
310 | - `printDrainHydrateMarks([stream])` - print our the `drainHydrateMarks`.
|
311 |
|
312 | #### Stream API
|
313 |
|
314 | - `createLoadableStream` - creates a steam
|
315 | - `ImportedStream` - wraps another component with import usage tracker.
|
316 | - `createLoadableTransformer` - creates nodejs StreamTransformer
|
317 | - `getLoadableTrackerCallback` - helper factory for the stream transformer
|
318 |
|
319 | ### Client side API
|
320 |
|
321 | > import {\*} from 'react-imported-component/boot';
|
322 |
|
323 | - `whenComponentsReady():Promise`, will be resolved, when all (loading right now) marks are loaded.
|
324 | - `rehydrateMarks([marks]):Promise`, loads _marked_ async chunks.
|
325 | - `injectLoadableTracker` - helper factory for the stream transformer
|
326 |
|
327 | ### Types
|
328 |
|
329 | #### Loadable
|
330 |
|
331 | All imports inside library are converted into `Loadable` object, and it's often accessible from outside via
|
332 | `useImported().loadable`, `useLoadable`(not documented), `getLoadable`(not documented). Even if it's documented from TS point of view -
|
333 | let's keep all fields in a secret, except one:
|
334 |
|
335 | - `resolution` - promise reflecting resolution of this loadable object
|
336 |
|
337 | <a name="setup"/>
|
338 |
|
339 | # Setup
|
340 |
|
341 | ## In short
|
342 |
|
343 | 1. Add `babel` plugin
|
344 | 2. Run `yarn imported-components src src/imported.js` to extract all your imports into a `run time chunk` (aka async-requires).
|
345 | 3. Replace `React.lazy` with our `lazy`, and `React.Suspense` with our `LazyBoundary`. Literraly [monkey-patch React to do so](#monkey-patch)
|
346 | 4. Add `printDrainHydrateMarks` to the server code.
|
347 | 5. Add `rehydrateMarks` to the client code
|
348 | 6. Done. Just read the rest of readme for details.
|
349 |
|
350 | There are examples for webpack, parcel, and react-snap. Just follow them.
|
351 |
|
352 | ## 1. Configure babel plugin
|
353 |
|
354 | **On the server**:
|
355 |
|
356 | ```json
|
357 | {
|
358 | "plugins": ["react-imported-component/babel", "babel-plugin-dynamic-import-node" /* might be optional for babel 7*/]
|
359 | }
|
360 | ```
|
361 |
|
362 | **On the client**:
|
363 |
|
364 | ```json
|
365 | {
|
366 | "plugins": ["react-imported-component/babel"]
|
367 | }
|
368 | ```
|
369 |
|
370 | Imported-Component will hook into dynamic imports, providing extra information about files you want to load.
|
371 |
|
372 | ## 2. Add one more command into package.json
|
373 |
|
374 | CLI command `imported-components [sources ROOT] [targetFile.js]` (use .ts for TypeScript)
|
375 |
|
376 | ```js
|
377 | "generate-imported-component": "imported-components src src/imported.js"
|
378 | ```
|
379 |
|
380 | When you will execute this command - all `imports` among your codebase would be found and extracted to a file provided.
|
381 | This will gave ability to orchestrate code-splitting later.
|
382 |
|
383 | If you need to search inside more that one top-level directory - just define more command, saving information into more than one target file.
|
384 |
|
385 | > The current implementation will discover and use all `imports`, even // commented ones
|
386 |
|
387 | > 💡 Feel free to **.gitignore** these autogenerated files
|
388 |
|
389 | ## 3. Start using `imported`, `lazy` or `useImported`
|
390 |
|
391 | Without you using API provided nothing would work.
|
392 |
|
393 | ## 4. Add server side tracking
|
394 |
|
395 | There are two ways to do it - in a single threaded way, and async
|
396 |
|
397 | #### Single threaded
|
398 |
|
399 | ```js
|
400 | import { printDrainHydrateMarks, drainHydrateMarks } from 'react-imported-component';
|
401 | // this action will "drain" all currently used(by any reason) marks
|
402 | // AND print a script tag
|
403 | const html = renderToString(<YourApp />) + printDrainHydrateMarks();
|
404 |
|
405 | // OR return list of usedmarks, and yet again CLEAR the marks list.
|
406 | const html = renderToString(<YourApp />) + '<script>const marks=' + JSON.stringify(drainHydrateMarks()) + '</script>';
|
407 | ```
|
408 |
|
409 | #### renderToStream or async render
|
410 |
|
411 | ```js
|
412 | import {createLoadableStream} from 'react-imported-component/server';
|
413 |
|
414 | let importedStream = createLoadableStream();
|
415 | // ImportedStream is a async rendering "provider"
|
416 | const stream = renderToStream(
|
417 | <ImportedStream stream={importedStream}>
|
418 | <YourApp />
|
419 | </ImportedStream>
|
420 | );
|
421 |
|
422 | // you'd then pipe the stream into the response object until it's done
|
423 | stream.pipe(res, { end: false });
|
424 |
|
425 | // and finalize the response with closing HTML
|
426 | stream.on('end', () =>
|
427 | // print marks used in the file
|
428 | res.end(`${printDrainHydrateMarks(importedStream)}</body></html>`),
|
429 | )
|
430 | ```
|
431 |
|
432 | However, the idea is just to use `streams` to separate renders
|
433 |
|
434 | ```js
|
435 | const html =
|
436 | renderToString(
|
437 | <ImportedStream stream={importedStream}>
|
438 | <YourApp />
|
439 | </ImportedStream>
|
440 | ) + printDrainHydrateMarks(importedStream);
|
441 | ```
|
442 |
|
443 | ## 5. Add `rehydrateMarks` to the client code
|
444 |
|
445 | Before rendering your application you have to ensure - all parts are loaded.
|
446 | `rehydrateMarks` will load everything you need, and provide a promise to await.
|
447 |
|
448 | ```js
|
449 | import { rehydrateMarks, ImportedController } from 'react-imported-component';
|
450 |
|
451 | // this will trigger all marked imports, and await for competition.
|
452 | rehydrateMarks().then(() => {
|
453 | // better (note ImportedController usage)
|
454 | ReactDOM.hydrate(
|
455 | <ImportedController>
|
456 | <App />
|
457 | </ImportedController>,
|
458 | document.getElementById('main')
|
459 | );
|
460 | // or
|
461 | ReactDOM.render(<App />, document.getElementById('main'));
|
462 | });
|
463 | ```
|
464 |
|
465 | `rehydrateMarks` accepts a list of `marks` from a server side(`drainHydrateMarks`), loads all
|
466 | necessary chunks and then resolves.
|
467 |
|
468 | <a name="concurrent-loading"/>
|
469 |
|
470 | ## A VERY IMPORTANT MOMENT - Concurrent Loading
|
471 |
|
472 | All other code splitting libraries are working a bit differently - they amend `webpack` building process,
|
473 | gathering information about how the final chunks are assembled, and **injects the real scripts and styles** to the server response,
|
474 | thus all scripts, used to render something on the Server would be loaded in a parallel in on Client.
|
475 | Literally - they are defined in the HTML.
|
476 | `React-imported-component` is different, it starts "working" when the bundle is loaded, thus
|
477 | **the loading of chunks is deferred**.
|
478 |
|
479 | > 💡 In the normals conditions `react-imported-component` would be "slower" than a "webpack" library. Refer to bundle integration section.
|
480 |
|
481 | However, it is not a problem, as long as (for now), script execution is single threaded, and even you if can **load** multiple scripts
|
482 | simultaneously - you can't **run** them in parallel\*.
|
483 |
|
484 | And there a way to utilize this limitation - just change your entry point, .
|
485 |
|
486 | And let's call it - a **Scheduler optimization**. See **loading prediction** section for more details.
|
487 |
|
488 | #### Scheduler optimization + simply static render
|
489 |
|
490 | 1. Split your app into `boot` and `main` parts
|
491 | 2. `rehydrate` at the boot
|
492 |
|
493 | ```js
|
494 | // index.js (boot)
|
495 | import './src/imported'; // the file generated by "generate-imported-component" (.2)
|
496 | import { rehydrateMarks } from 'react-imported-component/boot';
|
497 |
|
498 | rehydrateMarks(); // just start loading what's needed
|
499 |
|
500 | // load/execute the rest after letting the browser kick off chunk loading
|
501 | // for example wrapping it in two Promises (1ms timeout or setImmediate)
|
502 | Promise.resolve().then(() =>
|
503 | Promise.resolve().then(() => {
|
504 | // load the rest
|
505 | require('./main'); // <--- your main scripts
|
506 | // ! and don't forget to `await rehydrateMarks()` before render
|
507 | })
|
508 | );
|
509 |
|
510 | // main.js
|
511 | rehydrateMarks().then(() => {
|
512 | ReactDOM.hydrate(<App />, document.getElementById('root'));
|
513 | });
|
514 | ```
|
515 |
|
516 | This will just start loading extra chunks before the main bundle got completely parsed and executed.
|
517 |
|
518 | #### Defer till DOMReady
|
519 |
|
520 | > 💡 Another perfect option would be to wait till DomReady event.
|
521 |
|
522 | ```js
|
523 | // index.js (boot)
|
524 | import './src/imported'; // the file generated by "generate-imported-component" (.2)
|
525 | import { rehydrateMarks } from 'react-imported-component/boot';
|
526 |
|
527 | rehydrateMarks(); // just start loading what's needed
|
528 |
|
529 | const startApp = () => require('./main'); // <--- your main scripts
|
530 |
|
531 | // it's "not safe" to start you application before DOM is "ready"
|
532 | if (document.readyState === 'loading') {
|
533 | document.addEventListener('DOMContentLoaded', startApp);
|
534 | } else {
|
535 | startApp();
|
536 | }
|
537 | ```
|
538 |
|
539 | #### Scheduler optimization + stream render
|
540 |
|
541 | > See examples/SSR/parcel-react-ssr/server-stream for details
|
542 |
|
543 | 1. Add your main bundle to the `head`, using **async** script tag. Not defer! We have to do it async
|
544 | 2. Add `loadableTracker` at server side
|
545 |
|
546 | ```js
|
547 | import {createLoadableTransformer, getLoadableTrackerCallback} from 'react-imported-component/server';
|
548 | const importedTracker = createLoadableTransformer(
|
549 | loadableStream, // stream to observe
|
550 | getLoadableTrackerCallback() // helper factory to create global tracker.
|
551 | );
|
552 |
|
553 | // pipe result of `renderToStream` throught it
|
554 | const reactRenderStream = ReactDOM.renderToNodeStream(...).pipe(importedTracker);
|
555 | ```
|
556 |
|
557 | 3. Add `loadableTracker` at client side
|
558 |
|
559 | ```js
|
560 | // index.js
|
561 | import './src/imported'; // the file generated by "generate-imported-component" (.2)
|
562 | import { injectLoadableTracker } from 'react-imported-component/boot';
|
563 |
|
564 | injectLoadableTracker();
|
565 |
|
566 | // load the rest after letting the browser kick off chunk loading
|
567 | // for example wrapping it in two Promises (1ms timeout or setImmediate)
|
568 | Promise.resolve().then(() =>
|
569 | Promise.resolve().then(() => {
|
570 | require('./main');
|
571 | })
|
572 | );
|
573 | ```
|
574 |
|
575 | This "hack", will first introduce all possible `imports` to the `imported-component`, then gave it a "tick"
|
576 | to start loading required once, and only then execute the rest of the bundle.
|
577 | While the rest(99%) of the bundle would make CPU busy - chunks would be loaded over the network.
|
578 |
|
579 | > 💡This is utilizing the differences between `parse`(unenviable) phase of script, and execute(more expensive) one.
|
580 |
|
581 | # Cooking receipts
|
582 |
|
583 | <a name="monkey-patch"/>
|
584 |
|
585 | ## Replace React.lazy by lazy
|
586 |
|
587 | ```js
|
588 | import React from 'react';
|
589 | import { lazy, LazyBoundary } from 'react-imported-component';
|
590 | React.lazy = lazy;
|
591 | React.Suspense = LazyBoundary;
|
592 | ```
|
593 |
|
594 | That's all the work required to get it working.
|
595 |
|
596 | ## Partial hydration
|
597 |
|
598 | Just wrap "partial hydrated" component with _another_ `ImportedStream` so everything needed for it would be not automatically loaded with the main stream.
|
599 |
|
600 | ## Hybrid render (CSR with prerendering)
|
601 |
|
602 | This library could support hybrid rendering (aka pre-rendering) compatible in two cases:
|
603 |
|
604 | - pre-render supports `state hydration`, like `getState` in [react-snap](https://github.com/stereobooster/react-snap). See our [example](https://github.com/theKashey/react-imported-component/tree/master/examples/hybrid/react-snap).
|
605 | - for [rendertron](https://github.com/GoogleChrome/rendertron) or [https://prerender.io](https://prerender.io) follow `react-snap` example, just dump `state` using `setTimeout`.
|
606 | - You may use `react-prerendered-component` to maintain a component state until async chunk is not loaded. See example below.
|
607 |
|
608 | ### Works better in pair (boiled-place-less code splitting)
|
609 |
|
610 | You might not need to wait for all the chunks to be loaded before you can render you app -
|
611 | just use [react-prerendered-component](https://github.com/theKashey/react-prerendered-component).
|
612 |
|
613 | ```js
|
614 | import imported from 'react-imported-component';
|
615 | import { PrerenderedComponent } from 'react-prerendered-component';
|
616 |
|
617 | const AsyncComponent = imported(() => import('./myComponent.js'));
|
618 |
|
619 | <PrerenderedComponent
|
620 | // component will "go live" when chunk loading would be done
|
621 | live={AsyncComponent.preload()}
|
622 | >
|
623 | // until component is not "live" prerendered HTML code would be used // that's why you need to `preload`
|
624 | <AsyncComponent />
|
625 | </PrerenderedComponent>;
|
626 | ```
|
627 |
|
628 | `React-prerendered-component` is another way to work with code splitting, which makes everything far better.
|
629 |
|
630 | ## Timeout to display "spinners"
|
631 |
|
632 | There is no build in timeouts to display Error or Loading states. You could control everything by yourself
|
633 |
|
634 | - use react-delay, p-delay, p-timeout, or `Suspense` :P.
|
635 |
|
636 | ## Component loader
|
637 |
|
638 | You may use component api if you need it by any reason.
|
639 |
|
640 | ```js
|
641 | import { ComponentLoader } from 'react-imported-component';
|
642 |
|
643 | const MyPage = () => (
|
644 | <ComponentLoader
|
645 | loadable={() => import('./Page.js')}
|
646 | // all fields are optional, and matches the same field of importedComponent.
|
647 | LoadingComponent={Loading}
|
648 | ErrorComponent={Error}
|
649 | onError
|
650 | exportPicker
|
651 | render
|
652 | async
|
653 | />
|
654 | );
|
655 | ```
|
656 |
|
657 | <a name="css"/>
|
658 |
|
659 | ## CSS Support
|
660 |
|
661 | ### CSS-in-JS Support
|
662 |
|
663 | Out-of-the-box. Literally. CSS-in-JS library, like `styled-component` will do it by themselves, and there is nothing
|
664 | to be managed by this library.
|
665 |
|
666 | ### Static CSS Files Support
|
667 |
|
668 | > `imported` could knew only about JS you've used, not the CSS that js've used...
|
669 |
|
670 | This library **does not** support CSS as CSS, as long it's bundler independent and such deep integration is not possible.
|
671 | Even if [deep bundler integration](#bundler) is next following this section - the recomended solution is to skip `css` part of it, and use
|
672 | a more _bundler independent_ way to support CSS:
|
673 |
|
674 | 1. Configure you bundler, and server side rendering to emit the right `classNames` (just remove `style-loader` from webpack configuration)
|
675 | 2. Use `used-styles` to inject used **css files** to the resulting HTML.
|
676 |
|
677 | In short (streamed example is NOT short)
|
678 |
|
679 | ```js
|
680 | const lookup = discoverProjectStyles('./dist');
|
681 | // ....
|
682 | const markup = ReactDOM.renderToString(<App />);
|
683 | const usedStylesAsCSSFiles = getUsedStyles(markup, lookup);
|
684 | // generate `link` (better rel='preload') to load CSS files
|
685 |
|
686 | const usedStylesAsStyles = getCriticalStyles(markup, lookup);
|
687 | // extract critical CSS and inline in to the server response
|
688 | ```
|
689 |
|
690 | If you need _stream render_ example with **reduced TTFB** -
|
691 | please refer to [used-styles](https://github.com/theKashey/used-styles) documentation, or our [parcel-bundler stream server example](https://github.com/theKashey/react-imported-component/tree/master/examples/SSR/parcel-react-ssr/stream-server).
|
692 |
|
693 | #### Critical style extraction and Preloader
|
694 |
|
695 | With the critical style extracting you might not need the "real" and "full" styles on the page load -
|
696 | only `js` is really required for hydration.
|
697 | Thus - you might skip loading styles for the initial render, however still need them for the next render.
|
698 | However - your bundler might think that you have loaded them, this is not true.
|
699 | To handle this case you need `addPreloader` method
|
700 |
|
701 | ```js
|
702 | // load initial bundle
|
703 | // await dom ready
|
704 | // -> HYDRATE APP <-
|
705 | // add preloader
|
706 |
|
707 | import { addPreloader } from 'react-imported-component/boot';
|
708 |
|
709 | addPreloader(() => {
|
710 | // check that styles are loaded
|
711 | // or return Promise if they are not
|
712 | });
|
713 |
|
714 | // any not yet loaded ImportedComponent
|
715 | // will await for it's own `import` as well as for
|
716 | // promise returned from preloaders
|
717 | ```
|
718 |
|
719 | See critical css example for details
|
720 |
|
721 | #### Create React App
|
722 |
|
723 | Use `react-imported-component/macro` for CRA compatible SSR code splitting without ejecting.
|
724 |
|
725 | > It is safe to always use `react-imported-component/macro` without `babel-plugin-macros`, BUT with `react-hot-loader/babel` enabled, as long as it will remove all `macros`.
|
726 |
|
727 | <a name="bundler-integration"/>
|
728 |
|
729 | ## Bundler integration
|
730 |
|
731 | Keep in mind - you dont "need" this. It will just make integration slightly better in terms of prefetching (which affects network recourse priority) and thus startup time.
|
732 |
|
733 | ### Webpack integration
|
734 |
|
735 | You might preload/prefetch used styles and scripts, which were defined with `webpackChunkName`
|
736 |
|
737 | ```js
|
738 | // get mark somehow (drainHydrateMarks(importedStream))
|
739 | import { getMarkedChunks } from 'react-imported-component/server';
|
740 |
|
741 | const chunkNames = getMarkedChunks(marks);
|
742 | ```
|
743 |
|
744 | #### Via webpack-imported
|
745 |
|
746 | [webpack-imported](https://github.com/theKashey/webpack-imported) - provides `webpack` plugin to get data from the build, as well as to use it.
|
747 | Supports prefetching and critical style _guidance_.
|
748 |
|
749 | ```js
|
750 | import { WebpackImport } from 'webpack-imported/react';
|
751 | import importedStat from 'build/imported.json';
|
752 |
|
753 | <WebpackImport stats={importedStat} chunks={getMarkedChunks(drainHydrateMarks())} />;
|
754 | ```
|
755 |
|
756 | #### Via stat.json
|
757 |
|
758 | If you do have only `stat.json` - it could be converted into "importedStat" by `webpack-imported` without adding plugin.
|
759 |
|
760 | Alternatively - you can discover **all** resources you have to preload
|
761 | using [flush-webpack-chunks](https://github.com/faceyspacey/webpack-flush-chunks)
|
762 |
|
763 | ```js
|
764 | const { js, styles } = flushChunks(webpackStats, {
|
765 | chunkNames,
|
766 | });
|
767 |
|
768 | const prefetch = (targets, as) => targets.map(url => `<link as="${as}" rel="preload" href="${url}" />`).join('');
|
769 |
|
770 | res.send(prefetch(scripts, 'script'));
|
771 | res.send(prefetch(stylesheets, 'style'));
|
772 | ```
|
773 |
|
774 | **DO NOT** actually **load** reported resources - only preload them, and let webpack do rest.
|
775 |
|
776 | #### Via assets.json
|
777 |
|
778 | You you are using [assets-webpack-plugin](https://github.com/ztoben/assets-webpack-plugin) then you have only list of assets, without dependencies between.
|
779 | That's enought.
|
780 |
|
781 | ```js
|
782 | const prefetchChunks = (chunks, assets) =>
|
783 | chunks
|
784 | .map(chunk =>
|
785 | [
|
786 | assets[chunk].js && `<link rel="preload" as="script" href="${assets[chunk].js}" />`,
|
787 | assets[chunk].css && `<link rel="preload" as="style" href="${assets[chunk].css}" />`,
|
788 | ].join('')
|
789 | )
|
790 | .join('');
|
791 |
|
792 | res.send(prefetchChunks(chunkNames, assets));
|
793 | ```
|
794 |
|
795 | #### Preloading
|
796 |
|
797 | Proper preloading is available **only** with bundler integration. Calling `component.preload` will not
|
798 | only `preload` it, but will `execute` as well.
|
799 |
|
800 | > `.preload` is equal do loading script as usual.
|
801 |
|
802 | If you seek the best experience you might want to prefetch or preload only (network only), not load (network and CPU).
|
803 |
|
804 | In order to prefetch(or preload) scripts - use `webpack-imported`, as seen in examples above.
|
805 |
|
806 | You need SSR step in order to use preloading!
|
807 |
|
808 | ##### Step 1 - expose known chunks
|
809 |
|
810 | In order to prefetch or preload you should know scripts locations. This information is stored inside webpack, and you cannot access it from user code.
|
811 | As a result we have to duplicate this information:
|
812 |
|
813 | ```js
|
814 | import { importAssets } from 'webpack-imported';
|
815 | // react-imported-component data file
|
816 | import applicationImports from '../async-requires';
|
817 | // webpack-imported data file
|
818 | import importedChunks from '../build/imported.json';
|
819 | // 👆 this file is generated during the build, you cannot use it "IN" the build
|
820 | // that's why this is Server Side only component
|
821 |
|
822 | export const ScriptLocations = () => (
|
823 | <script
|
824 | dangerouslySetInnerHTML={{
|
825 | __html: `
|
826 | window.KNOWN_SCRIPT_MARKS = ${JSON.stringify(
|
827 | applicationImports.reduce((acc, [, chunkName]) => {
|
828 | if (chunkName) {
|
829 | acc[chunkName] = importAssets(importedChunks, chunkName).raw.load;
|
830 | }
|
831 | return acc;
|
832 | }, {})
|
833 | )};
|
834 | `,
|
835 | }}
|
836 | />
|
837 | );
|
838 | ```
|
839 |
|
840 | Then add `<ScriptLocations/>` somewhere in your SSR-ed `head`.
|
841 |
|
842 | ##### Step 2 - preload
|
843 |
|
844 | Then you will be able to `prefetch(() => import('./home')`.
|
845 |
|
846 | ```js
|
847 | import { getMarkedChunks, loadableResource } from 'react-imported-component';
|
848 |
|
849 | // this function is partially duplicating webpack internals
|
850 | export const prefetch = importCallback => {
|
851 | // get a "lodable" attached for a given import
|
852 | const loadable = loadableResource(importCallback);
|
853 | if (typeof document !== 'undefined') {
|
854 | const prefix = __webpack_public_path__;
|
855 | if (loadable) {
|
856 | // get chunks used in loadable
|
857 | const chunks = getMarkedChunks(loadable.mark);
|
858 | chunks.forEach(chunk => {
|
859 | // note the usage of KNOWN_SCRIPT_MARKS
|
860 | const { js = [], css = [] } = window.KNOWN_SCRIPT_MARKS[chunk];
|
861 | js.forEach(script => {
|
862 | const link = document.createElement('link');
|
863 |
|
864 | link.rel = 'prefetch';
|
865 | link.as = 'script';
|
866 |
|
867 | link.href = prefix + script;
|
868 | document.head.appendChild(link);
|
869 | });
|
870 | css.forEach(style => {
|
871 | const link = document.createElement('link');
|
872 |
|
873 | link.rel = 'prefetch';
|
874 | link.as = 'style';
|
875 |
|
876 | link.href = prefix + style;
|
877 | document.head.appendChild(link);
|
878 | });
|
879 | });
|
880 | }
|
881 | }
|
882 | };
|
883 | ```
|
884 |
|
885 | ### Parcel integration
|
886 |
|
887 | Use `parcel-manifest` and `getMarkedFileNames`(instead of `getMarkedChunks`) to find which files were required and has to be imported.
|
888 | Keep in mind - the base path would be different and you have to `resolve` the _right_ files in to the _right_ files (`/app/index` to `index.js`).
|
889 |
|
890 | ### CRA integration
|
891 |
|
892 | Consider using [concurrent-loading](#concurrent-loading) technique only.
|
893 |
|
894 | ## React-snap
|
895 |
|
896 | `react-imported-component` is compatible with [react-snap](https://github.com/stereobooster/react-snap) out of the box.
|
897 | Works even better with `react-prerendered-component`.
|
898 |
|
899 | ## Webpack-external-import (or native import)
|
900 |
|
901 | `react-imported-component` is compatible with [webpack-external-import](https://github.com/ScriptedAlchemy/webpack-external-import),
|
902 | as long as it executes `imports`, thus executes custom logic
|
903 |
|
904 | ## `useImported` and `Suspense`
|
905 |
|
906 | `useImported` is not supposed to be used with Suspense (by design), while it could
|
907 |
|
908 | ```js
|
909 | const MyComponent = () => {
|
910 | const { loading, error, loadable, imported } = useImported(() => import('...'));
|
911 |
|
912 | if (loading) throw loadable.resolution; // throw to the nearest Suspense boundary
|
913 | if (error) throw error; // throw to the nearest Error boundary
|
914 |
|
915 | // do something with `imported` value
|
916 | };
|
917 | ```
|
918 |
|
919 | ## SSR (Server side rendering)
|
920 |
|
921 | It was usually a headache - async components and SSR, which is currently sync.
|
922 | React-imported-component break this cycle, making ServerSide rendering sync, and providing
|
923 | comprehensive ways to rehydrate rendered tree on client.
|
924 | It will detect server-side environment and precache all used components.
|
925 |
|
926 | ### Server Side Auto Import
|
927 |
|
928 | On the server side `imported` **would auto-import** any found import. As long as not all imports could be executed at Server Side you might
|
929 | pop out this feature using a "magic comment".
|
930 |
|
931 | ```js
|
932 | import(/* client-side */ './file');
|
933 | ```
|
934 |
|
935 | Or file filtering
|
936 |
|
937 | ```js
|
938 | import { setConfiguration } from 'react-imported-component';
|
939 |
|
940 | setConfiguration({
|
941 | fileFilter: fileName => file.indexOf('/client') !== 0,
|
942 | });
|
943 | ```
|
944 |
|
945 | ### .imported.js
|
946 |
|
947 | There is possibility to finely control both with files are scanned, which imports are added and which chunks would be generated(webpackonly)
|
948 | via `.imported.js` at the root directory
|
949 |
|
950 | > 💩 .js, not .ts
|
951 |
|
952 | ```js
|
953 | // 👉 use provided helper for documentation and type safety
|
954 | const { configure } = require('react-imported-component');
|
955 |
|
956 | modules.export = configure({
|
957 | testFile: fileName => true | false,
|
958 | testImport: (targetFile, sourceFile) => true | false,
|
959 | clientSideOnly: (targetFile, sourceFile, sourceComments) => true | false,
|
960 | shouldPrefetch: (targetFile, sourceFile, sourceComments) => true | false,
|
961 | shouldPreload: (targetFile, sourceFile, sourceComments) => true | false,
|
962 | chunkName: (targetFile, sourceFile, sourceComments) => string | null | undefined,
|
963 | });
|
964 | ```
|
965 |
|
966 | None of those methods are required.
|
967 |
|
968 | `.imported` could be used to:
|
969 |
|
970 | - remove some files or imports
|
971 | - autogenerate chunk names or prefetch/preload
|
972 |
|
973 | ### Bundler independent SSR
|
974 |
|
975 | It does not matter how do you bundle your application - it could be even browser. The secrect sause is a **cli** command, to extract all your imports into imports map, and use it later to load chunks by request.
|
976 |
|
977 | - You might even dont have any separated chunk on the server side - it would still works.
|
978 | - You might even ship module/nomodule scripts, using, for example, [devolution](https://github.com/theKashey/devolution) - no additional configuration would be required.
|
979 |
|
980 | ## Why you need SSR
|
981 |
|
982 | In case of imported component SSR is a "dry run" of your application - an easy way to discover required pieces
|
983 | powered by zero latency(you are already on the server) and super fast speed connection (all scripts are in memory).
|
984 |
|
985 | However - you dont need SSR to get the same benefits on pure ClientSideRendered solutions - _prediction_ would be enought.
|
986 | The common approach is to
|
987 |
|
988 | - load first part of components, including Providers, which would load something they need. Like translations.
|
989 | - then hit, and load the current route
|
990 | - then do the same with the sub route
|
991 |
|
992 | This causes effect known as `loading waves`, the effect SSR could mitigate almost in full.
|
993 |
|
994 | However, nothing stops you from loading translation data in the parallel to the routes, and loading
|
995 | route and sub route in the same time, not sequentially.
|
996 | You can have backend-for-frontend, why not to have frontend-for-frontend?
|
997 | Just handle route, cookies, and whatever you could handle, outside of React. [Redux-first-router](https://github.com/faceyspacey/redux-first-router) and principles behind it
|
998 | are the great example of this idealogy.
|
999 |
|
1000 | ### Not using React.Lazy with React-Hot-Loader
|
1001 |
|
1002 | There is design limitation with React.lazy support from RHL size, so they could not be reloaded without
|
1003 | state loss if `lazy` is created not in the user space. At it would be created inside imported.
|
1004 |
|
1005 | If React-Hot-Loader is detected `lazy` switches to `imported async` mode, this behaves absolutely the same.
|
1006 |
|
1007 | <a name="comparisonLegend" />
|
1008 |
|
1009 | ### Comparison table legend
|
1010 |
|
1011 | - `Library` - the library name
|
1012 | - `Suspense` - does it support Suspense feature
|
1013 | - `SSR` - does it support SSR
|
1014 | - `Hooks` - does it have hooks API
|
1015 | - `Library` - does it support _library_, not Component level splitting
|
1016 | - `Non-modules` - could it "import" generic promise, not a real dynamic module import
|
1017 | - `import('./${value}')` - does it support a _full dynamic import_ - ability to import any file.
|
1018 | - `babel macro` - ability to work with babel-plugin-macros
|
1019 | - `webpack only` - is this solution hard bould to webpack
|
1020 |
|
1021 | ## Featured in
|
1022 |
|
1023 | - https://medium.com/hackernoon/react-and-code-splitting-made-easy-f118befb5168
|
1024 | - https://dev.to/thekashey/react-imported-component-v6-4304
|
1025 |
|
1026 | ## Other loaders
|
1027 |
|
1028 | Another loaders exist, and the only difference is in API, and how they manage (or not manage) SSR.
|
1029 |
|
1030 | - (no SSR) React.Lazy
|
1031 | - (webpack only) With [react-loadable](https://github.com/thejameskyle/react-loadable)
|
1032 | - (not compatible with hooks) [react-async-component](https://github.com/ctrlplusb/react-async-component)
|
1033 | - (webpack only) [loadable-components](https://github.com/smooth-code/loadable-components)
|
1034 | - (webpack only) [react-universal-component](https://github.com/faceyspacey/react-universal-component)
|
1035 |
|
1036 | - (not a component loader) [use-sidecar](https://github.com/theKashey/use-sidecar)
|
1037 |
|
1038 | ## Licence
|
1039 |
|
1040 | MIT
|