├── .npmrc
├── src
├── index.ts
├── component
│ ├── index.ts
│ ├── TryAgainPage.tsx
│ └── index.less
├── .DS_Store
├── types.ts
├── lazyEntrance.tsx
├── loadable.tsx
├── retry.ts
├── withErrorBoundary.tsx
└── assets
│ └── page-crash.svg
├── .gitignore
├── demo
├── WechatIMG591.png
├── app.tsx
├── error.tsx
└── index.tsx
├── babel.config.js
├── typings
└── global.d.ts
├── .fatherrc.ts
├── .prettierrc
├── tsconfig.node.json
├── .editorconfig
├── vite.config.ts
├── index.html
├── tsconfig.json
├── LICENSE
├── README.md
└── package.json
/.npmrc:
--------------------------------------------------------------------------------
1 | registry=https://registry.npmjs.org/
2 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './lazyEntrance';
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .DS_Store
3 | dist
4 |
--------------------------------------------------------------------------------
/src/component/index.ts:
--------------------------------------------------------------------------------
1 | export * from './TryAgainPage';
2 |
--------------------------------------------------------------------------------
/src/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hawx1993/lazy-import-with-error-boundary/main/src/.DS_Store
--------------------------------------------------------------------------------
/demo/WechatIMG591.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hawx1993/lazy-import-with-error-boundary/main/demo/WechatIMG591.png
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | ['@babel/preset-env', { targets: { node: 'current' } }],
4 | '@babel/preset-typescript',
5 | ],
6 | };
7 |
--------------------------------------------------------------------------------
/demo/app.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const App = () => {
4 | const a = 1;
5 | a += 1;
6 | return
;
13 | };
14 |
15 | export { State, ErrorProps, Options };
16 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | charset = utf-8
9 | trim_trailing_whitespace = true
10 | insert_final_newline = true
11 |
12 | [*.md]
13 | trim_trailing_whitespace = false
14 |
15 | [Makefile]
16 | indent_style = tab
17 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite';
2 | import react from '@vitejs/plugin-react';
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | css: {
8 | preprocessorOptions: {
9 | less: {
10 | javascriptEnabled: true,
11 | },
12 | },
13 | },
14 | });
15 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Vite + React + TS
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/demo/error.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from 'react';
2 | // import './index.less';
3 |
4 | const CustomError = ({ onReload }) => {
5 | const handleClick = useCallback(async () => {
6 | await onReload();
7 | }, [onReload]);
8 | return (
9 |
10 |
页面崩溃了,无法正确显示,(自定义文案)
11 |
14 |
15 | );
16 | };
17 | export { CustomError };
18 |
--------------------------------------------------------------------------------
/src/component/TryAgainPage.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from 'react';
2 | import PAGE_CRASH from '../assets/page-crash.svg';
3 | import './index.less';
4 |
5 | const TryAgainPage = ({ onReload }) => {
6 | const handleClick = useCallback(async () => {
7 | await onReload();
8 | }, [onReload]);
9 | return (
10 |
11 |

12 |
页面崩溃了,无法正确显示
13 |
16 |
17 | );
18 | };
19 | export { TryAgainPage };
20 |
--------------------------------------------------------------------------------
/src/lazyEntrance.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { loadableComponent } from './loadable';
3 | import { TryAgainPage } from './component/TryAgainPage';
4 | import { ErrorProps } from './types';
5 |
6 | interface DefaultImportedComponent {
7 | default: React.ElementType
;
8 | }
9 | type DefaultComponent
= React.ElementType
| DefaultImportedComponent
;
10 |
11 | const ErrorComponent = ({ retry }: ErrorProps) => (
12 |
13 | );
14 |
15 | export function lazyEntrance(
16 | loadFn: (props: T) => Promise>,
17 | customErrorComponent?: React.ElementType,
18 | ) {
19 | return loadableComponent(loadFn, {
20 | Loading: null,
21 | Error: customErrorComponent || ErrorComponent,
22 | });
23 | }
24 | export { ErrorComponent };
25 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "module": "commonjs",
5 | "moduleResolution": "node",
6 | "jsx": "react",
7 | "declaration": true,
8 | "pretty": true,
9 | "rootDir": "src",
10 | "sourceMap": false,
11 | "strict": true,
12 | "esModuleInterop": true,
13 | "noUnusedLocals": true,
14 | "noUnusedParameters": true,
15 | "noImplicitReturns": true,
16 | "noImplicitAny": false,
17 | "noFallthroughCasesInSwitch": true,
18 | "allowSyntheticDefaultImports": true,
19 | "outDir": "dist",
20 | "lib": [
21 | "es2018",
22 | "dom"
23 | ],
24 | "importHelpers": true
25 | },
26 | "include": ["src/**/*"],
27 | "files": ["./typings/global.d.ts"],
28 | "exclude": [
29 | "node_modules",
30 | "lib",
31 | "esm",
32 | "tests",
33 | ]
34 | }
35 |
--------------------------------------------------------------------------------
/src/loadable.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import loadable, {
3 | DefaultComponent,
4 | Options,
5 | LoadableComponentOptions,
6 | } from '@loadable/component';
7 | import withErrorBoundary from './withErrorBoundary';
8 | import { retry, MaxRetriesError } from './retry';
9 | import { ErrorProps } from './types';
10 |
11 | type LoadableComponentOptions = {
12 | timeout?: number;
13 | delay?: number;
14 | Error?: React.ElementType;
15 | retries?: number;
16 | } & Options;
17 |
18 | const RETRIES = 2;
19 |
20 | function loadableComponent(
21 | loadFn: (props: T) => Promise>,
22 | options: LoadableComponentOptions = {},
23 | ) {
24 | const { retries = RETRIES, Error } = options;
25 | const newLoadFn = (props: T) => retry(loadFn, retries, props);
26 | return withErrorBoundary(loadable(newLoadFn), {
27 | Error,
28 | });
29 | }
30 |
31 | export { loadableComponent, retry, MaxRetriesError };
32 |
--------------------------------------------------------------------------------
/src/retry.ts:
--------------------------------------------------------------------------------
1 | export class MaxRetriesError {
2 | readonly name = 'MaxRetriesError';
3 |
4 | readonly stack?: string;
5 |
6 | readonly message: string;
7 |
8 | constructor(message?: string) {
9 | const error = new Error(message);
10 | this.stack = error.stack;
11 | this.message = error.message;
12 | }
13 | }
14 |
15 | export async function retry(
16 | fn: (props?: any) => Promise,
17 | retriesLeft: number = 2,
18 | props?: P,
19 | interval: number = 500,
20 | exponential: boolean = true,
21 | ): Promise {
22 | try {
23 | const val = await fn(props);
24 | return val;
25 | } catch (error) {
26 | if (retriesLeft) {
27 | await new Promise((r) => setTimeout(r, interval));
28 | return retry(
29 | fn,
30 | retriesLeft - 1,
31 | props,
32 | exponential ? interval * 2 : interval,
33 | exponential,
34 | );
35 | }
36 | throw new MaxRetriesError();
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 trigkit4
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/component/index.less:
--------------------------------------------------------------------------------
1 | .error-wrap {
2 | position: relative;
3 | top: 50%;
4 | transform: translate(0, 100%);
5 | bottom: 0;
6 | left: 0;
7 | right: 0;
8 | display: flex;
9 | flex-direction: column;
10 | justify-content: center;
11 | align-items: center;
12 | img {
13 | width: 220px;
14 | height: 280px;
15 | }
16 | .title {
17 | font-size: 14px;
18 | color: #343434;
19 | line-height: 22px;
20 | font-weight: 600;
21 | }
22 | .refresh-btn{
23 | min-width: 72px;
24 | padding-right: 16px;
25 | padding-left: 16px;
26 | color: #fff;
27 | border-color: #346fff;
28 | background: #346fff;
29 | outline: 0;
30 | position: relative;
31 | display: inline-block;
32 | font-weight: 400;
33 | white-space: nowrap;
34 | text-align: center;
35 | user-select: none;
36 | touch-action: manipulation;
37 | height: 32px;
38 | padding: 4px 15px;
39 | font-size: 14px;
40 | border-radius: 4px;
41 | border: 1px solid #dae0f0;
42 | cursor: pointer;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/demo/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from 'react-dom';
3 | import { lazyEntrance } from '../src';
4 | import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
5 | import { CustomError } from './error';
6 |
7 | enum PATH_NAME {
8 | ROOT = '/',
9 | CUSTOM_ERROR_COMPONENT = '/custom-error-component',
10 | }
11 |
12 | const CustomErrorComponent = ({ retry }: any) => (
13 |
14 | );
15 | const Apps = lazyEntrance(
16 | () =>
17 | import(
18 | /* webpackChunkName: "page.onlineScripting" */
19 | /* webpackPrefetch: true */
20 | /* webpackPreload: true */ './app'
21 | ),
22 | );
23 |
24 | const CustomErrorPage = lazyEntrance(
25 | () =>
26 | import(
27 | /* webpackChunkName: "page.onlineScripting" */
28 | /* webpackPrefetch: true */
29 | /* webpackPreload: true */ './app'
30 | ),
31 | CustomErrorComponent,
32 | );
33 |
34 | const DemoComponent = () => {
35 | return (
36 |
37 |
38 |
39 |
43 |
44 |
45 | );
46 | };
47 | export default DemoComponent;
48 |
49 | render(, document.getElementById('root'));
50 |
--------------------------------------------------------------------------------
/src/withErrorBoundary.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { LoadableComponent } from '@loadable/component';
3 | import { MaxRetriesError } from './retry';
4 | import { State, Options } from './types';
5 |
6 | export async function retry(
7 | fn: (props?: P) => Promise,
8 | retriesLeft: number = 2,
9 | props?: P,
10 | interval: number = 500,
11 | exponential: boolean = true,
12 | ): Promise {
13 | try {
14 | const val = await fn(props);
15 | return val;
16 | } catch (error) {
17 | if (retriesLeft) {
18 | await new Promise((r) => setTimeout(r, interval));
19 | return retry(
20 | fn,
21 | retriesLeft - 1,
22 | props,
23 | exponential ? interval * 2 : interval,
24 | exponential,
25 | );
26 | }
27 | throw new MaxRetriesError();
28 | }
29 | }
30 |
31 | export default function withErrorBoundary(
32 | Loadable: LoadableComponent
,
33 | options: Options,
34 | ) {
35 | return class ErrorBoundary extends React.PureComponent
{
36 | state = {
37 | hasError: false,
38 | };
39 |
40 | retry = () => {
41 | this.setState({ hasError: false });
42 | Loadable.load(this.props);
43 | };
44 |
45 | componentDidCatch(error: Error) {
46 | if (error) {
47 | this.setState({
48 | hasError: true,
49 | });
50 | }
51 | }
52 |
53 | render() {
54 | if (this.state.hasError && options.Error) {
55 | const { Error } = options;
56 | return ;
57 | }
58 |
59 | return ;
60 | }
61 | };
62 | }
63 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # lazy-import-with-error-boundary
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | Features are as below:
11 |
12 | - Support code splitting
13 | - support load on-demand
14 | - support hooks component error boundary.
15 | - Support refresh function on bottom page
16 |
17 | ### Usages
18 |
19 | 1、install
20 | ```bash
21 | $ yarn add lazy-import-with-error-boundary
22 | ```
23 |
24 | import lazyEntrance in your react router entrance:
25 |
26 | 2、Usage
27 |
28 | ```tsx
29 | import { lazyEntrance } from 'lazy-import-with-error-boundary';
30 |
31 | export const Entrance = lazyEntrance(() =>
32 | import(
33 | /* webpackChunkName: "contact.entrance" */ /* webpackPrefetch: true */ './container/Entrance'
34 | ),
35 | );
36 | ```
37 |
38 | 3、custom error component
39 |
40 | ```tsx
41 | import { CustomError } from './error';
42 |
43 | const CustomErrorComponent = ({ retry }: any) => (
44 |
45 | );
46 |
47 | const CustomErrorPage = lazyEntrance(
48 | () =>
49 | import(
50 | /* webpackChunkName: "page.onlineScripting" */
51 | /* webpackPrefetch: true */
52 | /* webpackPreload: true */ './app'
53 | ),
54 | CustomErrorComponent,
55 | );
56 | ```
57 | more details: run demo, open: http://localhost:5173/custom-error-component
58 | ### Demo
59 |
60 | ```bash
61 | $ yarn run dev
62 | ```
63 |
64 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "lazy-import-with-error-boundary",
3 | "version": "0.1.8",
4 | "description": "lazy import react hooks component with error boundary",
5 | "sideEffects": false,
6 | "files": [
7 | "dist"
8 | ],
9 | "module": "dist/esm/index.js",
10 | "types": "dist/esm/index.d.ts",
11 | "main": "dist/cjs/index.js",
12 | "typings": "dist/esm/index.d.ts",
13 | "scripts": {
14 | "dev": "vite",
15 | "build": "npx father build",
16 | "version": "npm run build",
17 | "prepublishOnly": "npm version patch && git push",
18 | "release": "npm run build && npm publish"
19 | },
20 | "husky": {
21 | "hooks": {
22 | "pre-commit": "lint-staged",
23 | "pre-push": "yarn lint && yarn clean && yarn build"
24 | }
25 | },
26 | "author": "@hawx1993",
27 | "license": "MIT",
28 | "repository": {
29 | "type": "git"
30 | },
31 | "dependencies": {
32 | "@loadable/component": "5.12.0"
33 | },
34 | "peerDependencies": {
35 | "react": "^16.8.0 || ^17.0.0 || ^18.0.0",
36 | "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
37 | },
38 | "devDependencies": {
39 | "@babel/core": "7.19.6",
40 | "@babel/preset-env": "7.19.4",
41 | "@babel/preset-typescript": "7.18.6",
42 | "@types/react": "^18.0.25",
43 | "@types/react-dom": "^18.0.9",
44 | "@vitejs/plugin-react": "^2.2.0",
45 | "father": "^4.1.0",
46 | "husky": "^8.0.2",
47 | "less": "^4.1.3",
48 | "less-loader": "^11.1.0",
49 | "lint-staged": "^13.0.3",
50 | "react": "^18.2.0",
51 | "react-dom": "^18.2.0",
52 | "react-router": "^5.2.0",
53 | "react-router-dom": "^5.2.0",
54 | "vite": "^3.2.4"
55 | },
56 | "config": {
57 | "commitizen": {
58 | "path": "git-cz"
59 | }
60 | },
61 | "lint-staged": {
62 | "src/**/**/*.{ts,tsx}": [
63 | "eslint --fix",
64 | "git add"
65 | ]
66 | },
67 | "volta": {
68 | "node": "14.18.2",
69 | "yarn": "1.22.17"
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/assets/page-crash.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------