├── .babelrc.js ├── .circleci └── config.yml ├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── LICENSE ├── README.md ├── build.js ├── package-lock.json ├── package.json ├── src ├── Lazy.js ├── context.js ├── decorator.js ├── index.js └── intersectionListener.js └── test └── index.spec.js /.babelrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { 6 | targets: { 7 | browsers: ['ie >= 11'] 8 | }, 9 | modules: process.env.NODE_ENV === 'test' ? 'commonjs' : false, 10 | loose: true 11 | } 12 | ], 13 | '@babel/preset-react' 14 | ], 15 | plugins: ['@babel/plugin-proposal-object-rest-spread'] 16 | }; 17 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | test: 4 | docker: 5 | - image: circleci/node:8-browsers 6 | steps: 7 | - checkout 8 | - run: sudo npm install json -g 9 | - run: json -f package.json -e 'this.version="0.0.0"' > .package.json 10 | - restore_cache: 11 | keys: 12 | - node-cache-{{ checksum ".package.json" }}-{{ .Branch }} 13 | - node-cache-{{ checksum ".package.json" }} 14 | - node-cache 15 | - run: npm install 16 | - run: npm test 17 | - save_cache: 18 | key: node-cache-{{ checksum ".package.json" }}-{{ .Branch }} 19 | paths: 20 | - node_modules 21 | release: 22 | docker: 23 | - image: circleci/node:8-browsers 24 | steps: 25 | - checkout 26 | - run: sudo npm install json -g 27 | - run: npm install 28 | - run: 29 | name: Publish 30 | command: | 31 | if [ "$(git describe --abbrev=0 --tags)" != "v$(json -f package.json version)" ]; then 32 | git tag v`json -f package.json version`; 33 | git push origin --tags; 34 | echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc 35 | npm publish; 36 | fi 37 | prerelease: 38 | docker: 39 | - image: circleci/node:8-browsers 40 | steps: 41 | - checkout 42 | - run: sudo npm install -g json 43 | - run: npm install 44 | - run: 45 | name: Publish 46 | command: | 47 | if [ "$(git describe --abbrev=0 --tags)" != "v$(json -f package.json version)" ]; then 48 | git tag v`json -f package.json version`; 49 | git push origin --tags; 50 | echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc 51 | npm publish --tag next; 52 | fi 53 | workflows: 54 | version: 2 55 | build_and_publish: 56 | jobs: 57 | - test 58 | - release: 59 | type: approval 60 | requires: 61 | - test 62 | filters: 63 | branches: 64 | only: master 65 | - approval-release: 66 | type: approval 67 | requires: 68 | - test 69 | filters: 70 | branches: 71 | only: master 72 | - release: 73 | requires: 74 | - approval-release 75 | - approval-prerelease: 76 | type: approval 77 | requires: 78 | - test 79 | filters: 80 | branches: 81 | only: next 82 | - prerelease: 83 | requires: 84 | - approval-prerelease 85 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | indent_style = space 3 | end_of_line = lf 4 | indent_size = 2 5 | charset = utf-8 6 | trim_trailing_whitespace = true 7 | 8 | [Makefile] 9 | indent_style = tab 10 | indent_size = 8 11 | 12 | [*.md] 13 | max_line_length = 0 14 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | lib 2 | dist 3 | node_modules 4 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": [ 4 | "airbnb", 5 | "prettier" 6 | ], 7 | "parserOptions": { 8 | "sourceType": "module", 9 | "ecmaFeatures": { 10 | "jsx": true 11 | } 12 | }, 13 | "env": { 14 | "es6": true, 15 | "node": true, 16 | "browser": true 17 | }, 18 | "plugins": [ 19 | "markdown" 20 | ], 21 | "rules": { 22 | "react/jsx-filename-extension": 0, 23 | "react/forbid-prop-types": 0 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | .DS_Store 4 | dist/ 5 | lib/ 6 | es/ 7 | .tern-port 8 | .package.json -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | lib/ 2 | es/ -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 - 2017 HOU Bin 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | rrr-lazy 2 | ========================= 3 | 4 | Lazy load component with react && react-router. 5 | 6 | [![CircleCI](https://circleci.com/gh/kouhin/rrr-lazy/tree/master.svg?style=svg)](https://circleci.com/gh/kouhin/rrr-lazy/tree/master) 7 | [![dependency status](https://david-dm.org/kouhin/rrr-lazy.svg?style=flat-square)](https://david-dm.org/kouhin/rrr-lazy) 8 | 9 | ## Installationg 10 | rrr-lazy requires **React 16.2.0 or later.** 11 | 12 | For npm: 13 | ``` 14 | npm install rrr-lazy 15 | ``` 16 | 17 | For yarn: 18 | 19 | ``` 20 | yarn add rrr-lazy 21 | ``` 22 | 23 | IntersectionObserver is required by this library. You can use this polyfill for old browsers https://github.com/w3c/IntersectionObserver/tree/master/polyfill 24 | 25 | ## Usage 26 | 27 | ### Use as a common lazy component 28 | 29 | ```javascript 30 | import React from 'react'; 31 | import { Lazy } from 'rrr-lazy'; 32 | 33 | const MyComponent = () => ( 34 |
35 | ( 38 | if (status === 'unload') { 39 | return
Unload
40 | } 41 | if (status === 'loading') { 42 | return
Loading
43 | } 44 | if (status === 'loaded') { 45 | return ( 46 | ')}`} 49 | /> 50 | ); 51 | } 52 | throw new Error('Unknown status'); 53 | )} 54 | /> 55 |
56 | ); 57 | ``` 58 | 59 | ### Loading data or do something else with lifecycle hooks 60 | 61 | ```jsx 62 | import { lazy } from 'rrr-lazy'; 63 | 64 | async function onLoading() { 65 | // Loading data; 66 | // ... 67 | return data; 68 | } 69 | async function onLoaded() { 70 | // Do something on loaded 71 | // ... 72 | return result; 73 | } 74 | 75 | async function onUnload() { 76 | // Do something on unload 77 | // ... 78 | return result; 79 | } 80 | 81 | async function onError(error) { 82 | // Do something on error 83 | console.error(error); 84 | // ... 85 | return result; 86 | } 87 | 88 | class App extends React.Component { 89 | render() { 90 | // the matched child route components become props in the parent 91 | return ( 92 | ( 95 | if (status === 'unload') { 96 | return
Unload
97 | } 98 | if (status === 'loading') { 99 | return
Loading
100 | } 101 | if (status === 'loaded') { 102 | return ( 103 | ')}`} 106 | /> 107 | ); 108 | } 109 | throw new Error('Unknown status'); 110 | )} 111 | onLoading={onLoading} 112 | onLoaded={onLoaded} 113 | onUnload={onUnload} 114 | onError={onError} 115 | /> 116 | ) 117 | } 118 | } 119 | ``` 120 | 121 | ## API: `` 122 | 123 | ### Props 124 | 125 | #### `root` 126 | Type: `String|HTMLElement` Default: `null` 127 | 128 | This value will be used as root for IntersectionObserver (See [root](https://www.w3.org/TR/intersection-observer/#dom-intersectionobserver-root). 129 | 130 | #### `rootMargin` 131 | Type: `String` Default: `null` 132 | 133 | This value will be used as rootMargin for IntersectionObserver (See [rootMargin](https://www.w3.org/TR/intersection-observer/#dom-intersectionobserverinit-rootmargin). 134 | 135 | #### `render(status, props)` 136 | Type: `Function` **Required** 137 | 138 | `status` can be `unload`, `loading`, `loaded`. 139 | 140 | `props` are props that passed from `Lazy`. This is designed for `@lazy`, and when you use `` component, you may not need it. 141 | 142 | #### `loaderComponent` 143 | Type: `string` Default: `div` 144 | 145 | #### `laoderProps` 146 | Type: `string` Default: `{}` 147 | 148 | `loaderComponent` and `loaderProps` is used to create a LoaderComponent when status is `unload` or `loaded`. 149 | 150 | The result of `render(status, props)` will be passed to LoaderComponent as `children`. 151 | 152 | #### onError() 153 | 154 | #### onLoaded() 155 | 156 | #### onLoading() 157 | 158 | #### onUnload() 159 | 160 | ## API: `@lazy` 161 | 162 | Usage: 163 | 164 | ``` javascript 165 | @routerHooks({ 166 | fetch: async () => { 167 | await fetchData(); 168 | }, 169 | defer: async () => { 170 | await fetchDeferredData(); 171 | }, 172 | }) 173 | @lazy({ 174 | render: (status, props, Component) => { 175 | if (status === 'unload') { 176 | return
Unload
; 177 | } else if (status === 'loading') { 178 | return
Loading
; 179 | } else { 180 | return ; 181 | } 182 | }, 183 | onLoaded: () => console.log('look ma I have been lazyloaded!') 184 | }) 185 | class MyComponent extends React.Component { 186 | render() { 187 | return ( 188 |
189 | 190 |
191 | ); 192 | } 193 | } 194 | ``` 195 | 196 | Or 197 | 198 | ``` javascript 199 | class MyComponent extends React.Component { 200 | render() { 201 | return ( 202 |
203 | 204 |
205 | ); 206 | } 207 | } 208 | const myComponent = lazy({ 209 | render: (status, props, Component) => { 210 | if (status === 'unload') { 211 | return
Unload
; 212 | } else if (status === 'loading') { 213 | return
Loading
; 214 | } else { 215 | return ; 216 | } 217 | }, 218 | onLoaded: () => console.log('look ma I have been lazyloaded!') 219 | })(MyComponent); 220 | ``` 221 | 222 | ### options 223 | 224 | #### getComponent 225 | 226 | With webpack 2 import() 227 | 228 | ``` javascript 229 | const myComponent = lazy({ 230 | render: (status, props, Component) => { 231 | if (status === 'unload') { 232 | return
Unload
; 233 | } else if (status === 'loading') { 234 | return
Loading
; 235 | } else { 236 | return ; 237 | } 238 | }, 239 | onLoaded: () => console.log('look ma I have been lazyloaded!'), 240 | getComponent: () => import('./MyComponent'), 241 | })(); 242 | ``` 243 | 244 | ## API: LazyProvider 245 | 246 | You can optionally use `LazyProvider` and pass a version (such as location) to the `value` prop. 247 | All of the Lazy instances will be reset when version changed. 248 | 249 | Example: 250 | 251 | ``` javascript 252 | class Application extends React.Component { 253 | render() { 254 | // when pathname changed, all of the Lazy instances will be reset. 255 | const { pathname, children } = this.state; 256 | 257 | { children } 258 | 259 | } 260 | } 261 | ``` 262 | 263 | ## LICENSE 264 | 265 | MIT 266 | -------------------------------------------------------------------------------- /build.js: -------------------------------------------------------------------------------- 1 | /* eslint import/no-extraneous-dependencies: ["error", {"devDependencies": true}] */ 2 | const rollup = require('rollup'); 3 | const babel = require('rollup-plugin-babel'); 4 | const { uglify } = require('rollup-plugin-uglify'); 5 | const replace = require('rollup-plugin-replace'); 6 | const commonjs = require('rollup-plugin-commonjs'); 7 | const resolve = require('rollup-plugin-node-resolve'); 8 | const autoExternal = require('rollup-plugin-auto-external'); 9 | 10 | const pkg = require('./package.json'); 11 | 12 | const NAME = pkg.name 13 | .split(/-|_/) 14 | .filter(x => x) 15 | .map(s => s.charAt(0).toUpperCase() + s.slice(1)) 16 | .join(''); 17 | 18 | function createOptions(format, outputPath, minify) { 19 | return { 20 | inputOptions: { 21 | input: pkg.source, 22 | plugins: [ 23 | babel({ 24 | exclude: 'node_modules/**' 25 | }), 26 | resolve({ 27 | jsnext: true, 28 | main: true, 29 | browser: format === 'umd' 30 | }), 31 | commonjs({ 32 | include: /node_modules/ 33 | }), 34 | replace({ 35 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV) 36 | }), 37 | format === 'umd' ? null : autoExternal(), 38 | minify 39 | ? uglify({ 40 | compress: { 41 | pure_getters: true, 42 | unsafe: true, 43 | unsafe_comps: true, 44 | warnings: false 45 | } 46 | }) 47 | : null 48 | ].filter(x => x) 49 | }, 50 | outputOptions: { 51 | file: outputPath, 52 | format, 53 | name: NAME, 54 | indent: false, 55 | exports: 'named', 56 | globals: { 57 | react: 'React' 58 | } 59 | } 60 | }; 61 | } 62 | 63 | function generateMinPath(p) { 64 | const min = '.min'; 65 | const pos = p.lastIndexOf('.'); 66 | if (pos === -1) return `${p}${min}`; 67 | return `${p.substr(0, pos)}${min}${p.substr(pos)}`; 68 | } 69 | 70 | (async () => { 71 | const { module, main, 'umd:main': umd } = pkg; 72 | await Promise.all( 73 | [ 74 | { format: 'es', outputPath: module }, 75 | { format: 'cjs', outputPath: main }, 76 | { format: 'umd', outputPath: umd }, 77 | { format: 'umd', outputPath: generateMinPath(umd), minify: true } 78 | ] 79 | .filter(o => o.outputPath) 80 | .map(async build => { 81 | const { format, outputPath, minify } = build; 82 | const { inputOptions, outputOptions } = createOptions( 83 | format, 84 | outputPath, 85 | minify 86 | ); 87 | const bundle = await rollup.rollup(inputOptions); 88 | await bundle.write(outputOptions); 89 | }) 90 | ); 91 | })(); 92 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rrr-lazy", 3 | "version": "4.1.0", 4 | "description": "Lazy load component with react && react-router.", 5 | "source": "src/index.js", 6 | "module": "dist/rrr-lazy.esm.js", 7 | "main": "dist/rrr-lazy.js", 8 | "umd:main": "dist/rrr-lazy.umd.js", 9 | "scripts": { 10 | "build": "node build.js", 11 | "clean": "rimraf dist", 12 | "lint": "eslint src test --ext .js --ext .md", 13 | "prepare": "npm-run-all -s clean lint test build", 14 | "test": "npm run lint" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/kouhin/rrr-lazy.git" 19 | }, 20 | "files": [ 21 | "src", 22 | "dist" 23 | ], 24 | "keywords": [ 25 | "react", 26 | "react-router", 27 | "load", 28 | "lazy", 29 | "lazyload", 30 | "react-router-hook", 31 | "reactjs", 32 | "intersection", 33 | "observer" 34 | ], 35 | "author": "Bin Hou (https://twitter.com/houbin217jz)", 36 | "license": "MIT", 37 | "devDependencies": { 38 | "@babel/cli": "^7.1.5", 39 | "@babel/core": "^7.1.6", 40 | "@babel/node": "^7.0.0", 41 | "@babel/plugin-proposal-object-rest-spread": "^7.0.0", 42 | "@babel/polyfill": "^7.0.0", 43 | "@babel/preset-env": "^7.1.6", 44 | "@babel/preset-react": "^7.0.0", 45 | "@babel/register": "^7.0.0", 46 | "babel-eslint": "^10.0.1", 47 | "eslint": "^5.9.0", 48 | "eslint-config-airbnb": "^17.1.0", 49 | "eslint-config-prettier": "^3.3.0", 50 | "eslint-plugin-import": "^2.14.0", 51 | "eslint-plugin-jsx-a11y": "^6.1.2", 52 | "eslint-plugin-markdown": "^1.0.0-rc.0", 53 | "eslint-plugin-react": "^7.11.1", 54 | "jest": "^23.6.0", 55 | "npm-run-all": "^4.1.5", 56 | "react": "^16.6.3", 57 | "react-dom": "^16.6.3", 58 | "rimraf": "^2.6.2", 59 | "rollup": "^0.67.3", 60 | "rollup-plugin-auto-external": "^2.0.0", 61 | "rollup-plugin-babel": "^4.0.3", 62 | "rollup-plugin-commonjs": "^9.2.0", 63 | "rollup-plugin-node-resolve": "^3.4.0", 64 | "rollup-plugin-replace": "^2.1.0", 65 | "rollup-plugin-uglify": "^6.0.0" 66 | }, 67 | "dependencies": { 68 | "create-react-context": "^0.2.3", 69 | "hoist-non-react-statics": "^3.2.0", 70 | "prop-types": "^15.6.2", 71 | "react-fast-compare": "^2.0.4" 72 | }, 73 | "peerDependencies": { 74 | "react": ">=16.2.0", 75 | "react-dom": ">=16.2.0" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Lazy.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import isDeepEqual from 'react-fast-compare'; 4 | 5 | import { LazyConsumer } from './context'; 6 | import createIntersectionListener from './intersectionListener'; 7 | 8 | const Status = { 9 | Unload: 'unload', 10 | Loading: 'loading', 11 | Loaded: 'loaded' 12 | }; 13 | 14 | const VERSION_PROP = '@@rrr-lazy/Version'; 15 | 16 | class Lazy extends React.Component { 17 | constructor(props) { 18 | super(props); 19 | this.startListen = this.startListen.bind(this); 20 | this.stopListen = this.stopListen.bind(this); 21 | this.enterViewport = this.enterViewport.bind(this); 22 | 23 | const version = props[VERSION_PROP]; 24 | this.state = { 25 | version, // eslint-disable-line react/no-unused-state 26 | status: Status.Unload 27 | }; 28 | } 29 | 30 | static getDerivedStateFromProps(props, state) { 31 | const version = props[VERSION_PROP]; 32 | if (version !== state.version) { 33 | return { 34 | version, 35 | status: Status.Unload 36 | }; 37 | } 38 | return {}; 39 | } 40 | 41 | componentDidMount() { 42 | this.startListen(); 43 | } 44 | 45 | shouldComponentUpdate(nextProps, nextState) { 46 | return !isDeepEqual(this.props, nextProps) || !isDeepEqual(this.state, nextState); 47 | } 48 | 49 | componentDidUpdate(prevProps, prevState) { 50 | const { status } = this.state; 51 | const { onUnload } = this.props; 52 | if (status !== prevState.status && status === Status.Unload) { 53 | if (onUnload) { 54 | onUnload(); 55 | } 56 | this.startListen(); 57 | } 58 | } 59 | 60 | componentWillUnmount() { 61 | this.stopListen(); 62 | if (this.node) { 63 | this.node = null; 64 | } 65 | } 66 | 67 | startListen() { 68 | this.stopListen(); 69 | const { root, rootMargin } = this.props; 70 | const opts = {}; 71 | if (root) { 72 | opts.root = 73 | typeof root === 'string' ? document.querySelector(root) : root; 74 | } 75 | if (rootMargin) { 76 | opts.rootMargin = rootMargin; 77 | } 78 | const intersectionListener = createIntersectionListener(opts); 79 | if (this.node && !this.unlisten) { 80 | this.unlisten = intersectionListener.listen(this.node, entry => { 81 | if (entry.isIntersecting || entry.intersectionRatio > 0) { 82 | this.stopListen(); 83 | this.enterViewport(); 84 | } 85 | }); 86 | } 87 | } 88 | 89 | stopListen() { 90 | if (this.unlisten) { 91 | this.unlisten(); 92 | this.unlisten = null; 93 | } 94 | } 95 | 96 | enterViewport() { 97 | const { status } = this.state; 98 | const { onLoading, onLoaded, onError } = this.props; 99 | if (!this.node || status !== Status.Unload) { 100 | return null; 101 | } 102 | return Promise.resolve() 103 | .then(() => { 104 | if (!this.node) throw new Error('ABORT'); 105 | this.setState({ status: Status.Loading }); 106 | if (onLoading) { 107 | return onLoading(); 108 | } 109 | return null; 110 | }) 111 | .then(() => { 112 | if (!this.node) throw new Error('ABORT'); 113 | this.setState({ status: Status.Loaded }); 114 | if (onLoaded) { 115 | return onLoaded(); 116 | } 117 | return null; 118 | }) 119 | .catch(error => { 120 | if (error.message !== 'ABORT') { 121 | if (onError) { 122 | return onError(error); 123 | } 124 | throw error; 125 | } 126 | return null; 127 | }); 128 | } 129 | 130 | render() { 131 | const { 132 | root, 133 | rootMargin, 134 | render, 135 | loaderComponent, 136 | loaderProps = {}, 137 | onLoaded, 138 | onLoading, 139 | onUnload, 140 | onError, 141 | ...restProps 142 | } = this.props; 143 | const { status } = this.state; 144 | 145 | if (status !== Status.Loaded) { 146 | return React.createElement( 147 | loaderComponent, 148 | { 149 | ...loaderProps, 150 | ref: node => { 151 | this.node = node; 152 | } 153 | }, 154 | render(status, restProps) 155 | ); 156 | } 157 | return render(status, restProps); 158 | } 159 | } 160 | 161 | Lazy.propTypes = { 162 | [VERSION_PROP]: PropTypes.any, 163 | render: PropTypes.func.isRequired, 164 | root: PropTypes.oneOfType( 165 | [PropTypes.string].concat( 166 | typeof HTMLElement === 'undefined' 167 | ? [] 168 | : PropTypes.instanceOf(HTMLElement) 169 | ) 170 | ), 171 | rootMargin: PropTypes.string, 172 | loaderComponent: PropTypes.string, 173 | loaderProps: PropTypes.object, // eslint-disable-line react/forbid-prop-types 174 | onError: PropTypes.func, 175 | onLoaded: PropTypes.func, 176 | onLoading: PropTypes.func, 177 | onUnload: PropTypes.func 178 | }; 179 | 180 | Lazy.defaultProps = { 181 | [VERSION_PROP]: null, 182 | root: null, 183 | rootMargin: null, 184 | loaderComponent: 'div', 185 | loaderProps: null, 186 | onError: null, 187 | onLoaded: null, 188 | onLoading: null, 189 | onUnload: null 190 | }; 191 | 192 | export default function LazyWrapper(props) { 193 | return ( 194 | 195 | {version => { 196 | const passProps = { 197 | ...props, 198 | [VERSION_PROP]: version 199 | }; 200 | return ; 201 | }} 202 | 203 | ); 204 | } 205 | -------------------------------------------------------------------------------- /src/context.js: -------------------------------------------------------------------------------- 1 | import createContext from 'create-react-context'; 2 | 3 | const { Provider: LazyProvider, Consumer: LazyConsumer } = createContext(); 4 | 5 | export { LazyProvider, LazyConsumer }; 6 | -------------------------------------------------------------------------------- /src/decorator.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import hoistNonReactStatics from 'hoist-non-react-statics'; 4 | 5 | import Lazy from './Lazy'; 6 | 7 | function interopRequireDefault(obj) { 8 | // eslint-disable-next-line no-underscore-dangle 9 | return obj && obj.__esModule ? obj : { default: obj }; 10 | } 11 | 12 | function noop() {} 13 | 14 | function Empty() { 15 | return null; 16 | } 17 | 18 | class LazyDecorated extends React.PureComponent { 19 | constructor(props) { 20 | super(props); 21 | this.handleLoading = this.handleLoading.bind(this); 22 | this.renderComponent = this.renderComponent.bind(this); 23 | this.state = { 24 | Component: null 25 | }; 26 | } 27 | 28 | handleLoading() { 29 | const { getComponent, lazyProps } = this.props; 30 | const { onLoading = noop } = lazyProps; 31 | return Promise.resolve() 32 | .then(() => getComponent()) 33 | .then(c => { 34 | const Component = interopRequireDefault(c).default || Empty; 35 | this.setState({ 36 | Component 37 | }); 38 | return onLoading(); 39 | }); 40 | } 41 | 42 | renderComponent(status, props) { 43 | const { Component } = this.state; 44 | const { lazyProps } = this.props; 45 | const { render } = lazyProps; 46 | if (typeof render === 'function') { 47 | return render(status, props, Component); 48 | } 49 | if (!Component || status !== 'loaded') return null; 50 | return ; 51 | } 52 | 53 | render() { 54 | const { lazyProps, ownProps } = this.props; 55 | return ( 56 | 62 | ); 63 | } 64 | } 65 | 66 | LazyDecorated.propTypes = { 67 | getComponent: PropTypes.func.isRequired, 68 | lazyProps: PropTypes.object.isRequired, 69 | ownProps: PropTypes.object.isRequired 70 | }; 71 | 72 | const lazy = opts => WrappedComponent => { 73 | const { getComponent, ...lazyProps } = opts; 74 | const Component = ownProps => ( 75 | WrappedComponent 78 | } 79 | lazyProps={lazyProps} 80 | ownProps={ownProps} 81 | /> 82 | ); 83 | Component.WrappedComponent = WrappedComponent; 84 | hoistNonReactStatics(Component, WrappedComponent); 85 | return Component; 86 | }; 87 | 88 | export default lazy; 89 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Lazy from './Lazy'; 2 | import lazy from './decorator'; 3 | import { LazyProvider } from './context'; 4 | 5 | export { Lazy, lazy, LazyProvider }; 6 | -------------------------------------------------------------------------------- /src/intersectionListener.js: -------------------------------------------------------------------------------- 1 | class IntersectionListener { 2 | constructor(options) { 3 | this.callbacks = new WeakMap(); 4 | this.observer = new IntersectionObserver(entries => { 5 | entries.forEach(entry => { 6 | const callback = this.callbacks.get(entry.target); 7 | if (entry.target && callback) { 8 | callback(entry, this.observer); 9 | } else { 10 | this.observer.unobserve(entry.target); 11 | } 12 | }); 13 | }, options); 14 | } 15 | 16 | listen(element, callback) { 17 | this.callbacks.set(element, callback); 18 | this.observer.observe(element); 19 | return () => { 20 | this.callbacks.delete(element); 21 | this.observer.unobserve(element); 22 | }; 23 | } 24 | } 25 | 26 | const observerPool = new WeakMap(); 27 | 28 | export default function createIntersectionListener(options) { 29 | const { 30 | root = typeof window === 'undefined' ? Object : window, 31 | rootMargin = '', 32 | threshold = '' 33 | } = options; 34 | if (!observerPool.get(root)) { 35 | observerPool.set(root, {}); 36 | } 37 | const group = observerPool.get(root); 38 | const key = `${rootMargin}_${threshold}`; 39 | let result = group[key]; 40 | if (!result) { 41 | result = new IntersectionListener(options); 42 | group[key] = result; 43 | } 44 | return result; 45 | } 46 | -------------------------------------------------------------------------------- /test/index.spec.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kouhin/rrr-lazy/e7655c93204d1a9b20deec52e363dd40fe0556e4/test/index.spec.js --------------------------------------------------------------------------------