├── .gitignore ├── LICENSE ├── README.md ├── dist ├── grid.js └── item.js ├── example ├── index.html └── main.js ├── package.json ├── scripts └── postinstall.js ├── src ├── grid.js └── item.js └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | example/*bundle.js 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Gordan Grasarevic 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-infinite-grid 2 | 3 | _react infinite grid_ is a React component which renders a grid of React elements. It's different because it only renders the elements that the user can see (and a small buffer) meaning that it is well suited for displaying a large number of elements. 4 | 5 | # Installation 6 | 7 | ``` 8 | npm install react-infinite-grid 9 | ``` 10 | 11 | # Example 12 | 13 | The example below renders a grid with 100,000 items. 14 | 15 | ```jsx 16 | import React from 'react'; 17 | import ReactDOM from 'react-dom'; 18 | import InfiniteGrid from '../src/grid'; 19 | 20 | class ExampleItem extends React.Component { 21 | 22 | static get propTypes() { 23 | return { 24 | index: React.PropTypes.number 25 | }; 26 | } 27 | 28 | render() { 29 | return( 30 |
31 | This is {this.props.index} 32 |
33 | ); 34 | } 35 | 36 | } 37 | 38 | // Create 100,000 Example items 39 | let items = []; 40 | for (let i = 0; i <= 100000; i++) { 41 | items.push(); 42 | } 43 | 44 | ReactDOM.render(, document.getElementById('grid')); 45 | ``` 46 | 47 | ## Required props 48 | 49 | - **entries** `React.PropTypes.arrayOf(React.PropTypes.element)` - The only required property is an array of React elements that you want to render. 50 | 51 | ## Optional props 52 | 53 | - **height** `React.PropTypes.number` - The height of the grid item 54 | - **width** `React.PropTypes.number` - The width of the grid item 55 | - **padding** `React.PropTypes.number` - The padding around your items 56 | - **wrapperHeight** `React.PropTypes.number` - The height of the grid. 57 | - **lazyCallback** `React.PropTypes.func` - A function that takes no arguments which is called when a user reaches the end of the grid. Useful if you want to lazy load your data. 58 | 59 | # Demo 60 | 61 | You can find a demo [here](http://ggordan.com/post/react-infinite-grid.html). 62 | -------------------------------------------------------------------------------- /dist/grid.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); 4 | 5 | Object.defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | 9 | var _react = require('react'); 10 | 11 | var _react2 = _interopRequireDefault(_react); 12 | 13 | var _lodash = require('lodash'); 14 | 15 | var _item = require('./item'); 16 | 17 | var _item2 = _interopRequireDefault(_item); 18 | 19 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 20 | 21 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 22 | 23 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 24 | 25 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 26 | 27 | var InfiniteGrid = (function (_React$Component) { 28 | _inherits(InfiniteGrid, _React$Component); 29 | 30 | _createClass(InfiniteGrid, [{ 31 | key: 'initialState', 32 | value: function initialState() { 33 | return { 34 | initiatedLazyload: false, 35 | minHeight: window.innerHeight * 2, 36 | minItemIndex: 0, 37 | maxItemIndex: 100, 38 | itemDimensions: { 39 | height: this._itemHeight(), 40 | width: this._itemHeight(), 41 | gridWidth: 0, 42 | itemsPerRow: 2 43 | } 44 | }; 45 | } 46 | }], [{ 47 | key: 'propTypes', 48 | get: function get() { 49 | return { 50 | itemClassName: _react2.default.PropTypes.string, 51 | entries: _react2.default.PropTypes.arrayOf(_react2.default.PropTypes.object).isRequired, 52 | height: _react2.default.PropTypes.number, 53 | width: _react2.default.PropTypes.number, 54 | padding: _react2.default.PropTypes.number, 55 | wrapperHeight: _react2.default.PropTypes.number, 56 | lazyCallback: _react2.default.PropTypes.func, 57 | renderRangeCallback: _react2.default.PropTypes.func, 58 | buffer: _react2.default.PropTypes.number 59 | }; 60 | } 61 | }]); 62 | 63 | function InfiniteGrid(props) { 64 | _classCallCheck(this, InfiniteGrid); 65 | 66 | var _this2 = _possibleConstructorReturn(this, Object.getPrototypeOf(InfiniteGrid).call(this, props)); 67 | 68 | _this2.state = _this2.initialState(); 69 | // bind the functions 70 | _this2._scrollListener = _this2._scrollListener.bind(_this2); 71 | _this2._updateItemDimensions = _this2._updateItemDimensions.bind(_this2); 72 | _this2._resizeListener = _this2._resizeListener.bind(_this2); 73 | _this2._visibleIndexes = _this2._visibleIndexes.bind(_this2); 74 | return _this2; 75 | } 76 | 77 | // METHODS 78 | 79 | _createClass(InfiniteGrid, [{ 80 | key: '_wrapperStyle', 81 | value: function _wrapperStyle() { 82 | return { 83 | maxHeight: this._getGridHeight(), 84 | overflowY: 'scroll', 85 | width: '100%', 86 | height: this.props.wrapperHeight, 87 | WebkitOverflowScrolling: true 88 | }; 89 | } 90 | }, { 91 | key: '_gridStyle', 92 | value: function _gridStyle() { 93 | return { 94 | position: 'relative', 95 | marginTop: this.props.padding, 96 | marginLeft: this.props.padding, 97 | minHeight: this._getGridHeight() 98 | }; 99 | } 100 | }, { 101 | key: '_getGridRect', 102 | value: function _getGridRect() { 103 | return this.refs.grid.getBoundingClientRect(); 104 | } 105 | }, { 106 | key: '_getGridHeight', 107 | value: function _getGridHeight() { 108 | return Math.floor(this.props.entries.length / this.state.itemDimensions.itemsPerRow) * this.state.itemDimensions.height; 109 | } 110 | }, { 111 | key: '_getWrapperRect', 112 | value: function _getWrapperRect() { 113 | return this.refs.wrapper.getBoundingClientRect(); 114 | } 115 | }, { 116 | key: '_visibleIndexes', 117 | value: function _visibleIndexes() { 118 | var itemsPerRow = this._itemsPerRow(); 119 | 120 | // The number of rows that the user has scrolled past 121 | var scrolledPast = this._scrolledPastRows() * itemsPerRow; 122 | if (scrolledPast < 0) scrolledPast = 0; 123 | 124 | // If i have scrolled past 20 items, but 60 are visible on screen, 125 | // we do not want to change the minimum 126 | var min = scrolledPast - itemsPerRow; 127 | if (min < 0) min = 0; 128 | 129 | // the maximum should be the number of items scrolled past, plus some 130 | // buffer 131 | var bufferRows = this._numVisibleRows() + this.props.buffer; 132 | var max = scrolledPast + itemsPerRow * bufferRows; 133 | if (max > this.props.entries.length) max = this.props.entries.length; 134 | 135 | this.setState({ 136 | minItemIndex: min, 137 | maxItemIndex: max 138 | }, function () { 139 | this._lazyCallback(); 140 | }); 141 | } 142 | }, { 143 | key: '_updateItemDimensions', 144 | value: function _updateItemDimensions() { 145 | this.setState({ 146 | itemDimensions: { 147 | height: this._itemHeight(), 148 | width: this._itemHeight(), 149 | gridWidth: this._getGridRect().width, 150 | itemsPerRow: this._itemsPerRow() 151 | }, 152 | minHeight: this._totalRows() 153 | }); 154 | } 155 | }, { 156 | key: '_itemsPerRow', 157 | value: function _itemsPerRow() { 158 | return Math.floor(this._getGridRect().width / this._itemWidth()); 159 | } 160 | }, { 161 | key: '_totalRows', 162 | value: function _totalRows() { 163 | var scrolledPastHeight = this.props.entries.length / this._itemsPerRow() * this._itemHeight(); 164 | if (scrolledPastHeight < 0) return 0; 165 | return scrolledPastHeight; 166 | } 167 | }, { 168 | key: '_scrolledPastRows', 169 | value: function _scrolledPastRows() { 170 | var rect = this._getGridRect(); 171 | var topScrollOffset = rect.height - rect.bottom; 172 | return Math.floor(topScrollOffset / this._itemHeight()); 173 | } 174 | }, { 175 | key: '_itemHeight', 176 | value: function _itemHeight() { 177 | return this.props.height + 2 * this.props.padding; 178 | } 179 | }, { 180 | key: '_itemWidth', 181 | value: function _itemWidth() { 182 | return this.props.width + 2 * this.props.padding; 183 | } 184 | }, { 185 | key: '_numVisibleRows', 186 | value: function _numVisibleRows() { 187 | return Math.ceil(this._getWrapperRect().height / this._itemHeight()); 188 | } 189 | }, { 190 | key: '_lazyCallback', 191 | value: function _lazyCallback() { 192 | if (!this.state.initiatedLazyload && this.state.maxItemIndex === this.props.entries.length && this.props.lazyCallback) { 193 | this.setState({ initiatedLazyload: true }); 194 | this.props.lazyCallback(this.state.maxItemIndex); 195 | } 196 | } 197 | 198 | // LIFECYCLE 199 | 200 | }, { 201 | key: 'componentWillMount', 202 | value: function componentWillMount() { 203 | window.addEventListener('resize', this._resizeListener); 204 | } 205 | }, { 206 | key: 'componentDidMount', 207 | value: function componentDidMount() { 208 | this._updateItemDimensions(); 209 | this._visibleIndexes(); 210 | } 211 | }, { 212 | key: 'componentWillReceiveProps', 213 | value: function componentWillReceiveProps(nextProps) { 214 | if (nextProps.entries.length > this.props.entries.length) { 215 | this.setState({ 216 | initiatedLazyload: false 217 | }); 218 | } 219 | // Update these all the time because entries may change on the fly. 220 | // this._updateItemDimensions(); 221 | this._visibleIndexes(); 222 | } 223 | }, { 224 | key: 'componentDidUpdate', 225 | value: function componentDidUpdate(prevProps, prevState) { 226 | if (typeof this.props.renderRangeCallback === 'function') { 227 | this.props.renderRangeCallback(this.state.minItemIndex, this.state.maxItemIndex); 228 | } 229 | } 230 | }, { 231 | key: 'shouldComponentUpdate', 232 | value: function shouldComponentUpdate(nextProps, nextState) { 233 | return !(0, _lodash.isEqual)(this.state, nextState); 234 | } 235 | }, { 236 | key: 'componentWillUnmount', 237 | value: function componentWillUnmount() { 238 | window.removeEventListener('resize', this._resizeListener); 239 | } 240 | 241 | // LISTENERS 242 | 243 | }, { 244 | key: '_scrollListener', 245 | value: function _scrollListener(event) { 246 | var _this = this; 247 | 248 | clearTimeout(this.scrollOffset); 249 | this.scrollOffset = setTimeout(function () { 250 | _this._visibleIndexes(); 251 | }, 10); 252 | } 253 | }, { 254 | key: '_resizeListener', 255 | value: function _resizeListener(event) { 256 | if (!this.props.wrapperHeight) { 257 | this.setState({ 258 | wrapperHeight: window.innerHeight 259 | }); 260 | } 261 | this._updateItemDimensions(); 262 | this._visibleIndexes(); 263 | } 264 | 265 | // RENDER 266 | 267 | }, { 268 | key: 'render', 269 | value: function render() { 270 | var entries = []; 271 | 272 | // if no entries exist, there's nothing left to do 273 | if (!this.props.entries.length) { 274 | return null; 275 | } 276 | 277 | for (var i = this.state.minItemIndex; i <= this.state.maxItemIndex; i++) { 278 | var entry = this.props.entries[i]; 279 | if (!entry) { 280 | continue; 281 | } 282 | var itemProps = { 283 | key: 'item-' + i, 284 | index: i, 285 | padding: this.props.padding, 286 | dimensions: this.state.itemDimensions, 287 | data: entry 288 | }; 289 | entries.push(_react2.default.createElement(_item2.default, itemProps)); 290 | } 291 | return _react2.default.createElement( 292 | 'div', 293 | { className: 'infinite-grid-wrapper', ref: 'wrapper', onScroll: this._scrollListener, style: this._wrapperStyle() }, 294 | _react2.default.createElement( 295 | 'div', 296 | { ref: 'grid', className: 'infinite-grid', style: this._gridStyle() }, 297 | entries 298 | ) 299 | ); 300 | } 301 | }]); 302 | 303 | return InfiniteGrid; 304 | })(_react2.default.Component); 305 | 306 | exports.default = InfiniteGrid; 307 | ; 308 | 309 | InfiniteGrid.defaultProps = { 310 | buffer: 10, 311 | padding: 10, 312 | entries: [], 313 | height: 250, 314 | width: 250 315 | }; -------------------------------------------------------------------------------- /dist/item.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); 4 | 5 | Object.defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | 9 | var _react = require('react'); 10 | 11 | var _react2 = _interopRequireDefault(_react); 12 | 13 | var _lodash = require('lodash'); 14 | 15 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 16 | 17 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 18 | 19 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 20 | 21 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 22 | 23 | var Item = (function (_React$Component) { 24 | _inherits(Item, _React$Component); 25 | 26 | function Item(props) { 27 | _classCallCheck(this, Item); 28 | 29 | return _possibleConstructorReturn(this, Object.getPrototypeOf(Item).call(this, props)); 30 | } 31 | 32 | _createClass(Item, [{ 33 | key: '_itemWidth', 34 | value: function _itemWidth() { 35 | return this.props.dimensions.gridWidth / this.props.dimensions.itemsPerRow; 36 | } 37 | }, { 38 | key: '_itemLeft', 39 | value: function _itemLeft() { 40 | var column = this.props.index % this.props.dimensions.itemsPerRow; 41 | return column * (this.props.dimensions.gridWidth / this.props.dimensions.itemsPerRow); 42 | } 43 | }, { 44 | key: '_itemTop', 45 | value: function _itemTop() { 46 | return Math.floor(this.props.index / this.props.dimensions.itemsPerRow) * this.props.dimensions.height; 47 | } 48 | 49 | // LIFECYCLE 50 | 51 | }, { 52 | key: 'shouldComponentUpdate', 53 | value: function shouldComponentUpdate(nextProps, nextState) { 54 | return !(0, _lodash.isEqual)(this.props, nextProps); 55 | } 56 | 57 | // RENDER 58 | 59 | }, { 60 | key: 'render', 61 | value: function render() { 62 | var _style = { 63 | width: this._itemWidth() - this.props.padding, 64 | height: this.props.dimensions.height - this.props.padding, 65 | left: this._itemLeft(), 66 | top: this._itemTop(), 67 | position: 'absolute' 68 | }; 69 | 70 | var props = { 71 | className: 'item', 72 | style: _style 73 | }; 74 | 75 | return _react2.default.createElement( 76 | 'div', 77 | props, 78 | _react2.default.createElement( 79 | 'div', 80 | null, 81 | this.props.data 82 | ) 83 | ); 84 | } 85 | }]); 86 | 87 | return Item; 88 | })(_react2.default.Component); 89 | 90 | exports.default = Item; -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Example 5 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /example/main.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import InfiniteGrid from '../src/grid'; 4 | 5 | class ExampleItem extends React.Component { 6 | 7 | static get propTypes() { 8 | return { 9 | index: React.PropTypes.number 10 | }; 11 | } 12 | 13 | render() { 14 | return( 15 |
16 | This is {this.props.index} 17 |
18 | ); 19 | } 20 | 21 | } 22 | 23 | // Create 100,000 Example items 24 | let items = []; 25 | for (let i = 0; i <= 1000; i++) { 26 | items.push(); 27 | } 28 | 29 | const lazyCallback = (index) => { 30 | console.log(index); 31 | } 32 | 33 | ReactDOM.render(, document.getElementById('grid')); 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-infinite-grid", 3 | "version": "0.4.0", 4 | "description": "An React grid component which can handle rendering large amounts of data.", 5 | "author": "Gordan Grasarevic ", 6 | "main": "dist/grid.js", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/ggordan/react-infinite-grid" 10 | }, 11 | "keywords": [ 12 | "react-component", 13 | "react", 14 | "grid", 15 | "infinite" 16 | ], 17 | "license": "MIT", 18 | "dependencies": { 19 | "lodash": "^3.10.1", 20 | "react": "^0.14.2", 21 | "react-dom": "^0.14.2" 22 | }, 23 | "devDependencies": { 24 | "babel": "^6.1.18", 25 | "babel-cli": "^6.1.18", 26 | "babel-core": "^6.1.20", 27 | "babel-eslint": "^4.1.3", 28 | "babel-loader": "^6.1.0", 29 | "babel-preset-es2015": "^6.1.18", 30 | "babel-preset-react": "^6.1.18", 31 | "rimraf": "^2.4.3", 32 | "webpack": "^1.7.3" 33 | }, 34 | "scripts": { 35 | "build": "rimraf dist && babel ./src -d dist --presets=react,es2015" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /scripts/postinstall.js: -------------------------------------------------------------------------------- 1 | var execSync = require('child_process').execSync 2 | var stat = require('fs').stat 3 | 4 | function exec(command) { 5 | execSync(command, { stdio: [0, 1, 2] }) 6 | } 7 | 8 | stat('dist', function (error, stat) { 9 | if (error || !stat.isDirectory()) 10 | exec('npm run build'); 11 | }); 12 | -------------------------------------------------------------------------------- /src/grid.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {isEqual} from 'lodash'; 3 | import Item from './item'; 4 | 5 | export default class InfiniteGrid extends React.Component { 6 | 7 | static get propTypes() { 8 | return { 9 | itemClassName: React.PropTypes.string, 10 | entries: React.PropTypes.arrayOf(React.PropTypes.object).isRequired, 11 | height: React.PropTypes.number, 12 | width: React.PropTypes.number, 13 | padding: React.PropTypes.number, 14 | wrapperHeight: React.PropTypes.number, 15 | lazyCallback: React.PropTypes.func, 16 | renderRangeCallback: React.PropTypes.func, 17 | buffer: React.PropTypes.number 18 | } 19 | } 20 | 21 | initialState() { 22 | return { 23 | initiatedLazyload: false, 24 | minHeight: window.innerHeight * 2, 25 | minItemIndex: 0, 26 | maxItemIndex: 100, 27 | itemDimensions: { 28 | height: this._itemHeight(), 29 | width: this._itemHeight(), 30 | gridWidth: 0, 31 | itemsPerRow: 2, 32 | }, 33 | }; 34 | } 35 | 36 | constructor(props) { 37 | super(props); 38 | this.state = this.initialState(); 39 | // bind the functions 40 | this._scrollListener = this._scrollListener.bind(this); 41 | this._updateItemDimensions = this._updateItemDimensions.bind(this); 42 | this._resizeListener = this._resizeListener.bind(this); 43 | this._visibleIndexes = this._visibleIndexes.bind(this); 44 | } 45 | 46 | // METHODS 47 | 48 | _wrapperStyle() { 49 | return { 50 | maxHeight: this._getGridHeight(), 51 | overflowY: 'scroll', 52 | width: '100%', 53 | height: this.props.wrapperHeight, 54 | WebkitOverflowScrolling: true, 55 | }; 56 | } 57 | 58 | _gridStyle() { 59 | return { 60 | position: 'relative', 61 | marginTop: this.props.padding, 62 | marginLeft: this.props.padding, 63 | minHeight: this._getGridHeight(), 64 | }; 65 | } 66 | 67 | _getGridRect() { 68 | return this.refs.grid.getBoundingClientRect(); 69 | } 70 | 71 | _getGridHeight() { 72 | return (this.props.entries.length < this.state.itemDimensions.itemsPerRow) ? 73 | this.state.itemDimensions.height : 74 | Math.floor(this.props.entries.length / this.state.itemDimensions.itemsPerRow) * this.state.itemDimensions.height; 75 | } 76 | 77 | _getWrapperRect() { 78 | return this.refs.wrapper.getBoundingClientRect(); 79 | } 80 | 81 | _visibleIndexes() { 82 | var itemsPerRow = this._itemsPerRow(); 83 | 84 | // The number of rows that the user has scrolled past 85 | var scrolledPast = (this._scrolledPastRows() * itemsPerRow); 86 | if (scrolledPast < 0) scrolledPast = 0; 87 | 88 | // If i have scrolled past 20 items, but 60 are visible on screen, 89 | // we do not want to change the minimum 90 | var min = scrolledPast - itemsPerRow; 91 | if (min < 0) min = 0; 92 | 93 | // the maximum should be the number of items scrolled past, plus some 94 | // buffer 95 | var bufferRows = this._numVisibleRows() + this.props.buffer; 96 | var max = scrolledPast + (itemsPerRow * bufferRows); 97 | if (max > this.props.entries.length) max = this.props.entries.length; 98 | 99 | this.setState({ 100 | minItemIndex: min, 101 | maxItemIndex: max, 102 | }, function() { 103 | this._lazyCallback(); 104 | }); 105 | } 106 | 107 | _updateItemDimensions() { 108 | this.setState({ 109 | itemDimensions: { 110 | height: this._itemHeight(), 111 | width: this._itemHeight(), 112 | gridWidth: this._getGridRect().width, 113 | itemsPerRow: this._itemsPerRow(), 114 | }, 115 | minHeight: this._totalRows(), 116 | }); 117 | } 118 | 119 | _itemsPerRow() { 120 | return Math.floor(this._getGridRect().width / this._itemWidth()); 121 | } 122 | 123 | _totalRows() { 124 | const scrolledPastHeight = (this.props.entries.length / this._itemsPerRow()) * this._itemHeight(); 125 | if (scrolledPastHeight < 0) return 0; 126 | return scrolledPastHeight; 127 | } 128 | 129 | _scrolledPastRows() { 130 | const rect = this._getGridRect(); 131 | const topScrollOffset = rect.height - rect.bottom; 132 | return Math.floor(topScrollOffset / this._itemHeight()); 133 | } 134 | 135 | _itemHeight() { 136 | return this.props.height + (2 * this.props.padding); 137 | } 138 | 139 | _itemWidth() { 140 | return this.props.width + (2 * this.props.padding); 141 | } 142 | 143 | _numVisibleRows() { 144 | return Math.ceil(this._getWrapperRect().height / this._itemHeight()); 145 | } 146 | 147 | _lazyCallback() { 148 | if (!this.state.initiatedLazyload && (this.state.maxItemIndex === this.props.entries.length) && this.props.lazyCallback) { 149 | this.setState({initiatedLazyload: true }); 150 | this.props.lazyCallback(this.state.maxItemIndex); 151 | } 152 | } 153 | 154 | // LIFECYCLE 155 | 156 | componentWillMount() { 157 | window.addEventListener('resize', this._resizeListener); 158 | } 159 | 160 | componentDidMount() { 161 | this._updateItemDimensions(); 162 | this._visibleIndexes(); 163 | } 164 | 165 | componentWillReceiveProps(nextProps) { 166 | if (nextProps.entries.length > this.props.entries.length) { 167 | this.setState({ 168 | initiatedLazyload: false, 169 | }); 170 | } 171 | // Update these all the time because entries may change on the fly. 172 | // this._updateItemDimensions(); 173 | this._visibleIndexes(); 174 | } 175 | 176 | componentDidUpdate(prevProps, prevState) { 177 | if (typeof this.props.renderRangeCallback === 'function') { 178 | this.props.renderRangeCallback(this.state.minItemIndex, this.state.maxItemIndex); 179 | } 180 | } 181 | 182 | shouldComponentUpdate(nextProps, nextState) { 183 | return !isEqual(this.state, nextState); 184 | } 185 | 186 | componentWillUnmount() { 187 | window.removeEventListener('resize', this._resizeListener); 188 | } 189 | 190 | // LISTENERS 191 | 192 | _scrollListener(event) { 193 | clearTimeout(this.scrollOffset); 194 | this.scrollOffset = setTimeout(() => { 195 | this._visibleIndexes(); 196 | }, 10); 197 | } 198 | 199 | _resizeListener(event) { 200 | if (!this.props.wrapperHeight) { 201 | this.setState({ 202 | wrapperHeight: window.innerHeight, 203 | }); 204 | } 205 | this._updateItemDimensions(); 206 | this._visibleIndexes(); 207 | } 208 | 209 | // RENDER 210 | 211 | render() { 212 | var entries = []; 213 | 214 | // if no entries exist, there's nothing left to do 215 | if (!this.props.entries.length) { 216 | return null; 217 | } 218 | 219 | for (let i = this.state.minItemIndex; i <= this.state.maxItemIndex; i++) { 220 | let entry = this.props.entries[i]; 221 | if (!entry) { 222 | continue; 223 | } 224 | const itemProps = { 225 | key: 'item-' + i, 226 | index: i, 227 | padding: this.props.padding, 228 | dimensions: this.state.itemDimensions, 229 | data: entry 230 | }; 231 | entries.push(); 232 | } 233 | return ( 234 |
235 |
236 | {entries} 237 |
238 |
239 | ); 240 | 241 | } 242 | 243 | }; 244 | 245 | InfiniteGrid.defaultProps = { 246 | buffer: 10, 247 | padding: 10, 248 | entries: [], 249 | height: 250, 250 | width: 250 251 | } 252 | -------------------------------------------------------------------------------- /src/item.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {isEqual} from 'lodash'; 3 | 4 | export default class Item extends React.Component { 5 | 6 | constructor(props) { 7 | super(props); 8 | } 9 | 10 | _itemWidth() { 11 | return this.props.dimensions.gridWidth / this.props.dimensions.itemsPerRow; 12 | } 13 | 14 | _itemLeft() { 15 | var column = this.props.index % this.props.dimensions.itemsPerRow; 16 | return column * (this.props.dimensions.gridWidth / this.props.dimensions.itemsPerRow); 17 | } 18 | 19 | _itemTop() { 20 | return Math.floor(this.props.index / this.props.dimensions.itemsPerRow) * this.props.dimensions.height; 21 | } 22 | 23 | // LIFECYCLE 24 | 25 | shouldComponentUpdate(nextProps, nextState) { 26 | return !isEqual(this.props, nextProps); 27 | } 28 | 29 | // RENDER 30 | 31 | render() { 32 | const _style = { 33 | width: this._itemWidth() - this.props.padding, 34 | height: this.props.dimensions.height - this.props.padding, 35 | left: this._itemLeft(), 36 | top: this._itemTop(), 37 | position: 'absolute' 38 | }; 39 | 40 | var props = { 41 | className: 'item', 42 | style: _style 43 | }; 44 | 45 | return ( 46 |
47 |
{this.props.data}
48 |
49 | ); 50 | 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var path = require('path'); 3 | 4 | module.exports = { 5 | entry: "./example/main.js", 6 | output: { 7 | path: './example', 8 | filename: "example.bundle.js" 9 | }, 10 | module: { 11 | loaders: [{ 12 | test: /\.jsx?$/, 13 | loaders: ['babel?presets[]=es2015,presets[]=react'] 14 | }] 15 | } 16 | }; 17 | --------------------------------------------------------------------------------