From 0bc6de13626aee553b33ee12c442255f9c4da70a Mon Sep 17 00:00:00 2001 From: Paul Kaplan <pkaplan@media.mit.edu> Date: Wed, 23 Jan 2019 14:30:41 -0500 Subject: [PATCH] Throttle updates to assets on sprite selector item via HOC Adds unit tests for this HOC. --- .../sprite-selector/sprite-list.jsx | 5 +- src/lib/throttled-property-hoc.jsx | 43 +++++++++++++++ .../unit/util/throttled-property-hoc.test.jsx | 54 +++++++++++++++++++ 3 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 src/lib/throttled-property-hoc.jsx create mode 100644 test/unit/util/throttled-property-hoc.test.jsx diff --git a/src/components/sprite-selector/sprite-list.jsx b/src/components/sprite-selector/sprite-list.jsx index 3f8910fe5..2c9ad0ccc 100644 --- a/src/components/sprite-selector/sprite-list.jsx +++ b/src/components/sprite-selector/sprite-list.jsx @@ -8,9 +8,12 @@ import Box from '../box/box.jsx'; import SpriteSelectorItem from '../../containers/sprite-selector-item.jsx'; import SortableHOC from '../../lib/sortable-hoc.jsx'; import SortableAsset from '../asset-panel/sortable-asset.jsx'; +import ThrottledPropertyHOC from '../../lib/throttled-property-hoc.jsx'; import styles from './sprite-selector.css'; +const ThrottledSpriteSelectorItem = ThrottledPropertyHOC('asset', 500, SpriteSelectorItem); + const SpriteList = function (props) { const { containerRef, @@ -74,7 +77,7 @@ const SpriteList = function (props) { onAddSortable={onAddSortable} onRemoveSortable={onRemoveSortable} > - <SpriteSelectorItem + <ThrottledSpriteSelectorItem asset={sprite.costume && sprite.costume.asset} className={classNames(styles.sprite, { [styles.raised]: isRaised, diff --git a/src/lib/throttled-property-hoc.jsx b/src/lib/throttled-property-hoc.jsx new file mode 100644 index 000000000..cd7cd8ed8 --- /dev/null +++ b/src/lib/throttled-property-hoc.jsx @@ -0,0 +1,43 @@ +import React from 'react'; + +/* Higher Order Component to throttle updates to specific props. + * Why? Because certain prop updates are expensive, and need to be throttled. + * This allows renders when other properties change, and will use the last + * rendered value of a prop for comparison. + * @param {string} propName the name of the prop to throttle updates from. + * @param {string} throttleTime the minimum time between updates to that specific property. + * @param {React.Component} WrappedComponent component who will not update for certain props. + * @returns {React.Component} component with throttling behavior + */ +const ThrottledPropertyHOC = function (propName, throttleTime, WrappedComponent) { + class ThrottledPropertyWrapper extends React.Component { + shouldComponentUpdate (nextProps) { + for (const property in nextProps) { + if (property !== propName && this.props[property] !== nextProps[property]) { + return true; // Always update if another property has changed + } + } + + // If only that prop has changed, allow update to go to render based + // on _lastRenderedTime and _lastRenderTime are updated in render + if (nextProps[propName] !== this._lastRenderedValue && + Date.now() - this._lastRenderTime > throttleTime + ) { + return true; // Allow this update to go to render + } + + return false; + } + render () { + this._lastRenderTime = Date.now(); + this._lastRenderedValue = this.props[propName]; + return ( + <WrappedComponent {...this.props} /> + ); + } + } + + return ThrottledPropertyWrapper; +}; + +export default ThrottledPropertyHOC; diff --git a/test/unit/util/throttled-property-hoc.test.jsx b/test/unit/util/throttled-property-hoc.test.jsx new file mode 100644 index 000000000..37fca7022 --- /dev/null +++ b/test/unit/util/throttled-property-hoc.test.jsx @@ -0,0 +1,54 @@ +import React from 'react'; +import {mount} from 'enzyme'; + +import ThrottledPropertyHOC from '../../../src/lib/throttled-property-hoc.jsx'; + +describe('VMListenerHOC', () => { + let mounted; + const throttleTime = 500; + beforeEach(() => { + const Component = ({propToThrottle, doNotThrottle}) => ( + <input + name={doNotThrottle} + value={propToThrottle} + /> + ); + const WrappedComponent = ThrottledPropertyHOC('propToThrottle', throttleTime, Component); + + global.Date.now = () => 0; + + mounted = mount( + <WrappedComponent + doNotThrottle="oldvalue" + propToThrottle={0} + /> + ); + }); + + test('it passes the props on initial render ', () => { + expect(mounted.find('[value=0]').exists()).toEqual(true); + expect(mounted.find('[name="oldvalue"]').exists()).toEqual(true); + }); + + test('it does not rerender if throttled prop is updated too soon', () => { + global.Date.now = () => throttleTime / 2; + mounted.setProps({propToThrottle: 1}); + mounted.update(); + expect(mounted.find('[value=0]').exists()).toEqual(true); + }); + + test('it does rerender if throttled prop is updated after throttle timeout', () => { + global.Date.now = () => throttleTime * 2; + mounted.setProps({propToThrottle: 1}); + mounted.update(); + expect(mounted.find('[value=1]').exists()).toEqual(true); + }); + + test('it does rerender if a non-throttled prop is changed', () => { + global.Date.now = () => throttleTime / 2; + mounted.setProps({doNotThrottle: 'newvalue', propToThrottle: 2}); + mounted.update(); + expect(mounted.find('[name="newvalue"]').exists()).toEqual(true); + expect(mounted.find('[value=2]').exists()).toEqual(true); + }); +}); -- GitLab