From 19bd63b5f8f0774bd1e7233d82c5e54eca56754e Mon Sep 17 00:00:00 2001 From: Austin McGee <947888+amcgee@users.noreply.github.com> Date: Tue, 19 Feb 2019 11:00:43 +0000 Subject: [PATCH] feat: progressive dashboard loading (#244) This restricts the loading of dashboard items to the current viewport, with a buffer of one half-viewport above and below. As such, only 1.5x the viewport is initially loaded. Other items will be loaded when the user scrolls them into this buffered viewport. * feat: progressive dashboard loading * feat: support initial buffer factor * feat: support progressive loading after a filter change * fix: use margin instead of padding to prevent overflow * Move memoize to its own file * chore: remove unused import --- package.json | 1 + src/__tests__/memoizeOne.spec.js | 29 ++++++ .../Item/ProgressiveLoadingContainer.js | 91 +++++++++++++++++++ src/components/Item/VisualizationItem/Item.js | 66 +++++++------- src/components/ItemGrid/ItemGrid.css | 2 +- src/modules/memoizeOne.js | 23 +++++ yarn.lock | 5 - 7 files changed, 178 insertions(+), 39 deletions(-) create mode 100644 src/__tests__/memoizeOne.spec.js create mode 100644 src/components/Item/ProgressiveLoadingContainer.js create mode 100644 src/modules/memoizeOne.js diff --git a/package.json b/package.json index e94888e4a..f9985fc5e 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "jest": "^23.6.0", "jsdoc": "^3.5.5", "lint-staged": "^7.0.0", + "lodash": "^4.17.11", "material-design-icons": "^3.0.1", "material-ui": "^0.20.0", "object-assign": "4.1.1", diff --git a/src/__tests__/memoizeOne.spec.js b/src/__tests__/memoizeOne.spec.js new file mode 100644 index 000000000..b23b9f63b --- /dev/null +++ b/src/__tests__/memoizeOne.spec.js @@ -0,0 +1,29 @@ +import memoizeOne from '../modules/memoizeOne'; + +describe('memoizeOne', () => { + it('Should return the same value when called twice with shallow-equal arguments', () => { + const object = { a: 0, b: 0 }; + const fn = x => x.a + x.b; + const memoizedFn = memoizeOne(fn); + + const val0 = memoizedFn(object); + expect(val0).toBe(0); + object.a = 1; // maintain shallow equality + expect(fn(object)).toBe(1); + const val1 = memoizedFn(object); + expect(val1).toBe(0); + }); + + it('Should forget the first value when called thrice', () => { + const object = { a: 0, b: 0 }; + const memoizedFn = memoizeOne(x => x.a + x.b); + + const val0 = memoizedFn(object); + expect(val0).toBe(0); + object.a = 1; // maintain shallow equality + const val1 = memoizedFn({ a: 42, b: 84 }); // This will bump val0 out of the cache + expect(val1).toBe(42 + 84); + const val2 = memoizedFn(object); + expect(val2).toBe(1); + }); +}); diff --git a/src/components/Item/ProgressiveLoadingContainer.js b/src/components/Item/ProgressiveLoadingContainer.js new file mode 100644 index 000000000..b26aa5536 --- /dev/null +++ b/src/components/Item/ProgressiveLoadingContainer.js @@ -0,0 +1,91 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import debounce from 'lodash/debounce'; + +const defaultDebounceMs = 100; +const defaultBufferPx = 0; +const defaultInitialBufferFactor = 0.5; + +class ProgressiveLoadingContainer extends Component { + static propTypes = { + children: PropTypes.node.isRequired, + debounceMs: PropTypes.number, + bufferPx: PropTypes.number, + initialBufferFactor: PropTypes.number, + }; + static defaultProps = { + debounceMs: defaultDebounceMs, + bufferPx: defaultBufferPx, + initialBufferFactor: defaultInitialBufferFactor, + }; + + state = { + shouldLoad: false, + }; + containerRef = null; + shouldLoadHandler = null; + + checkShouldLoad(customBufferPx) { + const bufferPx = customBufferPx || this.props.bufferPx; + + if (!this.containerRef) { + return; + } + + const rect = this.containerRef.getBoundingClientRect(); + if ( + rect.bottom > -bufferPx && + rect.top < window.innerHeight + bufferPx + ) { + this.setState({ + shouldLoad: true, + }); + + this.removeHandler(); + } + } + + registerHandler() { + this.shouldLoadHandler = debounce( + () => this.checkShouldLoad(), + this.props.debounceMs + ); + + window.addEventListener('scroll', this.shouldLoadHandler); + } + removeHandler() { + window.removeEventListener('scroll', this.shouldLoadHandler); + } + + componentDidMount() { + this.registerHandler(); + + const initialBufferPx = this.props.initialBufferFactor + ? this.props.initialBufferFactor * window.innerHeight + : undefined; + this.checkShouldLoad(initialBufferPx); + } + + componentWillUnmount() { + this.removeHandler(); + } + + render() { + const { + children, + debounceMs, + bufferPx, + initialBufferFactor, + ...props + } = this.props; + const { shouldLoad } = this.state; + + return ( +