diff --git a/lib/VirtualList.js b/lib/VirtualList.js index bb14937..19087d1 100644 --- a/lib/VirtualList.js +++ b/lib/VirtualList.js @@ -46,32 +46,43 @@ var VirtualList = function VirtualList(options) { var _class, _temp; return _temp = _class = function (_PureComponent) { - _inherits(vlist, _PureComponent); + _inherits(VList, _PureComponent); - function vlist(props) { - _classCallCheck(this, vlist); + function VList(props) { + _classCallCheck(this, VList); - var _this = _possibleConstructorReturn(this, (vlist.__proto__ || Object.getPrototypeOf(vlist)).call(this, props)); + var _this = _possibleConstructorReturn(this, (VList.__proto__ || Object.getPrototypeOf(VList)).call(this, props)); _this._isMounted = false; + _this.refreshState = function () { + if (!_this._isMounted) { + return; + } + + var _this$props = _this.props, + itemHeight = _this$props.itemHeight, + items = _this$props.items, + itemBuffer = _this$props.itemBuffer; + var _this$state = _this.state, + domNode = _this$state.domNode, + container = _this$state.options.container, + firstItemIndex = _this$state.firstItemIndex, + lastItemIndex = _this$state.lastItemIndex; + + var state = VList.setStateIfNeeded(domNode, container, items, itemHeight, itemBuffer, firstItemIndex, lastItemIndex); - _this.options = _extends({ - container: typeof window !== 'undefined' ? window : undefined - }, options); + _this.setState(state); + }; _this.state = { firstItemIndex: 0, - lastItemIndex: -1 + lastItemIndex: -1, + options: _extends({ + container: typeof window !== 'undefined' ? window : undefined + }, options) }; - // initialState allows us to set the first/lastItemIndex (useful for server-rendering) - if (options && options.initialState) { - _this.state = _extends({}, _this.state, options.initialState); - } - - _this.refreshState = _this.refreshState.bind(_this); - // if requestAnimationFrame is available, use it to throttle refreshState if (typeof window !== 'undefined' && 'requestAnimationFrame' in window) { _this.refreshState = (0, _throttleWithRAF2.default)(_this.refreshState); @@ -79,87 +90,85 @@ var VirtualList = function VirtualList(options) { return _this; } - _createClass(vlist, [{ - key: 'setStateIfNeeded', - value: function setStateIfNeeded(list, container, items, itemHeight, itemBuffer) { - // get first and lastItemIndex - var state = (0, _getVisibleItemBounds2.default)(list, container, items, itemHeight, itemBuffer); - - if (state === undefined) { - return; - } - - if (state.firstItemIndex > state.lastItemIndex) { - return; - } - - if (state.firstItemIndex !== this.state.firstItemIndex || state.lastItemIndex !== this.state.lastItemIndex) { - this.setState(state); - } - } - }, { - key: 'refreshState', - value: function refreshState() { - if (!this._isMounted) { - return; - } - - var _props = this.props, - itemHeight = _props.itemHeight, - items = _props.items, - itemBuffer = _props.itemBuffer; + _createClass(VList, [{ + key: 'componentDidMount', + value: function componentDidMount() { + var container = this.state.options.container; - this.setStateIfNeeded(this.domNode, this.options.container, items, itemHeight, itemBuffer); - } - }, { - key: 'componentWillMount', - value: function componentWillMount() { this._isMounted = true; - } - }, { - key: 'componentDidMount', - value: function componentDidMount() { + // cache the DOM node - this.domNode = _reactDom2.default.findDOMNode(this); + this.setState({ domNode: _reactDom2.default.findDOMNode(this) }); // we need to refreshState because we didn't have access to the DOM node before this.refreshState(); // add events - this.options.container.addEventListener('scroll', this.refreshState); - this.options.container.addEventListener('resize', this.refreshState); + container.addEventListener('scroll', this.refreshState); + container.addEventListener('resize', this.refreshState); } }, { key: 'componentWillUnmount', value: function componentWillUnmount() { + var container = this.state.options.container; + + this._isMounted = false; // remove events - this.options.container.removeEventListener('scroll', this.refreshState); - this.options.container.removeEventListener('resize', this.refreshState); + container.removeEventListener('scroll', this.refreshState); + container.removeEventListener('resize', this.refreshState); } }, { - key: 'componentWillReceiveProps', + key: 'render', + value: function render() { + return _react2.default.createElement(InnerComponent, _extends({}, this.props, mapVirtualToProps(this.props, this.state))); + } + }], [{ + key: 'setStateIfNeeded', + value: function setStateIfNeeded(list, container, items, itemHeight, itemBuffer, firstItemIndex, lastItemIndex) { + // get first and lastItemIndex + var state = (0, _getVisibleItemBounds2.default)(list, container, items, itemHeight, itemBuffer); + if (state === undefined) { + return null; + } + + if (state.firstItemIndex > state.lastItemIndex) { + return null; + } + + if (state.firstItemIndex !== firstItemIndex || state.lastItemIndex !== lastItemIndex) { + return state; + } + } // if props change, just assume we have to recalculate - value: function componentWillReceiveProps(nextProps) { + + }, { + key: 'getDerivedStateFromProps', + value: function getDerivedStateFromProps(nextProps, nextState) { var itemHeight = nextProps.itemHeight, items = nextProps.items, itemBuffer = nextProps.itemBuffer; + var domNode = nextState.domNode, + container = nextState.options.container, + firstItemIndex = nextState.firstItemIndex, + lastItemIndex = nextState.lastItemIndex; - this.setStateIfNeeded(this.domNode, this.options.container, items, itemHeight, itemBuffer); - } - }, { - key: 'render', - value: function render() { - return _react2.default.createElement(InnerComponent, _extends({}, this.props, mapVirtualToProps(this.props, this.state))); + var state = VList.setStateIfNeeded(domNode, container, items, itemHeight, itemBuffer, firstItemIndex, lastItemIndex); + + if (state === undefined) { + return null; + } + + return state; } }]); - return vlist; + return VList; }(_react.PureComponent), _class.propTypes = { items: _propTypes2.default.array.isRequired, itemHeight: _propTypes2.default.number.isRequired, diff --git a/lib/utils/defaultMapVirtualToProps.js b/lib/utils/defaultMapVirtualToProps.js index 681f6e0..24aab04 100644 --- a/lib/utils/defaultMapVirtualToProps.js +++ b/lib/utils/defaultMapVirtualToProps.js @@ -18,6 +18,8 @@ var defaultMapToVirtualProps = function defaultMapToVirtualProps(_ref, _ref2) { return { virtual: { items: visibleItems, + firstItemIndex: firstItemIndex, + lastItemIndex: lastItemIndex, style: { height: height, paddingTop: paddingTop, diff --git a/lib/utils/getVisibleItemBounds.js b/lib/utils/getVisibleItemBounds.js index c0b2b34..14416dd 100644 --- a/lib/utils/getVisibleItemBounds.js +++ b/lib/utils/getVisibleItemBounds.js @@ -16,10 +16,9 @@ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { de var getVisibleItemBounds = function getVisibleItemBounds(list, container, items, itemHeight, itemBuffer) { // early return if we can't calculate - if (!container) return undefined; - if (!itemHeight) return undefined; - if (!items) return undefined; - if (items.length === 0) return undefined; + if (!container || !itemHeight || !items || items.length === 0) { + return null; + } // what the user can see var innerHeight = container.innerHeight, @@ -28,7 +27,7 @@ var getVisibleItemBounds = function getVisibleItemBounds(list, container, items, var viewHeight = innerHeight || clientHeight; // how many pixels are visible - if (!viewHeight) return undefined; + if (!viewHeight) return null; var viewTop = (0, _getElementTop2.default)(container); // top y-coordinate of viewport inside container var viewBottom = viewTop + viewHeight; diff --git a/package.json b/package.json index 3e51e60..ef25e99 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "webpack": "^1.13.3" }, "peerDependencies": { - "react": "^15.0.0 || ^16.0.0", - "react-dom": "^15.0.0 || ^16.0.0" + "react": "^16.0.0", + "react-dom": "^16.0.0" } } diff --git a/src/VirtualList.js b/src/VirtualList.js index a2a74d5..12877b0 100644 --- a/src/VirtualList.js +++ b/src/VirtualList.js @@ -7,100 +7,116 @@ import throttleWithRAF from './utils/throttleWithRAF'; import defaultMapToVirtualProps from './utils/defaultMapVirtualToProps'; const VirtualList = (options, mapVirtualToProps = defaultMapToVirtualProps) => (InnerComponent) => { - return class vlist extends PureComponent { - static propTypes = { - items: PropTypes.array.isRequired, - itemHeight: PropTypes.number.isRequired, - itemBuffer: PropTypes.number, - }; - - static defaultProps = { - itemBuffer: 0, - }; - - _isMounted = false; - + return class VList extends PureComponent { constructor(props) { super(props); - this.options = { - container: typeof window !== 'undefined' ? window : undefined, - ...options, - }; - this.state = { firstItemIndex: 0, lastItemIndex: -1, + options: { + container: typeof window !== 'undefined' ? window : undefined, + ...options, + } }; - // initialState allows us to set the first/lastItemIndex (useful for server-rendering) - if (options && options.initialState) { - this.state = { - ...this.state, - ...options.initialState, - }; - } - - this.refreshState = this.refreshState.bind(this); - // if requestAnimationFrame is available, use it to throttle refreshState if (typeof window !== 'undefined' && 'requestAnimationFrame' in window) { this.refreshState = throttleWithRAF(this.refreshState); } }; - setStateIfNeeded(list, container, items, itemHeight, itemBuffer) { - // get first and lastItemIndex - const state = getVisibleItemBounds(list, container, items, itemHeight, itemBuffer); - - if (state === undefined) { return; } - - if (state.firstItemIndex > state.lastItemIndex) { return; } + static propTypes = { + items: PropTypes.array.isRequired, + itemHeight: PropTypes.number.isRequired, + itemBuffer: PropTypes.number, + }; - if (state.firstItemIndex !== this.state.firstItemIndex || state.lastItemIndex !== this.state.lastItemIndex) { - this.setState(state); - } - } + static defaultProps = { + itemBuffer: 0, + }; - refreshState() { - if (!this._isMounted) { - return; - } - - const { itemHeight, items, itemBuffer } = this.props; + _isMounted = false; - this.setStateIfNeeded(this.domNode, this.options.container, items, itemHeight, itemBuffer); - }; + componentDidMount() { + const { options: { container } } = this.state - componentWillMount() { this._isMounted = true; - } - componentDidMount() { // cache the DOM node - this.domNode = ReactDOM.findDOMNode(this); + this.setState({domNode: ReactDOM.findDOMNode(this)}); // we need to refreshState because we didn't have access to the DOM node before this.refreshState(); // add events - this.options.container.addEventListener('scroll', this.refreshState); - this.options.container.addEventListener('resize', this.refreshState); + container.addEventListener('scroll', this.refreshState); + container.addEventListener('resize', this.refreshState); }; componentWillUnmount() { + const { options: { container } } = this.state + this._isMounted = false; // remove events - this.options.container.removeEventListener('scroll', this.refreshState); - this.options.container.removeEventListener('resize', this.refreshState); + container.removeEventListener('scroll', this.refreshState); + container.removeEventListener('resize', this.refreshState); }; + static setStateIfNeeded(list, container, items, itemHeight, itemBuffer, firstItemIndex, lastItemIndex) { + // get first and lastItemIndex + const state = getVisibleItemBounds(list, container, items, itemHeight, itemBuffer); + + if (state === undefined) { return null; } + + if (state.firstItemIndex > state.lastItemIndex) { return null; } + + if (state.firstItemIndex !== firstItemIndex || state.lastItemIndex !== lastItemIndex) { + return state; + } + } + // if props change, just assume we have to recalculate - componentWillReceiveProps(nextProps) { + static getDerivedStateFromProps(nextProps, nextState) { const { itemHeight, items, itemBuffer } = nextProps; + const { domNode, options: { container }, firstItemIndex, lastItemIndex } = nextState + + const state = VList.setStateIfNeeded( + domNode, + container, + items, + itemHeight, + itemBuffer, + firstItemIndex, + lastItemIndex, + ); + + if (state === undefined) { + return null; + } - this.setStateIfNeeded(this.domNode, this.options.container, items, itemHeight, itemBuffer); + return state; + }; + + refreshState = () => { + if (!this._isMounted) { + return; + } + + const { itemHeight, items, itemBuffer } = this.props; + const { domNode, options: { container }, firstItemIndex, lastItemIndex } = this.state + const state = VList.setStateIfNeeded( + domNode, + container, + items, + itemHeight, + itemBuffer, + firstItemIndex, + lastItemIndex, + ); + + this.setState(state) }; render() { diff --git a/src/utils/defaultMapVirtualToProps.js b/src/utils/defaultMapVirtualToProps.js index f64f79b..0abeb19 100644 --- a/src/utils/defaultMapVirtualToProps.js +++ b/src/utils/defaultMapVirtualToProps.js @@ -14,6 +14,8 @@ const defaultMapToVirtualProps = ({ return { virtual: { items: visibleItems, + firstItemIndex, + lastItemIndex, style: { height, paddingTop, diff --git a/src/utils/getVisibleItemBounds.js b/src/utils/getVisibleItemBounds.js index 5f88a75..ccadecd 100644 --- a/src/utils/getVisibleItemBounds.js +++ b/src/utils/getVisibleItemBounds.js @@ -3,17 +3,16 @@ import getElementTop from './getElementTop'; const getVisibleItemBounds = (list, container, items, itemHeight, itemBuffer) => { // early return if we can't calculate - if (!container) return undefined; - if (!itemHeight) return undefined; - if (!items) return undefined; - if (items.length === 0) return undefined; + if (!container || !itemHeight || !items || items.length === 0) { + return null + } // what the user can see const { innerHeight, clientHeight } = container; const viewHeight = innerHeight || clientHeight; // how many pixels are visible - if (!viewHeight) return undefined; + if (!viewHeight) return null; const viewTop = getElementTop(container); // top y-coordinate of viewport inside container const viewBottom = viewTop + viewHeight;