Skip to content

Commit 4b5321a

Browse files
committed
feat(ObserveViewport): implement deferUpdateUntilIdle option
deferUpdateUntilIdle allows to defer the update as long as no events are collected anymore. Regardless whether the component is only listening for e.g. dimensions updates, it is also defered for scroll updates. This option is useful for updates that are optional and can be defered until the page has time to do so. The onUpdate function is always triggered with the latest viewport state. At the moment the implmentation is based on a simple debounce functionality with a wait of 700ms. In case it makes sense it might get smarter in the future.
1 parent d86e1ae commit 4b5321a

File tree

9 files changed

+95
-41
lines changed

9 files changed

+95
-41
lines changed

examples/index.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,13 @@ class Example extends React.PureComponent<{}, { disabled: boolean }> {
8888
}
8989
}}
9090
/>
91+
<ObserveViewport
92+
deferUpdateUntilIdle
93+
disableScrollUpdates
94+
onUpdate={props => {
95+
console.log('update dimensions lazy', props.dimensions);
96+
}}
97+
/>
9198
<Placeholder />
9299
<Placeholder />
93100
{this.renderButton()}

lib/ConnectViewport.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@ type TPropStrings = 'scroll' | 'dimensions';
1313

1414
interface IOptions {
1515
omit?: TPropStrings[];
16+
deferUpdateUntilIdle?: boolean;
1617
}
1718

1819
export default function connect(options: IOptions = {}) {
20+
const deferUpdateUntilIdle = Boolean(options.deferUpdateUntilIdle);
1921
const omit = options.omit || [];
2022
const shouldOmitScroll = omit.indexOf('scroll') !== -1;
2123
const shouldOmitDimensions = omit.indexOf('dimensions') !== -1;
@@ -33,6 +35,7 @@ export default function connect(options: IOptions = {}) {
3335
<ObserveViewport
3436
disableScrollUpdates={shouldOmitScroll}
3537
disableDimensionsUpdates={shouldOmitDimensions}
38+
deferUpdateUntilIdle={deferUpdateUntilIdle}
3639
>
3740
{({ scroll, dimensions }) => (
3841
<WrappedComponent

lib/ObserveViewport.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ interface IProps {
2727
recalculateLayoutBeforeUpdate?: (props: IChildProps) => any;
2828
disableScrollUpdates: boolean;
2929
disableDimensionsUpdates: boolean;
30+
deferUpdateUntilIdle: boolean;
3031
}
3132

3233
interface IContext {
@@ -55,6 +56,7 @@ export default class ObserveViewport extends React.Component<IProps, IState> {
5556
static defaultProps = {
5657
disableScrollUpdates: false,
5758
disableDimensionsUpdates: false,
59+
deferUpdateUntilIdle: false,
5860
};
5961

6062
constructor(props: IProps) {
@@ -148,6 +150,7 @@ ReactDOM.render(
148150
addViewportChangeListener(this.handleViewportUpdate, {
149151
notifyScroll: () => !this.props.disableScrollUpdates,
150152
notifyDimensions: () => !this.props.disableDimensionsUpdates,
153+
notifyOnlyWhenIdle: () => this.props.deferUpdateUntilIdle,
151154
recalculateLayoutBeforeUpdate: (viewport: IViewport) => {
152155
if (this.props.recalculateLayoutBeforeUpdate) {
153156
return this.props.recalculateLayoutBeforeUpdate(viewport);

lib/ViewportCollector.tsx

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import * as React from 'react';
2-
import debounce from 'lodash.debounce';
32
import memoize from 'memoize-one';
43
import raf from 'raf';
54

@@ -8,14 +7,16 @@ import {
87
shallowEqualPrivateScroll,
98
shallowEqualDimensions,
109
browserSupportsPassiveEvents,
10+
simpleDebounce,
11+
debounceOnUpdate,
1112
} from './utils';
1213

1314
import {
1415
IDimensions,
1516
IPrivateScroll,
1617
IScroll,
1718
IViewport,
18-
IViewportCollectorUpdateOptions,
19+
OnUpdateType,
1920
} from './types';
2021

2122
export const SCROLL_DIR_DOWN = Symbol('SCROLL_DIR_DOWN');
@@ -136,10 +137,8 @@ export const createInitDimensionsState = (): IDimensions => {
136137
};
137138

138139
interface IProps {
139-
onUpdate: (
140-
props: IViewport,
141-
options: IViewportCollectorUpdateOptions,
142-
) => void;
140+
onUpdate: OnUpdateType;
141+
onIdledUpdate?: OnUpdateType;
143142
}
144143

145144
export default class ViewportCollector extends React.PureComponent<IProps> {
@@ -211,7 +210,7 @@ export default class ViewportCollector extends React.PureComponent<IProps> {
211210
this.componentMightHaveUpdated = true;
212211
};
213212

214-
handleResize = debounce(() => {
213+
handleResize = simpleDebounce(() => {
215214
Object.assign(this.dimensionsState, getClientDimensions());
216215

217216
this.componentMightHaveUpdated = true;
@@ -251,9 +250,19 @@ export default class ViewportCollector extends React.PureComponent<IProps> {
251250
scrollDidUpdate,
252251
dimensionsDidUpdate,
253252
});
253+
this.updateOnIdle(publicState, {
254+
scrollDidUpdate,
255+
dimensionsDidUpdate,
256+
});
254257
}
255258
};
256259

260+
updateOnIdle = debounceOnUpdate((...args) => {
261+
if (typeof this.props.onIdledUpdate === 'function') {
262+
this.props.onIdledUpdate(...args);
263+
}
264+
}, 700);
265+
257266
getPropsFromState(): IViewport {
258267
return {
259268
scroll: this.getPublicScroll(

lib/ViewportProvider.tsx

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,21 @@ export default class ViewportProvider extends React.PureComponent<
4343
clearTimeout(this.updateListenersTick);
4444
}
4545

46-
updateListeners = (
46+
triggerUpdateToListeners = (
4747
publicState: IViewport,
4848
{ scrollDidUpdate, dimensionsDidUpdate }: IViewportCollectorUpdateOptions,
49+
options?: { isIdle: boolean },
4950
) => {
51+
const { isIdle } = Object.assign({ isIdle: false }, options);
5052
const updatableListeners = this.listeners.filter(
51-
({ notifyScroll, notifyDimensions }) =>
52-
(notifyScroll() && scrollDidUpdate) ||
53-
(notifyDimensions() && dimensionsDidUpdate),
53+
({ notifyScroll, notifyDimensions, notifyOnlyWhenIdle }) => {
54+
if (notifyOnlyWhenIdle() && !isIdle) {
55+
return false;
56+
}
57+
const updateForScroll = notifyScroll() && scrollDidUpdate;
58+
const updateForDimensions = notifyDimensions() && dimensionsDidUpdate;
59+
return updateForScroll || updateForDimensions;
60+
},
5461
);
5562
const layouts = updatableListeners.map(
5663
({ recalculateLayoutBeforeUpdate }) => {
@@ -100,7 +107,12 @@ export default class ViewportProvider extends React.PureComponent<
100107
return (
101108
<React.Fragment>
102109
{this.state.hasListeners && (
103-
<ViewportCollector onUpdate={this.updateListeners} />
110+
<ViewportCollector
111+
onUpdate={this.triggerUpdateToListeners}
112+
onIdledUpdate={(state, updates) =>
113+
this.triggerUpdateToListeners(state, updates, { isIdle: true })
114+
}
115+
/>
104116
)}
105117
<ViewportContext.Provider value={value}>
106118
{this.props.children}

lib/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,16 @@ export type TViewportChangeHandler = (
6262
export interface IViewportChangeOptions {
6363
notifyScroll: () => boolean;
6464
notifyDimensions: () => boolean;
65+
notifyOnlyWhenIdle: () => boolean;
6566
recalculateLayoutBeforeUpdate?: (viewport: IViewport) => any;
6667
}
6768

6869
export interface IViewportCollectorUpdateOptions {
6970
scrollDidUpdate: boolean;
7071
dimensionsDidUpdate: boolean;
7172
}
73+
74+
export type OnUpdateType = (
75+
props: IViewport,
76+
options: IViewportCollectorUpdateOptions,
77+
) => void;

lib/utils.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import { IRect, IScroll, IPrivateScroll, IDimensions } from './types';
1+
import {
2+
IRect,
3+
IScroll,
4+
IPrivateScroll,
5+
IDimensions,
6+
OnUpdateType,
7+
} from './types';
28

39
export const shallowEqualScroll = (a: IScroll, b: IScroll) => {
410
if (a === b) {
@@ -79,3 +85,37 @@ export const browserSupportsPassiveEvents = (() => {
7985
}
8086
return supportsPassive;
8187
})();
88+
89+
export const simpleDebounce = <F extends (...args: any[]) => any>(
90+
fn: F,
91+
delay: number,
92+
): F => {
93+
let timeout: NodeJS.Timer;
94+
return ((...args: any[]) => {
95+
clearTimeout(timeout);
96+
timeout = setTimeout(fn, delay, ...args);
97+
}) as F;
98+
};
99+
100+
export const debounceOnUpdate = (
101+
fn: OnUpdateType,
102+
delay: number,
103+
): OnUpdateType => {
104+
let timeout: NodeJS.Timer;
105+
let scrollDidUpdate = false;
106+
let dimensionsDidUpdate = false;
107+
108+
return (viewport, options) => {
109+
clearTimeout(timeout);
110+
scrollDidUpdate = scrollDidUpdate || options.scrollDidUpdate;
111+
dimensionsDidUpdate = dimensionsDidUpdate || options.dimensionsDidUpdate;
112+
timeout = setTimeout(() => {
113+
fn(viewport, {
114+
scrollDidUpdate,
115+
dimensionsDidUpdate,
116+
});
117+
scrollDidUpdate = false;
118+
dimensionsDidUpdate = false;
119+
}, delay);
120+
};
121+
};

package-lock.json

Lines changed: 2 additions & 25 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,6 @@
3131
],
3232
"license": "MIT",
3333
"devDependencies": {
34-
"@types/lodash.debounce": "^4.0.4",
35-
"@types/lodash.throttle": "^4.1.4",
3634
"@types/memoize-one": "^3.1.1",
3735
"@types/node": "^10.12.0",
3836
"@types/react": "^16.4.18",
@@ -51,7 +49,6 @@
5149
"typescript": "^3.1.3"
5250
},
5351
"dependencies": {
54-
"lodash.debounce": "^4.0.8",
5552
"memoize-one": "^4.0.2",
5653
"raf": "^3.4.0",
5754
"recompose": "^0.30.0"

0 commit comments

Comments
 (0)