├── src ├── index.ts ├── loadable.ts ├── core │ └── lazily.ts └── loadable │ └── loadable.ts ├── .gitignore ├── tsconfig.json ├── babel.config.js ├── tests ├── loadable.test.tsx └── lazily.test.tsx ├── .size-snapshot.json ├── LICENSE ├── rollup.config.js ├── README.md └── package.json /src/index.ts: -------------------------------------------------------------------------------- 1 | export { lazily } from './core/lazily' 2 | -------------------------------------------------------------------------------- /src/loadable.ts: -------------------------------------------------------------------------------- 1 | export { loadable } from './loadable/loadable' 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules 3 | .pnp 4 | .pnp.js 5 | 6 | # testing 7 | coverage 8 | 9 | # production 10 | dist 11 | build 12 | 13 | # dotenv environment variables file 14 | .env 15 | .env.local 16 | .env.development.local 17 | .env.test.local 18 | .env.production.local 19 | 20 | # logs 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | # misc 26 | .DS_Store 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "strict": true, 5 | "jsx": "preserve", 6 | "allowSyntheticDefaultImports": true, 7 | "esModuleInterop": true, 8 | "moduleResolution": "node", 9 | "baseUrl": ".", 10 | "paths": { 11 | "react-lazily": ["./src/index.ts"] 12 | } 13 | }, 14 | "include": ["src/**/*", "tests/**/*"], 15 | "exclude": ["node_modules", "dist"] 16 | } 17 | -------------------------------------------------------------------------------- /src/core/lazily.ts: -------------------------------------------------------------------------------- 1 | import { lazy } from 'react' 2 | 3 | export const lazily = ( 4 | loader: (x?: string) => Promise 5 | ) => 6 | new Proxy(({} as unknown) as T, { 7 | get: (target, componentName: string | symbol) => { 8 | if (typeof componentName === 'string') { 9 | return lazy(() => 10 | loader(componentName).then((x) => ({ 11 | default: (x[componentName as U] as any) as React.ComponentType, 12 | })) 13 | ) 14 | } 15 | }, 16 | }) 17 | -------------------------------------------------------------------------------- /src/loadable/loadable.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore does not provide TypeScript definitions 2 | import load from '@loadable/component' 3 | 4 | // see https://loadable-components.com/docs/babel-plugin/#loadable-detection 5 | export const loadable = ( 6 | loader: () => Promise, 7 | opts?: any 8 | ) => 9 | new Proxy(({} as unknown) as T, { 10 | get: (target, componentName: U) => { 11 | if (typeof componentName === 'string') { 12 | return load(() => loader().then((x) => x[componentName]), opts) 13 | } 14 | }, 15 | }) 16 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = (api, targets) => { 2 | // https://babeljs.io/docs/en/config-files#config-function-api 3 | const isTestEnv = api.env('test') 4 | 5 | return { 6 | babelrc: false, 7 | ignore: ['./node_modules'], 8 | presets: [ 9 | [ 10 | '@babel/preset-env', 11 | { 12 | loose: true, 13 | modules: isTestEnv ? 'commonjs' : false, 14 | targets: isTestEnv ? { node: 'current' } : targets, 15 | }, 16 | ], 17 | ], 18 | plugins: [ 19 | '@babel/plugin-transform-react-jsx', 20 | ['@babel/plugin-transform-typescript', { isTSX: true }], 21 | ], 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/loadable.test.tsx: -------------------------------------------------------------------------------- 1 | import React, { Suspense } from 'react' 2 | import { fireEvent, render } from '@testing-library/react' 3 | import { loadable } from '../src/loadable' 4 | 5 | it('Shows loading for some random component', async () => { 6 | const f = jest.fn() 7 | const { Component } = loadable(() => new Promise((r) => f(r)), { 8 | fallback: 'Loading...', 9 | }) 10 | 11 | const App: React.FC = () => { 12 | const [open, setOpen] = React.useReducer(() => true, false) 13 | return <>{open ? : } 14 | } 15 | 16 | const { findByText, getByText } = render() 17 | 18 | fireEvent.click(getByText('Load')) 19 | await findByText('Loading...') 20 | 21 | expect(f).toBeCalledTimes(1) 22 | }) 23 | -------------------------------------------------------------------------------- /tests/lazily.test.tsx: -------------------------------------------------------------------------------- 1 | import React, { Suspense } from 'react' 2 | import { fireEvent, render } from '@testing-library/react' 3 | import { lazily } from '../src/index' 4 | 5 | it('Shows loading for some random component', async () => { 6 | const f = jest.fn() 7 | const { Component } = lazily(() => new Promise((r) => f(r))) 8 | 9 | const App: React.FC = () => { 10 | const [open, setOpen] = React.useReducer(() => true, false) 11 | return ( 12 | <> 13 | {open ? ( 14 | Loading...}> 15 | 16 | 17 | ) : ( 18 | 19 | )} 20 | 21 | ) 22 | } 23 | 24 | const { findByText, getByText } = render() 25 | 26 | fireEvent.click(getByText('Load')) 27 | await findByText('Loading...') 28 | 29 | expect(f).toBeCalledTimes(1) 30 | }) 31 | -------------------------------------------------------------------------------- /.size-snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "index.js": { 3 | "bundled": 291, 4 | "minified": 154, 5 | "gzipped": 147, 6 | "treeshaked": { 7 | "rollup": { 8 | "code": 14, 9 | "import_statements": 14 10 | }, 11 | "webpack": { 12 | "code": 998 13 | } 14 | } 15 | }, 16 | "index.iife.js": { 17 | "bundled": 642, 18 | "minified": 281, 19 | "gzipped": 201 20 | }, 21 | "loadable.js": { 22 | "bundled": 332, 23 | "minified": 165, 24 | "gzipped": 150, 25 | "treeshaked": { 26 | "rollup": { 27 | "code": 28, 28 | "import_statements": 28 29 | }, 30 | "webpack": { 31 | "code": 1012 32 | } 33 | } 34 | }, 35 | "index.cjs.js": { 36 | "bundled": 517, 37 | "minified": 288, 38 | "gzipped": 209 39 | }, 40 | "loadable.cjs.js": { 41 | "bundled": 746, 42 | "minified": 450, 43 | "gzipped": 272 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 JLarky 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 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import babel from '@rollup/plugin-babel' 3 | import resolve from '@rollup/plugin-node-resolve' 4 | import typescript from '@rollup/plugin-typescript' 5 | import { sizeSnapshot } from 'rollup-plugin-size-snapshot' 6 | 7 | const createBabelConfig = require('./babel.config') 8 | 9 | const { root } = path.parse(process.cwd()) 10 | const external = (id) => !id.startsWith('.') && !id.startsWith(root) 11 | const extensions = ['.js', '.ts', '.tsx'] 12 | const getBabelOptions = (targets) => ({ 13 | ...createBabelConfig({ env: (env) => env === 'build' }, targets), 14 | extensions, 15 | }) 16 | 17 | function createDeclarationConfig(input, output) { 18 | return { 19 | input, 20 | output: { 21 | dir: output, 22 | }, 23 | external, 24 | plugins: [typescript({ declaration: true, outDir: output })], 25 | } 26 | } 27 | 28 | function createESMConfig(input, output) { 29 | return { 30 | input, 31 | output: { file: output, format: 'esm' }, 32 | external, 33 | plugins: [ 34 | resolve({ extensions }), 35 | typescript(), 36 | babel(getBabelOptions({ node: 8 })), 37 | sizeSnapshot(), 38 | ], 39 | } 40 | } 41 | 42 | function createCommonJSConfig(input, output) { 43 | return { 44 | input, 45 | output: { file: output, format: 'cjs', exports: 'named' }, 46 | external, 47 | plugins: [ 48 | resolve({ extensions }), 49 | typescript(), 50 | babel(getBabelOptions({ ie: 11 })), 51 | sizeSnapshot(), 52 | ], 53 | } 54 | } 55 | 56 | function createIIFEConfig(input, output, globalName) { 57 | return { 58 | input, 59 | output: { 60 | file: output, 61 | format: 'iife', 62 | exports: 'named', 63 | name: globalName, 64 | globals: { 65 | react: 'React', 66 | }, 67 | }, 68 | external, 69 | plugins: [ 70 | resolve({ extensions }), 71 | typescript(), 72 | babel(getBabelOptions({ ie: 11 })), 73 | sizeSnapshot(), 74 | ], 75 | } 76 | } 77 | export default (args) => 78 | args['config-cjs'] 79 | ? [ 80 | createCommonJSConfig('src/index.ts', 'dist/index.cjs.js'), 81 | createCommonJSConfig('src/loadable.ts', 'dist/loadable.cjs.js'), 82 | ] 83 | : [ 84 | createDeclarationConfig('src/index.ts', 'dist'), 85 | createESMConfig('src/index.ts', 'dist/index.js'), 86 | createIIFEConfig('src/index.ts', 'dist/index.iife.js', 'reactLazily'), 87 | createESMConfig('src/loadable.ts', 'dist/loadable.js'), 88 | ] 89 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-lazily 2 | 3 | [![minzip size](https://badgen.deno.dev/bundlephobia/minzip/react-lazily)](https://bundlephobia.com/result?p=react-lazily) 4 | [![install size](https://badgen.deno.dev/packagephobia/install/react-lazily)](https://packagephobia.com/result?p=react-lazily) 5 | [![dependency count](https://badgen.deno.dev/bundlephobia/dependency-count/react-lazily)](https://bundlephobia.com/result?p=react-lazily) 6 | 7 | `react-lazily` is a simple wrapper around `React.lazy` (or `loadable` from `@loadable/component`) that supports named imports. 8 | 9 | ## Usage 10 | 11 | Consider a component `MyComponent` that is exported with a default export: 12 | 13 | ```ts 14 | export default MyComponent; 15 | ``` 16 | 17 | Per React docs, you could use `React.lazy` as follows: 18 | 19 | ```ts 20 | const MyComponent = React.lazy(() => import('./MyComponent')); 21 | ``` 22 | 23 | However, if the component is exported with a named export: 24 | 25 | ```ts 26 | export const MyComponent = ... 27 | ``` 28 | 29 | You would have to use `React.lazy` like this: 30 | 31 | 32 | 33 | But if the component is exported with named export `export const MyComponent = ...` then you have to do: 34 | 35 | ```ts 36 | const MyComponent = React.lazy(() => 37 | import('./MyComponent').then((module) => ({ default: module.MyComponent })) 38 | ); 39 | ``` 40 | 41 | With `react-lazily` it becomes: 42 | 43 | ```ts 44 | const { MyComponent } = lazily(() => import('./MyComponent')); 45 | ``` 46 | 47 | ## Full example 48 | 49 | See the live example: https://codesandbox.io/s/react-lazily-example-p7hyj 50 | 51 | ```ts 52 | import React, { Suspense } from 'react'; 53 | import { lazily } from 'react-lazily'; 54 | 55 | const { MyComponent } = lazily(() => import('./MyComponent')); 56 | 57 | const App = () => { 58 | const [open, setOpen] = React.useReducer(() => true, false); 59 | 60 | return ( 61 | <> 62 | {open ? ( 63 | Loading...}> 64 | 65 | 66 | ) : ( 67 | 68 | )} 69 | 70 | ); 71 | }; 72 | ``` 73 | 74 | ## Full Example with @loadable/component 75 | 76 | Don't forget to install `@loadable/component` first. 77 | 78 | ```ts 79 | import React from 'react'; 80 | import { loadable } from 'react-lazily/loadable'; 81 | 82 | const { MyComponent } = loadable(() => import('./MyComponent'), { 83 | fallback:
Loading...
, 84 | }); 85 | 86 | const App = () => { 87 | const [open, setOpen] = React.useReducer(() => true, false); 88 | 89 | return ( 90 | <> 91 | {open ? : } 92 | 93 | ); 94 | }; 95 | ``` 96 | 97 | ## Related 98 | 99 | - [Allow for named imports in React.lazy (GitHub Issue)](https://github.com/facebook/react/issues/14603#issuecomment-726551598) 100 | - [Can you deconstruct lazily loaded React components? (Stack Overflow)](https://stackoverflow.com/a/61879800/74167) 101 | - [solidjs-lazily (NPM Package)](https://www.npmjs.com/package/solidjs-lazily) 102 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-lazily", 3 | "private": true, 4 | "version": "0.9.2", 5 | "description": "React.lazy wrapper that works with allows you to destruct imported module, so it will work with non default exports", 6 | "main": "index.cjs.js", 7 | "module": "index.js", 8 | "types": "index.d.ts", 9 | "files": [ 10 | "**" 11 | ], 12 | "sideEffects": false, 13 | "scripts": { 14 | "prebuild": "shx rm -rf dist", 15 | "build": "rollup -c --config-esm && rollup -c --config-cjs", 16 | "postbuild": "yarn copy", 17 | "eslint": "eslint --fix '{src,tests}/**/*.{js,ts,jsx,tsx}'", 18 | "eslint:ci": "eslint '{src,tests}/**/*.{js,ts,jsx,tsx}'", 19 | "prepare": "yarn build", 20 | "pretest": "tsc --noEmit", 21 | "test": "jest", 22 | "test:dev": "jest --watch --no-coverage", 23 | "test:coverage:watch": "jest --watch", 24 | "copy": "shx mv dist/src/* dist && shx rm -rf dist/{src,tests} && shx cp dist/loadable.d.ts dist/loadable.cjs.d.ts && copyfiles -f package.json README.md LICENSE dist && json -I -f dist/package.json -e \"this.private=false; this.devDependencies=undefined; this.optionalDependencies=undefined; this.scripts=undefined; this.husky=undefined; this.prettier=undefined; this.jest=undefined; this['lint-staged']=undefined;\"" 25 | }, 26 | "husky": { 27 | "hooks": { 28 | "pre-commit": "lint-staged" 29 | } 30 | }, 31 | "prettier": { 32 | "semi": false, 33 | "trailingComma": "es5", 34 | "singleQuote": true, 35 | "jsxBracketSameLine": true, 36 | "tabWidth": 2, 37 | "printWidth": 80 38 | }, 39 | "lint-staged": { 40 | "*.{js,ts,tsx}": [ 41 | "prettier --write" 42 | ] 43 | }, 44 | "repository": { 45 | "type": "git", 46 | "url": "git+https://github.com/JLarky/react-lazily.git" 47 | }, 48 | "keywords": [ 49 | "react", 50 | "lazy", 51 | "default export" 52 | ], 53 | "author": "JLarky", 54 | "contributors": [], 55 | "license": "MIT", 56 | "bugs": { 57 | "url": "https://github.com/JLarky/react-lazily/issues" 58 | }, 59 | "homepage": "https://github.com/JLarky/react-lazily", 60 | "jest": { 61 | "rootDir": ".", 62 | "moduleNameMapper": { 63 | "^react-lazily$": "/src/index.ts" 64 | }, 65 | "modulePathIgnorePatterns": [ 66 | "dist" 67 | ], 68 | "testRegex": "test.(js|ts|tsx)$", 69 | "coverageDirectory": "./coverage/", 70 | "collectCoverage": true, 71 | "coverageReporters": [ 72 | "json", 73 | "html", 74 | "text", 75 | "text-summary" 76 | ], 77 | "collectCoverageFrom": [ 78 | "src/**/*.{js,ts,tsx}", 79 | "tests/**/*.{js,ts,tsx}" 80 | ] 81 | }, 82 | "dependencies": {}, 83 | "devDependencies": { 84 | "@babel/core": "^7.12.7", 85 | "@babel/plugin-transform-react-jsx": "^7.12.7", 86 | "@babel/plugin-transform-typescript": "^7.12.1", 87 | "@babel/preset-env": "^7.12.7", 88 | "@loadable/component": "^5.14.1", 89 | "@rollup/plugin-babel": "^5.2.1", 90 | "@rollup/plugin-node-resolve": "^10.0.0", 91 | "@rollup/plugin-typescript": "^6.1.0", 92 | "@testing-library/react": "^11.2.2", 93 | "@types/jest": "^26.0.15", 94 | "@types/react": "^17.0.0", 95 | "@types/react-dom": "^17.0.0", 96 | "@types/scheduler": "^0.16.1", 97 | "@typescript-eslint/eslint-plugin": "^4.8.1", 98 | "@typescript-eslint/parser": "^4.8.1", 99 | "copyfiles": "^2.4.0", 100 | "eslint": "^7.14.0", 101 | "eslint-config-prettier": "^6.15.0", 102 | "eslint-import-resolver-alias": "^1.1.2", 103 | "eslint-plugin-import": "^2.22.1", 104 | "eslint-plugin-jest": "^24.1.3", 105 | "eslint-plugin-prettier": "^3.1.4", 106 | "eslint-plugin-react": "^7.21.5", 107 | "eslint-plugin-react-hooks": "^4.2.0", 108 | "husky": "^4.3.0", 109 | "jest": "^26.6.3", 110 | "json": "^10.0.0", 111 | "lint-staged": "^10.5.1", 112 | "prettier": "^2.2.0", 113 | "react": "^17.0.1", 114 | "react-dom": "^17.0.1", 115 | "rollup": "^2.33.3", 116 | "rollup-plugin-size-snapshot": "^0.12.0", 117 | "shx": "^0.3.3", 118 | "typescript": "^4.1.2" 119 | }, 120 | "peerDependencies": { 121 | "react": ">=16.8", 122 | "react-dom": "*", 123 | "react-native": "*" 124 | }, 125 | "peerDependenciesMeta": { 126 | "react-dom": { 127 | "optional": true 128 | }, 129 | "react-native": { 130 | "optional": true 131 | } 132 | } 133 | } 134 | --------------------------------------------------------------------------------