├── .npmignore
├── .gitignore
├── .travis.yml
├── .babelrc.js
├── .editorconfig
├── rollup.config.js
├── index.html
├── LICENSE
├── package.json
├── readme.md
├── example.js
└── src
└── LazyList.js
/.npmignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | /example.*
3 | /index.html
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /dist
2 | /node_modules
3 | /example.build.js
4 | package-lock.json
5 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | sudo: false
3 | node_js:
4 | - 10
5 | - 8
6 | - 7
7 | - 6
8 |
--------------------------------------------------------------------------------
/.babelrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | ['@babel/preset-env', {
4 | loose: true,
5 | modules: process.env.EXAMPLE === '1' ? 'commonjs' : false,
6 | }],
7 | '@babel/preset-react',
8 | ],
9 | };
10 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig helps developers define and maintain consistent
2 | # coding styles between different editors and IDEs
3 | # editorconfig.org
4 |
5 | root = true
6 |
7 | [*]
8 | end_of_line = lf
9 | charset = utf-8
10 | trim_trailing_whitespace = true
11 | insert_final_newline = true
12 | indent_style = space
13 | indent_size = 2
14 |
15 | [*.md]
16 | trim_trailing_whitespace = false
17 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import babel from '@rollup/plugin-babel'
2 |
3 | const meta = require('./package.json')
4 |
5 | export default {
6 | input: './src/LazyList.js',
7 | output: [
8 | { format: 'cjs', file: meta.main, exports: 'default' },
9 | { format: 'es', file: meta.module }
10 | ],
11 |
12 | external: Object.keys(meta.dependencies)
13 | .concat(Object.keys(meta.peerDependencies)),
14 | plugins: [
15 | babel({ babelHelpers: 'bundled' })
16 | ]
17 | }
18 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | react-list Lazy Load example
6 |
11 |
12 |
13 | react-list-lazy-load example
14 |
15 | An example fixed-size lazy loading list using React,
16 | react-list and
17 | react-list-lazy-load.
18 | view source
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Renée Kooi
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 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-list-lazy-load",
3 | "version": "1.1.2",
4 | "description": "Lazy loading wrapper for react-list.",
5 | "main": "dist/react-list-lazy-load.js",
6 | "module": "dist/react-list-lazy-load.es.js",
7 | "repository": "u-wave/react-list-lazy-load",
8 | "scripts": {
9 | "build": "rollup -c",
10 | "example": "cross-env EXAMPLE=1 browserify example.js -t babelify --debug -r ./:react-list-lazy-load -o example.build.js",
11 | "test": "standard example.js src/**/*.js",
12 | "prepare": "npm run build"
13 | },
14 | "keywords": [],
15 | "author": "Renée Kooi ",
16 | "license": "MIT",
17 | "dependencies": {
18 | "prop-types": "^15.7.2"
19 | },
20 | "devDependencies": {
21 | "@babel/core": "^7.12.17",
22 | "@babel/plugin-transform-modules-commonjs": "^7.12.13",
23 | "@babel/preset-env": "^7.12.17",
24 | "@babel/preset-react": "^7.12.13",
25 | "@rollup/plugin-babel": "^5.3.0",
26 | "babelify": "^10.0.0",
27 | "browserify": "^17.0.0",
28 | "cross-env": "^7.0.3",
29 | "react": "^16.14.0",
30 | "react-dom": "^16.14.0",
31 | "react-list": "^0.8.16",
32 | "rollup": "^2.0.0",
33 | "standard": "^14.3.4"
34 | },
35 | "peerDependencies": {
36 | "react": "^0.14.0 || ^15.0.0 || ^16.0.0",
37 | "react-dom": "^0.14.0 || ^ 15.0.0 || ^16.0.0",
38 | "react-list": "^0.7.0 || ^0.8.0 < 0.8.10 || ^0.8.10"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # react-list-lazy-load
2 |
3 | > **Unmaintained**. Consider using [react-virtual](https://github.com/tanstack/virtual).
4 |
5 | Lazy Loading wrapper for [React-List](https://github.com/orgsync/react-list).
6 |
7 | This component works for finite collections only. If you have a large list of a
8 | known size that you do not want to load all at once, this component allows
9 | loading pages once the user scrolls near them. It does **not** implement
10 | infinite scrolling.
11 |
12 | [![npm][npm-image]][npm-url]
13 | [![travis][travis-image]][travis-url]
14 | [![size][size-image]][size-url]
15 |
16 | [npm-image]: https://img.shields.io/npm/v/react-list-lazy-load.svg?style=flat-square
17 | [npm-url]: https://www.npmjs.com/package/react-list-lazy-load
18 | [travis-image]: https://img.shields.io/travis/u-wave/react-list-lazy-load.svg?style=flat-square
19 | [travis-url]: https://travis-ci.org/u-wave/react-list-lazy-load
20 | [size-image]: https://img.shields.io/bundlephobia/minzip/react-list-lazy-load.svg?style=flat-square
21 | [size-url]: https://bundlephobia.com/result?p=react-list-lazy-load
22 |
23 | ## Installation
24 |
25 | ```bash
26 | npm install --save react-list-lazy-load
27 | ```
28 |
29 | ## Usage
30 |
31 | [Demo](https://u-wave.github.io/react-list-lazy-load) - [Demo source code](./example.js)
32 |
33 | You wrap it around a `` element to :sparkles: magically :sparkles:
34 | add lazy loading hooks:
35 |
36 | ```js
37 | import ReactList from 'react-list';
38 | import LazyLoading from 'react-list-lazy-load';
39 |
40 | const MyList = ({ items, onRequestPage }) => (
41 |
46 | (
48 | {items[idx]}
49 | )}
50 | type="uniform"
51 | length={items.length}
52 | />
53 |
54 | );
55 | ```
56 |
57 | ## API
58 |
59 | ### `items={Array}`
60 |
61 | An array of the items you're showing. This is used to determine when to load a
62 | page. If the user scrolls close to a `null` item in this array, or outside of
63 | array bounds, a new page will be loaded.
64 |
65 | I.e., a `null` item is regarded as an unloaded item.
66 |
67 | ### `length={Number}`
68 |
69 | The total amount of items, on all "pages". Defaults to
70 |
71 | ### `pageSize={Number}`
72 |
73 | Amount of items on a page. This is used to determine which page to load.
74 | Defaults to 25 (rather arbitrarily).
75 |
76 | ### `loadMargin={Number}`
77 |
78 | When to start loading the next page. The next page will be loaded when the user
79 | scrolls within `loadMargin` items from an unloaded item. You'll want to change
80 | this depending on the size of your items. If your items are super small, you
81 | should pick a larger `loadMargin`, but if they are rather large, you might be
82 | good with a margin of like 1 or 2 items.
83 |
84 | Defaults to 5.
85 |
86 | ### `onRequestPage={function(page, cb)}`
87 |
88 | Callback to load a page. This only tells you to load a new page--you should
89 | merge it into the `items` prop yourself.
90 |
91 | ## Licence
92 |
93 | [MIT](./LICENSE)
94 |
--------------------------------------------------------------------------------
/example.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import PropTypes from 'prop-types'
4 | // LazyList wraps around ReactList--so we need both!
5 | import LazyList from 'react-list-lazy-load'
6 | import ReactList from 'react-list'
7 |
8 | const randBetween = (min, max) =>
9 | Math.floor(min + (Math.random() * (max - min)))
10 |
11 | // Utility to create arrays with test data
12 | const array = (len, val = () => null) => {
13 | const arr = []
14 | for (let i = 0; i < len; i++) {
15 | arr.push(val(i))
16 | }
17 | return arr
18 | }
19 |
20 | function mergePage (items, newItems, offset) {
21 | const merged = items.slice()
22 | newItems.forEach((item, idx) => {
23 | merged[idx + offset] = item
24 | })
25 | return merged
26 | }
27 |
28 | class App extends React.Component {
29 | constructor (props) {
30 | super(props)
31 | this.state = {
32 | items: array(25, (i) => `item #${i}`)
33 | }
34 |
35 | this.handleRequestPage = this.handleRequestPage.bind(this)
36 | }
37 |
38 | // Simulate a network request for `limit` items
39 | fetch (page, cb) {
40 | const { minLoadTime, maxLoadTime, pageSize } = this.props
41 | setTimeout(() => {
42 | // Generate a new page of items
43 | const data = array(pageSize, (i) => `item #${(page * pageSize) + i}`)
44 |
45 | cb(data)
46 | }, randBetween(minLoadTime, maxLoadTime))
47 | }
48 |
49 | handleRequestPage (page, cb) {
50 | const { pageSize } = this.props
51 |
52 | // Simulate a network request or other async operation
53 | this.fetch(page, (data) => {
54 | // Merge the new page into the current `items` collection and rerender
55 | this.setState({
56 | items: mergePage(this.state.items, data, page * pageSize)
57 | })
58 |
59 | // Tell LazyList that the page was loaded
60 | cb()
61 | })
62 | }
63 |
64 | render () {
65 | const { totalItems } = this.props
66 | const { items } = this.state
67 | return (
68 |
74 | (
78 | // If `items[index] == null`, the page is still being loaded.
79 | items[index] != null ? (
80 |
81 | #{index}
82 | {items[index]}
83 |
84 | ) : (
85 | Loading …
86 | )
87 | )}
88 | />
89 |
90 | )
91 | }
92 | }
93 |
94 | App.propTypes = {
95 | pageSize: PropTypes.number.isRequired,
96 | totalItems: PropTypes.number.isRequired,
97 | minLoadTime: PropTypes.number.isRequired,
98 | maxLoadTime: PropTypes.number.isRequired
99 | }
100 |
101 | ReactDOM.render(
102 | ,
108 | document.getElementById('example')
109 | )
110 |
--------------------------------------------------------------------------------
/src/LazyList.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | const proxyMethods = [
5 | 'getOffset',
6 | 'getScroll',
7 | 'setScroll',
8 | 'getViewportSize',
9 | 'getScrollSize',
10 | 'getStartAndEnd',
11 | 'getItemSizeAndItemsPerRow',
12 | 'getSpaceBefore',
13 | 'getSizeOf',
14 | 'scrollTo',
15 | 'scrollAround',
16 | 'getVisibleRange'
17 | ]
18 |
19 | function requestPage (call, page, cb) {
20 | const promise = call(page, cb)
21 | if (promise && promise.then) {
22 | promise
23 | .then((res) => cb(null, res))
24 | .then(null, cb)
25 | }
26 | }
27 |
28 | /**
29 | * Like `.slice`, but doesn't care about array bounds.
30 | *
31 | * [0, 1].slice(1, 3) === [1]
32 | * eagerSlice([0, 1], 1, 3) === [1, undefined, undefined]
33 | */
34 | function eagerSlice (list, start, end) {
35 | const sliced = []
36 | for (let i = start; i < end; i++) {
37 | sliced.push(list[i])
38 | }
39 | return sliced
40 | }
41 |
42 | /**
43 | * Adds simple lazy loading to react-list.
44 | */
45 | class LazyList extends React.Component {
46 | constructor (props) {
47 | super(props)
48 |
49 | this._list = null
50 | this._loadingPages = {}
51 | this.updateFrame = this.updateFrame.bind(this)
52 | }
53 |
54 | componentDidMount () {
55 | this.updateScrollParent()
56 | this.updateFrame()
57 | }
58 |
59 | componentDidUpdate () {
60 | this.updateScrollParent()
61 | this.updateFrame()
62 | }
63 |
64 | updateScrollParent () {
65 | const prev = this.scrollParent
66 | const list = this.getList()
67 | this.scrollParent = list.scrollParent
68 |
69 | if (prev === this.scrollParent) {
70 | return
71 | }
72 | if (prev) {
73 | prev.removeEventListener('scroll', this.updateFrame)
74 | }
75 | if (this.props.onRequestPage) {
76 | this.scrollParent.addEventListener('scroll', this.updateFrame)
77 | }
78 | }
79 |
80 | getList () {
81 | return this._list
82 | }
83 |
84 | isLoadingPage (page) {
85 | return !!this._loadingPages[page]
86 | }
87 |
88 | itemNeedsLoad (idx) {
89 | const { items, pageSize } = this.props
90 | const page = Math.floor(idx / pageSize)
91 | return items[idx] != null || this.isLoadingPage(page)
92 | }
93 |
94 | updateFrame () {
95 | const {
96 | pageSize, loadMargin,
97 | items, length,
98 | onRequestPage
99 | } = this.props
100 |
101 | // Item range that should be loaded right about now.
102 | let [topItem, bottomItem] = this.getVisibleRange()
103 |
104 | if (topItem === undefined || bottomItem === undefined) {
105 | return
106 | }
107 |
108 | topItem = Math.max(topItem - loadMargin, 0)
109 | bottomItem = Math.min(bottomItem + loadMargin, length)
110 |
111 | const almostVisibleItems = eagerSlice(items, topItem, bottomItem)
112 |
113 | const unloadedPages = almostVisibleItems.reduce((pages, item, idx) => {
114 | if (item == null) {
115 | const page = Math.floor((topItem + idx) / pageSize)
116 | if (!this.isLoadingPage(page) && pages.indexOf(page) === -1) {
117 | return [...pages, page]
118 | }
119 | }
120 | return pages
121 | }, [])
122 |
123 | unloadedPages.forEach((page) => {
124 | this._loadingPages[page] = true
125 | requestPage(onRequestPage, page, () => {
126 | // Always delete after completion. If there was an error, we can retry
127 | // later. If there wasn't, we don't need to keep this around :)
128 | delete this._loadingPages[page]
129 | })
130 | })
131 | }
132 |
133 | render () {
134 | return React.cloneElement(this.props.children, {
135 | ref: (list) => {
136 | this._list = list
137 | }
138 | })
139 | }
140 | }
141 |
142 | if (process.env.NODE_ENV !== 'production') {
143 | LazyList.propTypes = {
144 | /**
145 | * Total amount of items, on all pages.
146 | */
147 | length: PropTypes.number.isRequired,
148 | /**
149 | * Items per page.
150 | */
151 | pageSize: PropTypes.number,
152 | /**
153 | * When to begin loading the next page.
154 | */
155 | loadMargin: PropTypes.number,
156 | /**
157 | * Loaded items. NULLs in this array indicate unloaded items.
158 | */
159 | items: PropTypes.array,
160 |
161 | /**
162 | * Callback to begin loading a page.
163 | */
164 | onRequestPage: PropTypes.func.isRequired
165 | }
166 | }
167 |
168 | LazyList.defaultProps = {
169 | pageSize: 25,
170 | loadMargin: 5
171 | }
172 |
173 | proxyMethods.forEach((name) => {
174 | LazyList.prototype[name] = function (...args) {
175 | return this.getList()[name](...args)
176 | }
177 | })
178 |
179 | export default LazyList
180 |
--------------------------------------------------------------------------------