diff --git a/.size-limit.js b/.size-limit.js index ae4d44a2..d402b73a 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -2,12 +2,12 @@ module.exports = [ { path: ['dist/es2015/entrypoints/index.js', 'dist/es2015/entrypoints/boot.js'], ignore: ['tslib'], - limit: '3.9 KB', + limit: '4.2 KB', }, { path: 'dist/es2015/entrypoints/index.js', ignore: ['tslib'], - limit: '3.6 KB', + limit: '3.8 KB', }, { path: 'dist/es2015/entrypoints/boot.js', diff --git a/.size.json b/.size.json index 50092380..ce082556 100644 --- a/.size.json +++ b/.size.json @@ -2,16 +2,16 @@ { "name": "dist/es2015/entrypoints/index.js, dist/es2015/entrypoints/boot.js", "passed": true, - "size": 3975 + "size": 4217 }, { "name": "dist/es2015/entrypoints/index.js", "passed": true, - "size": 3606 + "size": 3852 }, { "name": "dist/es2015/entrypoints/boot.js", "passed": true, - "size": 1923 + "size": 1938 } ] diff --git a/README.md b/README.md index c1783dac..e23471fe 100644 --- a/README.md +++ b/README.md @@ -144,7 +144,7 @@ const ClientSideOnly = () => ( const ServerSideFriendly = () => ( {' '} - // LazyBoundary is Suspense on the client, and "nothing" on the server + // LazyBoundary is Suspense* on the client, and "nothing" on the server ); @@ -254,7 +254,7 @@ If you have `imported` definition in one file, and use it from another - just `i - `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**. + - `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. @@ -291,6 +291,10 @@ 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 @@ -442,12 +446,17 @@ Before rendering your application you have to ensure - all parts are loaded. `rehydrateMarks` will load everything you need, and provide a promise to await. ```js -import { rehydrateMarks } from 'react-imported-component'; +import { rehydrateMarks, ImportedController } from 'react-imported-component'; // this will trigger all marked imports, and await for competition. rehydrateMarks().then(() => { - // better - ReactDOM.hydrate(, document.getElementById('main')); + // better (note ImportedController usage) + ReactDOM.hydrate( + + + , + document.getElementById('main') + ); // or ReactDOM.render(, document.getElementById('main')); }); diff --git a/package.json b/package.json index 649d670e..2ac815fb 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,8 @@ "main": "dist/es5/entrypoints/index.js", "jsnext:main": "dist/es2015/entrypoints/index.js", "module": "dist/es2015/entrypoints/index.js", - "sideEffects": false, "types": "dist/es5/entrypoints/index.d.ts", + "sideEffects": false, "scripts": { "build:ci": "lib-builder build && yarn size", "build": "rm -Rf ./dist/* && lib-builder build && yarn size && yarn size:report", @@ -79,7 +79,7 @@ "crc-32": "^1.2.0", "detect-node-es": "^1.0.0", "scan-directory": "^2.0.0", - "tslib": "^1.10.0" + "tslib": "^2.0.0" }, "engines": { "node": ">=8.5.0" diff --git a/src/entrypoints/index.ts b/src/entrypoints/index.ts index fd3f914c..7c397a5f 100644 --- a/src/entrypoints/index.ts +++ b/src/entrypoints/index.ts @@ -12,10 +12,12 @@ import { ImportedComponent } from '../ui/Component'; import { ImportedComponent as ComponentLoader } from '../ui/Component'; import { ImportedStream } from '../ui/context'; import imported, { lazy } from '../ui/HOC'; -import LazyBoundary from '../ui/LazyBoundary'; +import { ImportedController } from '../ui/ImportedController'; +import { LazyBoundary } from '../ui/LazyBoundary'; import { ImportedModule, importedModule } from '../ui/Module'; import { useImported, useLazy, useLoadable } from '../ui/useImported'; import { remapImports } from '../utils/helpers'; +import { useIsClientPhase } from '../utils/useClientPhase'; export { printDrainHydrateMarks, @@ -36,6 +38,8 @@ export { importedModule, lazy, LazyBoundary, + ImportedController, + useIsClientPhase, remapImports, useLoadable, useImported, diff --git a/src/entrypoints/server.ts b/src/entrypoints/server.ts index ebc463dc..08cbdcd9 100644 --- a/src/entrypoints/server.ts +++ b/src/entrypoints/server.ts @@ -4,6 +4,7 @@ import { drainHydrateMarks, printDrainHydrateMarks } from '../loadable/marks'; import { createLoadableStream } from '../loadable/stream'; import { getLoadableTrackerCallback } from '../trackers/globalTracker'; import { createLoadableTransformer } from '../transformers/loadableTransformer'; +import { Stream as ImportedStreamTracker } from '../types'; import { ImportedStream } from '../ui/context'; export { @@ -16,4 +17,5 @@ export { getLoadableTrackerCallback, getMarkedChunks, getMarkedFileNames, + ImportedStreamTracker, }; diff --git a/src/loadable/assignImportedComponents.ts b/src/loadable/assignImportedComponents.ts index df05427c..02c5ee5b 100644 --- a/src/loadable/assignImportedComponents.ts +++ b/src/loadable/assignImportedComponents.ts @@ -17,11 +17,16 @@ export const assignImportedComponents = (set: ImportedDefinition[]) => { assignMetaData(loadable.mark, loadable, imported[1], imported[2]); }); - if (countBefore === LOADABLE_SIGNATURE.size) { + if (set.length === 0) { // tslint:disable-next-line:no-console console.error('react-imported-component: no import-marks found, please check babel plugin'); } + if (countBefore === LOADABLE_SIGNATURE.size) { + // tslint:disable-next-line:no-console + console.error('react-imported-component: no new imports found'); + } + done(); return set; diff --git a/src/loadable/stream.ts b/src/loadable/stream.ts index 4a461fb5..6a16d474 100644 --- a/src/loadable/stream.ts +++ b/src/loadable/stream.ts @@ -1,6 +1,6 @@ import { Stream } from '../types'; -export const createLoadableStream = () => ({ marks: {} }); +export const createLoadableStream = (): Stream => ({ marks: {} }); export const clearStream = (stream?: Stream) => { if (stream) { stream.marks = {}; diff --git a/src/ui/HOC.tsx b/src/ui/HOC.tsx index 23178ff0..8449f0cb 100644 --- a/src/ui/HOC.tsx +++ b/src/ui/HOC.tsx @@ -25,10 +25,12 @@ function loader( loaderFunction: DefaultComponentImport

, baseOptions: Partial> & HOCOptions = {} ): HOCType { - const loadable = getLoadable(loaderFunction); + let loadable = getLoadable(loaderFunction); const Imported = React.forwardRef(function ImportedComponentHOC({ importedProps = {}, ...props }, ref) { const options = { ...baseOptions, ...importedProps }; + // re-get loadable in order to have fresh reference + loadable = getLoadable(loaderFunction); return ( ( return loadable.resolution; }; - Imported.done = loadable.resolution; + Object.defineProperty(Imported, 'done', { + get() { + return loadable.resolution; + }, + }); return Imported; } diff --git a/src/ui/ImportedController.tsx b/src/ui/ImportedController.tsx new file mode 100644 index 00000000..d26d87de --- /dev/null +++ b/src/ui/ImportedController.tsx @@ -0,0 +1,46 @@ +import React, { createContext, useCallback, useLayoutEffect, useState } from 'react'; + +interface ImportedState { + usesHydration: boolean; + pastHydration: boolean; +} + +export const importedState = createContext(undefined); + +export const HydrationState: React.FC<{ state: ImportedState }> = ({ state, children }) => ( + {children} +); + +/** + * this component just creates a "the first-most" effect in the system + */ +const HydrationEffect = ({ loopCallback }: { loopCallback(): void }): null => { + useLayoutEffect(loopCallback, []); + return null; +}; + +/** + * @see [LazyBoundary]{@link LazyBoundary} - HydrationController is required for LazyBoundary to properly work with React>16.10 + * Established a control over LazyBoundary suppressing fallback during the initial hydration + * @param props + * @param [props.usesHydration=true] determines of Application is rendered using hydrate + */ +export const ImportedController: React.FC<{ + /** + * determines of Application is rendered using hydrate + */ + usesHydration?: boolean; +}> = ({ children, usesHydration = true }) => { + const [state, setState] = useState({ + usesHydration, + pastHydration: false, + }); + + const onFirstHydration = useCallback(() => setState(oldState => ({ ...oldState, pastHydration: true })), []); + return ( + <> + + {children} + + ); +}; diff --git a/src/ui/LazyBoundary.tsx b/src/ui/LazyBoundary.tsx index 26fa8c71..2c672ae7 100644 --- a/src/ui/LazyBoundary.tsx +++ b/src/ui/LazyBoundary.tsx @@ -1,13 +1,26 @@ import * as React from 'react'; import { isBackend } from '../utils/detectBackend'; +import { useIsClientPhase } from '../utils/useClientPhase'; -const LazyBoundary: React.FC<{ +const LazyServerBoundary: React.FC<{ fallback: NonNullable | null; }> = ({ children }) => {children}; +const LazyClientBoundary: React.FC<{ + fallback: NonNullable | null; +}> = ({ children, fallback }) => ( + + {children} + +); + /** - * React.Suspense "as-is" replacement + * React.Suspense "as-is" replacement. Automatically "removed" during SSR and "patched" to work accordingly on the clientside + * + * @see {@link HydrationController} has to wrap entire application in order to provide required information */ -const Boundary = isBackend ? LazyBoundary : React.Suspense; - -export default Boundary; +export const LazyBoundary = isBackend ? LazyServerBoundary : LazyClientBoundary; diff --git a/src/ui/context.tsx b/src/ui/context.tsx index 3e4b9405..82a21dc7 100644 --- a/src/ui/context.tsx +++ b/src/ui/context.tsx @@ -2,16 +2,14 @@ import * as React from 'react'; import { defaultStream } from '../loadable/stream'; import { Stream } from '../types'; -interface TakeProps { - stream: Stream; -} - export const streamContext = React.createContext(defaultStream); /** * SSR. Tracker for used marks */ -export const ImportedStream: React.FC = ({ stream, children, ...props }) => { +export const ImportedStream: React.FC<{ + stream: Stream; +}> = ({ stream, children, ...props }) => { if (process.env.NODE_ENV !== 'development') { if ('takeUID' in props) { throw new Error('react-imported-component: `takeUID` was replaced by `stream`.'); diff --git a/src/utils/useClientPhase.tsx b/src/utils/useClientPhase.tsx new file mode 100644 index 00000000..2daef073 --- /dev/null +++ b/src/utils/useClientPhase.tsx @@ -0,0 +1,18 @@ +import { useContext } from 'react'; +import { importedState } from '../ui/ImportedController'; + +/** + * returns "true" if currently is a "client" phase and all features should be active + * @see {@link HydrationController} + */ +export const useIsClientPhase = (): boolean => { + const value = useContext(importedState); + if (!value) { + if (process.env.NODE_ENV !== 'production') { + // tslint:disable-next-line:no-console + console.warn('react-imported-component: please wrap your entire application with ImportedController'); + } + return true; + } + return value.pastHydration; +}; diff --git a/yarn.lock b/yarn.lock index d9da3282..4c0df7b5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15976,7 +15976,7 @@ ts-pnp@^1.1.6: resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.2.0.tgz#a500ad084b0798f1c3071af391e65912c86bca92" integrity sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw== -tslib@^1.10.0, tslib@^1.6.0, tslib@^1.8.0, tslib@^1.8.1, tslib@^1.9.3: +tslib@^1.6.0, tslib@^1.8.0, tslib@^1.8.1, tslib@^1.9.3: version "1.10.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ== @@ -15986,6 +15986,11 @@ tslib@^1.9.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286" integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ== +tslib@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a" + integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A== + tslint-config-prettier@^1.18.0: version "1.18.0" resolved "https://registry.yarnpkg.com/tslint-config-prettier/-/tslint-config-prettier-1.18.0.tgz#75f140bde947d35d8f0d238e0ebf809d64592c37"