Skip to content

Commit 76131b2

Browse files
authored
Merge pull request #212 from theKashey/suspense-hydration
Suspense hydration
2 parents 03165dc + f3f005d commit 76131b2

File tree

14 files changed

+134
-28
lines changed

14 files changed

+134
-28
lines changed

.size-limit.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@ module.exports = [
22
{
33
path: ['dist/es2015/entrypoints/index.js', 'dist/es2015/entrypoints/boot.js'],
44
ignore: ['tslib'],
5-
limit: '3.9 KB',
5+
limit: '4.2 KB',
66
},
77
{
88
path: 'dist/es2015/entrypoints/index.js',
99
ignore: ['tslib'],
10-
limit: '3.6 KB',
10+
limit: '3.8 KB',
1111
},
1212
{
1313
path: 'dist/es2015/entrypoints/boot.js',

.size.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,16 @@
22
{
33
"name": "dist/es2015/entrypoints/index.js, dist/es2015/entrypoints/boot.js",
44
"passed": true,
5-
"size": 3975
5+
"size": 4217
66
},
77
{
88
"name": "dist/es2015/entrypoints/index.js",
99
"passed": true,
10-
"size": 3606
10+
"size": 3852
1111
},
1212
{
1313
"name": "dist/es2015/entrypoints/boot.js",
1414
"passed": true,
15-
"size": 1923
15+
"size": 1938
1616
}
1717
]

README.md

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ const ClientSideOnly = () => (
144144
const ServerSideFriendly = () => (
145145
<LazyBoundary>
146146
{' '}
147-
// LazyBoundary is Suspense on the client, and "nothing" on the server
147+
// LazyBoundary is Suspense* on the client, and "nothing" on the server
148148
<Component />
149149
</LazyBoundary>
150150
);
@@ -254,7 +254,7 @@ If you have `imported` definition in one file, and use it from another - just `i
254254

255255
- `importFunction` - function which resolves with Component to be imported.
256256
- `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**.
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)
258258
- `options.LoadingComponent` - component to be shown in Loading state
259259
- `options.ErrorComponent` - component to be shown in Error state. Will re-throw error if ErrorComponent is not set. Use ErrorBoundary to catch it.
260260
- `options.onError` - function to consume the error, if one will thrown. Will rethrow a real error if not set.
@@ -291,6 +291,10 @@ Hints:
291291
- use `options.import=false` to perform conditional import - `importFunction` would not be used if this option set to `false.
292292
- use `options.track=true` to perform SSR only import - to usage would be tracked if this option set to `false.
293293

294+
##### ImportedController
295+
296+
- `<ImportedControoler>` - a controller for Suspense Hydration. **Compulsory** for async/lazy usecases
297+
294298
##### Misc
295299

296300
There is also API method, unique for imported-component, which could be useful on the client side
@@ -442,12 +446,17 @@ Before rendering your application you have to ensure - all parts are loaded.
442446
`rehydrateMarks` will load everything you need, and provide a promise to await.
443447

444448
```js
445-
import { rehydrateMarks } from 'react-imported-component';
449+
import { rehydrateMarks, ImportedController } from 'react-imported-component';
446450

447451
// this will trigger all marked imports, and await for competition.
448452
rehydrateMarks().then(() => {
449-
// better
450-
ReactDOM.hydrate(<App />, document.getElementById('main'));
453+
// better (note ImportedController usage)
454+
ReactDOM.hydrate(
455+
<ImportedController>
456+
<App />
457+
</ImportedController>,
458+
document.getElementById('main')
459+
);
451460
// or
452461
ReactDOM.render(<App />, document.getElementById('main'));
453462
});

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
"main": "dist/es5/entrypoints/index.js",
66
"jsnext:main": "dist/es2015/entrypoints/index.js",
77
"module": "dist/es2015/entrypoints/index.js",
8-
"sideEffects": false,
98
"types": "dist/es5/entrypoints/index.d.ts",
9+
"sideEffects": false,
1010
"scripts": {
1111
"build:ci": "lib-builder build && yarn size",
1212
"build": "rm -Rf ./dist/* && lib-builder build && yarn size && yarn size:report",
@@ -79,7 +79,7 @@
7979
"crc-32": "^1.2.0",
8080
"detect-node-es": "^1.0.0",
8181
"scan-directory": "^2.0.0",
82-
"tslib": "^1.10.0"
82+
"tslib": "^2.0.0"
8383
},
8484
"engines": {
8585
"node": ">=8.5.0"

src/entrypoints/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@ import { ImportedComponent } from '../ui/Component';
1212
import { ImportedComponent as ComponentLoader } from '../ui/Component';
1313
import { ImportedStream } from '../ui/context';
1414
import imported, { lazy } from '../ui/HOC';
15-
import LazyBoundary from '../ui/LazyBoundary';
15+
import { ImportedController } from '../ui/ImportedController';
16+
import { LazyBoundary } from '../ui/LazyBoundary';
1617
import { ImportedModule, importedModule } from '../ui/Module';
1718
import { useImported, useLazy, useLoadable } from '../ui/useImported';
1819
import { remapImports } from '../utils/helpers';
20+
import { useIsClientPhase } from '../utils/useClientPhase';
1921

2022
export {
2123
printDrainHydrateMarks,
@@ -36,6 +38,8 @@ export {
3638
importedModule,
3739
lazy,
3840
LazyBoundary,
41+
ImportedController,
42+
useIsClientPhase,
3943
remapImports,
4044
useLoadable,
4145
useImported,

src/entrypoints/server.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { drainHydrateMarks, printDrainHydrateMarks } from '../loadable/marks';
44
import { createLoadableStream } from '../loadable/stream';
55
import { getLoadableTrackerCallback } from '../trackers/globalTracker';
66
import { createLoadableTransformer } from '../transformers/loadableTransformer';
7+
import { Stream as ImportedStreamTracker } from '../types';
78
import { ImportedStream } from '../ui/context';
89

910
export {
@@ -16,4 +17,5 @@ export {
1617
getLoadableTrackerCallback,
1718
getMarkedChunks,
1819
getMarkedFileNames,
20+
ImportedStreamTracker,
1921
};

src/loadable/assignImportedComponents.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,16 @@ export const assignImportedComponents = (set: ImportedDefinition[]) => {
1717
assignMetaData(loadable.mark, loadable, imported[1], imported[2]);
1818
});
1919

20-
if (countBefore === LOADABLE_SIGNATURE.size) {
20+
if (set.length === 0) {
2121
// tslint:disable-next-line:no-console
2222
console.error('react-imported-component: no import-marks found, please check babel plugin');
2323
}
2424

25+
if (countBefore === LOADABLE_SIGNATURE.size) {
26+
// tslint:disable-next-line:no-console
27+
console.error('react-imported-component: no new imports found');
28+
}
29+
2530
done();
2631

2732
return set;

src/loadable/stream.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Stream } from '../types';
22

3-
export const createLoadableStream = () => ({ marks: {} });
3+
export const createLoadableStream = (): Stream => ({ marks: {} });
44
export const clearStream = (stream?: Stream) => {
55
if (stream) {
66
stream.marks = {};

src/ui/HOC.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,12 @@ function loader<P, K = P>(
2525
loaderFunction: DefaultComponentImport<P>,
2626
baseOptions: Partial<ComponentOptions<P, K>> & HOCOptions = {}
2727
): HOCType<P, K> {
28-
const loadable = getLoadable(loaderFunction);
28+
let loadable = getLoadable(loaderFunction);
2929

3030
const Imported = React.forwardRef<any, any>(function ImportedComponentHOC({ importedProps = {}, ...props }, ref) {
3131
const options = { ...baseOptions, ...importedProps };
32+
// re-get loadable in order to have fresh reference
33+
loadable = getLoadable(loaderFunction);
3234

3335
return (
3436
<ImportedComponent
@@ -49,7 +51,11 @@ function loader<P, K = P>(
4951

5052
return loadable.resolution;
5153
};
52-
Imported.done = loadable.resolution;
54+
Object.defineProperty(Imported, 'done', {
55+
get() {
56+
return loadable.resolution;
57+
},
58+
});
5359

5460
return Imported;
5561
}

src/ui/ImportedController.tsx

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import React, { createContext, useCallback, useLayoutEffect, useState } from 'react';
2+
3+
interface ImportedState {
4+
usesHydration: boolean;
5+
pastHydration: boolean;
6+
}
7+
8+
export const importedState = createContext<ImportedState | undefined>(undefined);
9+
10+
export const HydrationState: React.FC<{ state: ImportedState }> = ({ state, children }) => (
11+
<importedState.Provider value={state}>{children}</importedState.Provider>
12+
);
13+
14+
/**
15+
* this component just creates a "the first-most" effect in the system
16+
*/
17+
const HydrationEffect = ({ loopCallback }: { loopCallback(): void }): null => {
18+
useLayoutEffect(loopCallback, []);
19+
return null;
20+
};
21+
22+
/**
23+
* @see [LazyBoundary]{@link LazyBoundary} - HydrationController is required for LazyBoundary to properly work with React>16.10
24+
* Established a control over LazyBoundary suppressing fallback during the initial hydration
25+
* @param props
26+
* @param [props.usesHydration=true] determines of Application is rendered using hydrate
27+
*/
28+
export const ImportedController: React.FC<{
29+
/**
30+
* determines of Application is rendered using hydrate
31+
*/
32+
usesHydration?: boolean;
33+
}> = ({ children, usesHydration = true }) => {
34+
const [state, setState] = useState<ImportedState>({
35+
usesHydration,
36+
pastHydration: false,
37+
});
38+
39+
const onFirstHydration = useCallback(() => setState(oldState => ({ ...oldState, pastHydration: true })), []);
40+
return (
41+
<>
42+
<HydrationEffect loopCallback={onFirstHydration} />
43+
<HydrationState state={state}>{children}</HydrationState>
44+
</>
45+
);
46+
};

src/ui/LazyBoundary.tsx

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,26 @@
11
import * as React from 'react';
22
import { isBackend } from '../utils/detectBackend';
3+
import { useIsClientPhase } from '../utils/useClientPhase';
34

4-
const LazyBoundary: React.FC<{
5+
const LazyServerBoundary: React.FC<{
56
fallback: NonNullable<React.ReactNode> | null;
67
}> = ({ children }) => <React.Fragment>{children}</React.Fragment>;
78

9+
const LazyClientBoundary: React.FC<{
10+
fallback: NonNullable<React.ReactNode> | null;
11+
}> = ({ children, fallback }) => (
12+
<React.Suspense
13+
// we keep fallback null during hydration as it is expected behavior for "ssr-ed" Suspense blocks - they should not "fallback"
14+
// see https://github.com/sebmarkbage/react/blob/185700696ebbe737c99bd6c4b678d5f2a923bd29/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js#L668-L682
15+
fallback={useIsClientPhase() ? fallback : (undefined as any)}
16+
>
17+
{children}
18+
</React.Suspense>
19+
);
20+
821
/**
9-
* React.Suspense "as-is" replacement
22+
* React.Suspense "as-is" replacement. Automatically "removed" during SSR and "patched" to work accordingly on the clientside
23+
*
24+
* @see {@link HydrationController} has to wrap entire application in order to provide required information
1025
*/
11-
const Boundary = isBackend ? LazyBoundary : React.Suspense;
12-
13-
export default Boundary;
26+
export const LazyBoundary = isBackend ? LazyServerBoundary : LazyClientBoundary;

src/ui/context.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,14 @@ import * as React from 'react';
22
import { defaultStream } from '../loadable/stream';
33
import { Stream } from '../types';
44

5-
interface TakeProps {
6-
stream: Stream;
7-
}
8-
95
export const streamContext = React.createContext(defaultStream);
106

117
/**
128
* SSR. Tracker for used marks
139
*/
14-
export const ImportedStream: React.FC<TakeProps> = ({ stream, children, ...props }) => {
10+
export const ImportedStream: React.FC<{
11+
stream: Stream;
12+
}> = ({ stream, children, ...props }) => {
1513
if (process.env.NODE_ENV !== 'development') {
1614
if ('takeUID' in props) {
1715
throw new Error('react-imported-component: `takeUID` was replaced by `stream`.');

src/utils/useClientPhase.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { useContext } from 'react';
2+
import { importedState } from '../ui/ImportedController';
3+
4+
/**
5+
* returns "true" if currently is a "client" phase and all features should be active
6+
* @see {@link HydrationController}
7+
*/
8+
export const useIsClientPhase = (): boolean => {
9+
const value = useContext(importedState);
10+
if (!value) {
11+
if (process.env.NODE_ENV !== 'production') {
12+
// tslint:disable-next-line:no-console
13+
console.warn('react-imported-component: please wrap your entire application with ImportedController');
14+
}
15+
return true;
16+
}
17+
return value.pastHydration;
18+
};

yarn.lock

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15976,7 +15976,7 @@ ts-pnp@^1.1.6:
1597615976
resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.2.0.tgz#a500ad084b0798f1c3071af391e65912c86bca92"
1597715977
integrity sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw==
1597815978

15979-
tslib@^1.10.0, tslib@^1.6.0, tslib@^1.8.0, tslib@^1.8.1, tslib@^1.9.3:
15979+
tslib@^1.6.0, tslib@^1.8.0, tslib@^1.8.1, tslib@^1.9.3:
1598015980
version "1.10.0"
1598115981
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a"
1598215982
integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==
@@ -15986,6 +15986,11 @@ tslib@^1.9.0:
1598615986
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286"
1598715987
integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==
1598815988

15989+
tslib@^2.0.0:
15990+
version "2.1.0"
15991+
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a"
15992+
integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==
15993+
1598915994
tslint-config-prettier@^1.18.0:
1599015995
version "1.18.0"
1599115996
resolved "https://registry.yarnpkg.com/tslint-config-prettier/-/tslint-config-prettier-1.18.0.tgz#75f140bde947d35d8f0d238e0ebf809d64592c37"

0 commit comments

Comments
 (0)