Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/theKashey/react-imported-component

βœ‚οΈπŸ“¦Bundler-independent solution for SSR-friendly code-splitting
https://github.com/theKashey/react-imported-component

async-components code-splitting loader parcel-bundler react server-side-rendering ssr webpack

Last synced: 3 months ago
JSON representation

βœ‚οΈπŸ“¦Bundler-independent solution for SSR-friendly code-splitting

Awesome Lists containing this project

README

        


IMPORTED COMPONENT βœ‚


Code splitting which always works*




imported components




SSR-friendly code splitting compatible with any platform.


Deliver a better experience within a single import.














npm downloads


bundle size





> \* It's really will never let you down. All credits to your bundler.

πŸ‘‰ [Usage](#usage) | [API](#api) | [Setup](#setup) | [SSR](#ssr) | [CCS](#css) [Concurrent loading](#concurrent-loading) | [Webpack/Parcel](#bundler-integration)

| Library | Suspense | SSR | Hooks | Library | Non-modules | import(`./${value}`) | babel-macro | webpack only |
| ------------------- | :------: | :-: | :---: | :-----: | :---------: | :------------------: | :---------: | :----------: |
| React.lazy | βœ… | ❌ | ❌ | ❌ | ❌ | ❌ | 😹 | no-ssr |
| react-loadable | βœ… | βœ… | ❌ | ❌ | βœ… | ❌ | ❌ | 😿 |
| @loadable/component | βœ… | βœ… | ❌ | βœ… | ❌ | βœ… | ❌ | 😿 |
| imported-component | βœ… | βœ… | βœ… | βœ… | βœ… | ❌ | βœ… | 😸 |

> Read more about [what this table displays](#comparisonLegend)

Key features:

- 1️⃣ Single source of truth - your **bundler drives** everything
- πŸ“– **library** level code **splitting**
- πŸ§™οΈ Hybrid and **Prerendering** compatible
- πŸ’‘ **TypeScript** bindings
- βš›οΈ **React.Lazy** underneath (if hot module updates are disabled)
- 🌟 Async on client, sync on server. Supports **Suspense** (even on server side)
- πŸ“¦ could work with **any bundler** - webpack, rollup, parcel or puppeteer - it does not matter
- πŸ€Ήβ€β™‚οΈ working as well with any `import` you may provide

Other features:

- πŸ”₯ Hot-Module-Replacement/React-Hot-Loader friendly
- ⛓️ support forwardRef
- βš›οΈ React 16/Async/Hooks ready
- πŸ›  HOC, Component, Hooks API
- 🐳 stream rendering support
- πŸ‘₯ partial hydration out of the box
- πŸ“¦ 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**

πŸ‘ Better than [React.Lazy](https://reactjs.org/docs/code-splitting.html#reactlazy):

- It **IS** Lazy, just with some stuff around\*
- SSR, Prerendering and Preloading support
- With or without Suspense, and easier Error cases support

πŸ‘ Better than others:

- Not bound to webpack
- Easy way to use per-browser(modern/legacy) bundles - you down have to mess with actual browser support
- Strong typing
- Working with any imports, even native, external or derived ones.

πŸ‘Œ Client-side module resolution

- Loads chunks only after the `main one`, as long as loader code is bundled inside the main chunk, so it should be loaded first.
- Not an issue with the `progressive hydration`, and might provide a better UX via feature detection.
- Provides πŸ‘¨β€πŸ”¬ technological workaround - [see here](#concurrent-loading)

πŸ“¦ Optional bundler integration for the best experience

- prefetching backed by webpack `stat.json` and `asset.json`
- `parcel-manifest.json` support

πŸ‘―β€β™€οΈWorks better in pair

- [react-prerendered-component](https://github.com/theKashey/react-prerendered-component) for prerendering, partial hydration and react fragment caching
- [used-style](https://github.com/theKashey/used-styles) for CSS and critical CSS extraction
- [devolution](https://github.com/theKashey/devolution) for shipping legacy/modern bundles
- [webpack-imported](https://github.com/theKashey/webpack-imported) for deep `webpack` integration, the only bundler dependent tool in this list.

# Usage

## Server side

Just a proper setup and a bit of magic

## Client side

### Component

`imported` provides 2 common ways to define a component, which are more different inside than outside

- using `pre-lazy` API.

```javascript
import importedComponent from 'react-imported-component';
const Component = importedComponent( () => import('./Component'));

const Component = importedComponent( () => import('./Component'), {
LoadingComponent: Spinner, // what to display during the loading
ErrorComponent: FatalError // what to display in case of error
});

Component.preload(); // force preload

// render it

```

- using `lazy` API. It's almost the same `React.lazy` outside, and exactly the same inside.

```js
import { lazy, LazyBoundary } from 'react-imported-component';
const Component = lazy(() => import('./Component'));

const ClientSideOnly = () => (



);

// or let's make it SSR friendly
const ServerSideFriendly = () => (

{' '}
// LazyBoundary is Suspense* on the client, and "nothing" on the server


);
```

`LazyBoundary` is a `Suspense` on Client Side, and `React.Fragment` on Server Side. Don't forget - "dynamic" imports are sync on a server.

Example: [React.lazy vs Imported-component](https://codesandbox.io/s/wkl95r0qw8)

### Hook

However, you may not load only components - you may load anything

```js
import {useImported} from 'react-imported-component'

const MyCalendarComponent = () => {
const {
imported: moment,
loading
} = useImported(() => import("moment"));

return loading ? "..." : today is {moment(Date.now).format()}
}

// or we could make it a bit more interesting...

const MyCalendarComponent = () => {
const {
imported: format = x => "---", // default value is used while importing library
} = useImported(
() => import("moment"),
moment => x => moment(x).format // masking everything behind
);

return today is {format(Date.now())
}
```

What you could load using `useImported`? Everything - `imported` itself is using it to import components.

> `useImported` is an excellent example for loading translations, which are usually a simple json, in a _trackable_ way.

> πŸ’‘ 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.

πŸ€” Keep in mind - **everything here is using `useImported`**, and **you can build whatever you need** using just it.

### Module

A slim helper to help handle `modules`, you might require using `useImported` in a component way

```js
import { importedModule, ImportedModule } from 'react-imported-component';

const Moment = importedModule(() => import('moment'));

{(momentjs /* default imports are auto-imported*/) => momentjs(date).fromNow()}
;
```

> Yes, this example was taken from [loadable.lib](https://www.smooth-code.com/open-source/loadable-components/docs/library-splitting/)

Can I also use a ref, populated when the library is loaded? No, you cant. Use `useImported` for any special case like this.

Plus, there is a Component helper:

```js
import('moment').then(({momentDefault})=> momentDefault(date).fromNow()}
fallback="long time ago"
>
{(fromNow) => fromNow()}

```

> ImportedModule will throw a promise to the nearest Suspense boundary if no `fallback` provided.

## Babel macro

If you could not use babel plugin, but do have `babel-plugin-macro` (like CRA) - consider using macro API:

```js
import { imported, lazy, useImported } from 'react-imported-component/macro';
// notice - there is no default import here
```

### Indirect usage

Just importing `react-imported-component/macro` would enable babel transformation for the current file.
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)

# API

> Don't forget - there are TS typings provided.

### Code splitting components

> import {\*} from 'react-imported-component';

##### importedComponent

- `importedComponent(importFunction, [options]): ComponentLoader` - main API, default export, HOC to create imported component.

- `importFunction` - function which resolves with Component to be imported.
- `options` - optional settings
- `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)
- `options.LoadingComponent` - component to be shown in Loading state
- `options.ErrorComponent` - component to be shown in Error state. Will re-throw error if ErrorComponent is not set. Use ErrorBoundary to catch it.
- `options.onError` - function to consume the error, if one will thrown. Will rethrow a real error if not set.
- `options.exportPicker` - function to pick `not default` export from a `importFunction`
- `options.render(Component, state, props)`- function to render the result. Could be used to tune the rendering.

- [static] `.preload` - static method to preload components.

##### lazy

- `lazy(importFunction)` - helper to mimic **React.lazy** behavior

##### useImported

- `useImported(importFunction, [exportPicker], [options])` - code splitting hook

- `importFunction` - a function which resolves to `default` or `wildcard` import(T | {default:T})
- `[exportPicker]` - function to pick "T" from the import
- `[options]` - options to the hook

- `[options.import]` - controls import. Hooks would be executed only if this is not false
- `[options.track]` - ability to disable server-side usage tracking.

`useImported` returns complex object(ImportedShape):

- `imported` - the imported resource
- `error` - error (if present)
- `loading` - is it loading right now?
- `loadable` - the underlying `Loadable` object
- `retry` - retry action (in case of error)

Hints:

- use `options.import=false` to perform conditional import - `importFunction` would not be used if this option set to `false.
- use `options.track=true` to perform SSR only import - to usage would be tracked if this option set to `false.

##### ImportedController

- `` - a controller for Suspense Hydration. **Compulsory** for async/lazy usecases

##### Misc

There is also API method, unique for imported-component, which could be useful on the client side

- `addPreloader(fn):fn` - adds a function, result of which would be _awaited_ when any component is loaded. Returns cancel method.

### Server side API

> import {\*} from 'react-imported-component/server';

- `whenComponentsReady():Promise` - will be resolved, when all components are loaded. Usually on the next "Promise" tick.
- `drainHydrateMarks([stream])` - returns the currently used marks, and clears the list.
- `printDrainHydrateMarks([stream])` - print our the `drainHydrateMarks`.

#### Stream API

- `createLoadableStream` - creates a steam
- `ImportedStream` - wraps another component with import usage tracker.
- `createLoadableTransformer` - creates nodejs StreamTransformer
- `getLoadableTrackerCallback` - helper factory for the stream transformer

### Client side API

> import {\*} from 'react-imported-component/boot';

- `whenComponentsReady():Promise`, will be resolved, when all (loading right now) marks are loaded.
- `rehydrateMarks([marks]):Promise`, loads _marked_ async chunks.
- `injectLoadableTracker` - helper factory for the stream transformer

### Types

#### Loadable

All imports inside library are converted into `Loadable` object, and it's often accessible from outside via
`useImported().loadable`, `useLoadable`(not documented), `getLoadable`(not documented). Even if it's documented from TS point of view -
let's keep all fields in a secret, except one:

- `resolution` - promise reflecting resolution of this loadable object



# Setup

## In short

1. Add `babel` plugin
2. Run `yarn imported-components src src/imported.js` to extract all your imports into a `run time chunk` (aka async-requires).
3. Replace `React.lazy` with our `lazy`, and `React.Suspense` with our `LazyBoundary`. Literraly [monkey-patch React to do so](#monkey-patch)
4. Add `printDrainHydrateMarks` to the server code.
5. Add `rehydrateMarks` to the client code
6. Done. Just read the rest of readme for details.

There are examples for webpack, parcel, and react-snap. Just follow them.

## 1. Configure babel plugin

**On the server**:

```json
{
"plugins": ["react-imported-component/babel", "babel-plugin-dynamic-import-node" /* might be optional for babel 7*/]
}
```

**On the client**:

```json
{
"plugins": ["react-imported-component/babel"]
}
```

Imported-Component will hook into dynamic imports, providing extra information about files you want to load.

## 2. Add one more command into package.json

CLI command `imported-components [sources ROOT] [targetFile.js]` (use .ts for TypeScript)

```js
"generate-imported-component": "imported-components src src/imported.js"
```

When you will execute this command - all `imports` among your codebase would be found and extracted to a file provided.
This will gave ability to orchestrate code-splitting later.

If you need to search inside more that one top-level directory - just define more command, saving information into more than one target file.

> The current implementation will discover and use all `imports`, even // commented ones

> πŸ’‘ Feel free to **.gitignore** these autogenerated files

## 3. Start using `imported`, `lazy` or `useImported`

Without you using API provided nothing would work.

## 4. Add server side tracking

There are two ways to do it - in a single threaded way, and async

#### Single threaded

```js
import { printDrainHydrateMarks, drainHydrateMarks } from 'react-imported-component';
// this action will "drain" all currently used(by any reason) marks
// AND print a script tag
const html = renderToString() + printDrainHydrateMarks();

// OR return list of usedmarks, and yet again CLEAR the marks list.
const html = renderToString() + 'const marks=' + JSON.stringify(drainHydrateMarks()) + '';
```

#### renderToStream or async render

```js
import {createLoadableStream} from 'react-imported-component/server';

let importedStream = createLoadableStream();
// ImportedStream is a async rendering "provider"
const stream = renderToStream(



);

// you'd then pipe the stream into the response object until it's done
stream.pipe(res, { end: false });

// and finalize the response with closing HTML
stream.on('end', () =>
// print marks used in the file
res.end(`${printDrainHydrateMarks(importedStream)}