Skip to content

Commit b69a378

Browse files
committed
throttleTimeout param + cut down unnecessary renders + throttle if number input
1 parent 7141ff7 commit b69a378

File tree

1 file changed

+121
-48
lines changed

1 file changed

+121
-48
lines changed

src/index.js

Lines changed: 121 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState, useMemo, useCallback, useRef, useLayoutEffect, } from 'react';
1+
import { useState, useMemo, useCallback, useRef, useLayoutEffect } from 'react';
22

33
function getVpWidth() {
44
return (typeof window != 'undefined') ? Math.max(
@@ -44,6 +44,57 @@ const resolverMap = new Map();
4444
let vpWidth = getVpWidth();
4545
let vpHeight = getVpHeight();
4646

47+
/**
48+
* called to update resize dimensions with the handlers
49+
* passed themselves; separated from the actual
50+
* pub-sub caller (onResize) so we can individually
51+
* dispatch subscription events and avoid updating all
52+
* components at once
53+
*
54+
* @param {*} listener
55+
* @param {*} vpWidth
56+
* @param {*} vpHeight
57+
*/
58+
function triggerResizeListener(listener, vpWidth, vpHeight) {
59+
const params = { vpW: vpWidth, vpH: vpHeight };
60+
61+
let shouldRun = false;
62+
let hash;
63+
64+
const { options, prevHash=undefined } = resolverMap?.get(listener) || {};
65+
const { hasher } = options;
66+
67+
if(!options?.hasher) {
68+
const dimensionsUpdated = new Set();
69+
70+
switch (options?.dimension) {
71+
case 'w':
72+
hash = `${vpWidth}`;
73+
break;
74+
case 'h':
75+
hash = `${vpHeight}`;
76+
break;
77+
default:
78+
case 'both':
79+
hash = `${vpWidth}_${vpHeight}`;
80+
break;
81+
}
82+
}
83+
else {
84+
hash = hasher(params);
85+
}
86+
87+
if(hash != prevHash) { shouldRun = true }
88+
89+
if(shouldRun) {
90+
const state = { ...params, options, hash };
91+
resolverMap.set(listener, {
92+
options, prevHash: hash, prevState: state
93+
});
94+
listener(state);
95+
}
96+
}
97+
4798
/**
4899
* called to update resize dimensions;
49100
* only called once throughout hooks so if
@@ -55,81 +106,103 @@ function onResize() {
55106
vpWidth = getVpWidth();
56107
vpHeight = getVpHeight();
57108

58-
listeners.forEach(listener => {
59-
const params = { vpW: vpWidth, vpH: vpHeight };
60-
61-
let shouldRun = false;
62-
let hash;
63-
64-
const { options, prevHash=undefined } = resolverMap?.get(listener) || {};
65-
const { hasher } = options;
66-
67-
if(!options?.hasher) {
68-
shouldRun = true;
69-
}
70-
else {
71-
hash = hasher(params);
72-
if(hash != prevHash) { shouldRun = true }
73-
}
74-
75-
if(shouldRun) {
76-
resolverMap.set(listener, { options, prevHash: hash });
77-
listener({ ...params, options, hash });
78-
}
79-
});
109+
listeners.forEach(listener =>
110+
triggerResizeListener(listener, vpWidth, vpHeight)
111+
);
80112
}
81113

82114
// =============== //
83115
// the Hook //
84116
// =============== //
85117

86-
/**
87-
* observes the viewport. If input not specified,
88-
* returns the [width, height] when the window changes.
89-
* If input is specified as a number, it returns the [width, height].
90-
*
91-
* If the input is specified as a function, it accepts { vpW, vpH }
92-
* and will only return a new value and update when the value
93-
* computed changes.
94-
*
95-
* @input {Function|Number} input
96-
*/
118+
function getInitialState(options, vpW, vpH) {
119+
let returnValue = {};
120+
if(options.hasher) {
121+
returnValue = options.hasher({ vpW, vpH });
122+
} else {
123+
returnValue = { vpW, vpH };
124+
}
125+
126+
return (!options.hasher ?
127+
{ vpW, vpH } :
128+
hasher && hasher({ vpW: vpWidth, vpH: vpHeight })
129+
)
130+
}
131+
97132
function useViewportSizes(input) {
98133
const hasher = ((typeof input == 'function') ?
99134
input :
100135
input?.hasher
101136
) || undefined;
102137

103-
const debounceTimeout = ((typeof input == 'number') ?
138+
const debounceTimeout = ((typeof input?.debounceTimeout == 'number') ?
139+
input?.debounceTimeout : 0
140+
);
141+
142+
const throttleTimeout = ((typeof input == 'number') ?
104143
input :
105-
input?.debounceTimeout
144+
input?.throttleTimeout
106145
) || 0;
107146

108147
const dimension = input?.dimension || 'both';
109148

110149
const options = {
111-
...(typeof input == 'function' ? {} : input),
150+
...(typeof input == 'object' ? input : {}),
112151
dimension,
113152
hasher
114153
};
115154

116-
const [state, setState] = useState(() => (!hasher ?
117-
{ vpW: vpWidth, vpH: vpHeight } :
118-
hasher && hasher({ vpW: vpWidth, vpH: vpHeight })
119-
));
155+
const [state, setState] = useState(() => getInitialState(options));
120156
const debounceTimeoutRef = useRef(undefined);
121-
const listener = useCallback((!debounceTimeout ?
122-
state => setState(state) :
123-
state => {
157+
const throttleTimeoutRef = useRef(undefined);
158+
const lastThrottledRef = useRef(undefined);
159+
160+
const listener = useCallback(nextState => {
161+
if(!debounceTimeout && !throttleTimeout) {
162+
setState(nextState);
163+
return;
164+
}
165+
166+
if(debounceTimeout) {
124167
if(debounceTimeoutRef.current) {
125168
clearTimeout(debounceTimeoutRef.current);
126169
}
127170

128-
debounceTimeoutRef.current = setTimeout(() => {
129-
setState(state);
130-
}, debounceTimeout);
171+
debounceTimeoutRef.current = setTimeout(() => (
172+
setState(nextState)
173+
), debounceTimeout);
174+
}
175+
176+
if(throttleTimeout) {
177+
const lastTick = lastThrottledRef.current;
178+
const timeSinceLast = (!lastTick ? throttleTimeout : Date.now() - lastTick);
179+
console.log('should process in ->', throttleTimeout - timeSinceLast);
180+
181+
throttleTimeoutRef.current = setTimeout(() => {
182+
lastThrottledRef.current = new Date().getTime();
183+
const vpWidth = getVpWidth();
184+
const vpHeight = getVpHeight();
185+
186+
const dimensionsUpdated = new Set();
187+
188+
if(vpHeight != state.vpH) {
189+
dimensionsUpdated.add('h');
190+
}
191+
192+
if(vpWidth != state.vpW) {
193+
dimensionsUpdated.add('w');
194+
}
195+
196+
if(dimensionsUpdated.has('w') || dimensionsUpdated.has('h')) {
197+
dimensionsUpdated.add('both');
198+
}
199+
200+
if(dimensionsUpdated.has(dimension)) {
201+
setState({ vpW: vpWidth, vpH: vpHeight });
202+
}
203+
}, Math.max(0, throttleTimeout - timeSinceLast));
131204
}
132-
), [debounceTimeoutRef, hasher, dimension]);
205+
}, [debounceTimeoutRef, hasher, dimension, state]);
133206

134207
useLayoutEffect(() => {
135208
resolverMap.set(listener, {

0 commit comments

Comments
 (0)