├── .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 | [](https://circleci.com/gh/kouhin/rrr-lazy/tree/master)
7 | [](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
--------------------------------------------------------------------------------