I have to make request to an API when window.scrollTop < 20
. On success, I prepend a new child component to the top of .list
component.
I use throttle
to prevent multiple API calls that occurs due to inertia of scroll. It works well if fetch duration is close to throttle duration and both are greater than 800 ( I am not sure why this works well only when duration is larger than 800, not necessarily but works well for large duration). It does not work well otherwise - say duration is close to 300.
Behaviour -
- due to inertia scrolling, window continues to scroll after successful fetch and
window.scrollTop
once again is less than20
that makes another API call. or, window.scrollTop
is less than 20 and scroll bar hits top.
Parameters on which this behaviour depends is:
- momentum of scroll
- height of prepended list child.
- throttle duration
- fetch duration
To end inertia of scroll, I tried overflow: none
. This makes UI unresponsive for fetch duration that is not acceptable.
I maintain the position of scrollTop
that was at the time of fetch request , for which I do
window.scrollTo(0, scrollHeight - state.docScrollHeight). I record the position of scrollTop at the time of fetch request in
state.docScrollHeight`.
I want to prevent multiple API calls and maintain scroll position.
Fiddle: https://jsfiddle.net/05mwb3hq/1/
This issue is best reproduced in developer tool.
var state = {
loadingMore: false,
docScrollHeight: 0,
lastCall: 0,
lastCallToFetch: 0,
}
var count = 0;
function createEl() {
let el = document.createElement('div');
el.classList.add('list-comp');
let elNum = document.createElement('span');
elNum.textContent = count;
elNum.classList.add('elNum');
el.appendChild(elNum);
const listEl = document.querySelector('#list');
listEl.prepend(el);
count++;
}
function mockApi(lagDuration) {
return new Promise(resolve => {
setTimeout(resolve, lagDuration)
});
}
function throttle(fetchCb, throttleDuration) {
let previousCall = state.lastCall;
state.lastCall = Date.now();
if (previousCall === undefined || (state.lastCall - previousCall) > throttleDuration) {
state.lastCallToFetch = Date.now();
state.loadingMore = true;
document.querySelector('.loader').classList.remove('d-none');
fetchCb(300)
.then(() => {
createEl();
state.loadingMore = false;
})
.then(() => {
const {
scrollHeight
} = document.documentElement;
if (!state.loadingMore) {
window.scrollTo(0, scrollHeight - state.docScrollHeight);
document.querySelector('.loader').classList.add('d-none');
}
});
}
}
var onScrollList = function on_scroll_list() {
if (window.scrollY < 20) {
if (!state.loadingMore) {
state.docScrollHeight = document.documentElement.scrollHeight;
state.loagingMore = true;
throttle(mockApi, 800);
}
}
}
window.addEventListener('scroll', onScrollList);
window.onload = function() {
window.scrollTo(0, document.body.clientHeight);
}
* {
box-sizing: border-box;
}
.d-none {
display: none;
}
#main-scrollable-comp {
overflow-y: scroll;
background-color: grey;
}
#default-comp {
background-color: red;
height: 800px;
}
.list-comp {
height: 200px;
width: 100%;
background-color: purple;
border: 2px solid yellow;
display: flex;
align-items: flex-end;
justify-content: center;
}
.elNum {
color: green;
font-size: 54px;
}
.loader-container {
height: 100px;
background-color: white;
display: flex;
align-items: center;
justify-content: center;
}
.loader {
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
width: 36px;
height: 36px;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>Smooth Scroll</title>
<link href="styles.css" rel="stylesheet">
</head>
<body>
<div id="main-scrollable-comp">
<div class='loader-container'>
<div class='loader d-none'></div>
</div>
<div id='list'></div>
<div id="default-comp"></div>
</div>
</body>
</html>