At work we're having some performance issues with rendering data tables so we've decided to try to virtualize a list "window". Essentially following the same idea as react-window, whereby you only render the sublist - the one showing on your viewport - of your data list.
For a myriad of reasons, we tried to implement the technique ourselves. In doing so, we learnt this is mostly done using position: absolute on each list item, which didn't really suitable for us. So we came up with the idea of just having two "wrapper" divs around the sublist we want to render.
Essentially box1 would have the height equal to the combined height of all the list items before our window and box2 would have the height of all the items after our window. Every time the user scrolls, we figure out which indices to render and adjust the box heights.
Unfortunately we ran into an issue where, when the user scrolls down, the scroll event keeps firing even after the user has stopped scrolling. This scrolls the list all the way to the end. It seems to work fine when scrolling up though, so we're really at a loss here. We couldn't figure out why it keeps firing.
Here's a link to an example. I just replaced all the list item logic with a fixed box for simplicity. I've also added a timeout to the scroll handler so the scrolling up behaviour is more noticeable, otherwise it's too fast and the red upper box is not noticeable.
Any help is much appreciated. Thanks in advance!
EDIT: We're actually using this in a <table> element, which means solutions based on css position property will not work, given that that property has undefined behaviour for table elements, and it breaks the standard table layout.
The problem is most likely caused by using the scrollTop value to change the height of the items which cause the scrollTop value to change and so on (maybe).
Here is the right way to do it https://codesandbox.io/s/react-hooks-playground-forked-97vsq
import React, { useState, useCallback } from "react";
import ReactDOM from "react-dom";
const ROW_HEIGHT = 25;
const App = (props) => {
const [items, innerHeight, onScroll] = useVirtualizedList({
numItems: 500,
itemHeight: ROW_HEIGHT,
windowHeight: ROW_HEIGHT * 5,
windowExtension: 0
});
return (
<div onScroll={onScroll} style={{ height: "500px", overflowY: "scroll" }}>
<div style={{
// position should be calculated depanding on the parent element position
position: 'fixed'
}}>{items}</div>
<div className="forceOverflow" style={{height: 500 * ROW_HEIGHT}}></div>
</div>
);
};
const useVirtualizedList = ({ numItems, itemHeight, windowHeight }) => {
const [scrollTop, setScrollTop] = useState(0);
const innerHeight = numItems * itemHeight;
const startIndex = Math.floor(scrollTop / itemHeight);
const endIndex = Math.min(
numItems - 1,
startIndex + Math.floor( windowHeight / itemHeight)
);
const onScroll = useCallback((e) => {
const currentScroll = e.currentTarget.scrollTop;
setScrollTop(currentScroll);
}, []);
const items = `${startIndex} --- ${endIndex}`
return [items, innerHeight, onScroll];
};
ReactDOM.render(<App />, document.getElementById("root"));
Related
Basically I want to render a div next to where I click.
I have done that works perfectly but my problem is when I scroll down its not following and render relevant to the page and not the screen.
Any work around my code if this so far:
const [position, setPosition] = useState([0,0])
const renderGuess = (event) =>{
if(guessed){
setPosition([0,0]);
setGuessed(false);
}else{
setPosition([event.clientX, event.clientY]);
setGuessed(true);
}
}
function Guess (position){
const {location} = position
console.log(location)
return(
<div id="guess-div" style={{top: `${location[1]}px`, left: `${location[0]}px`}}>
Note: I'm using react the render works fine only problem is when I scroll down it renders for example 150px from the top of the page not 150px from the screen.
I have tried 150 / 10, with vh and vw.
The event.clientX and event.clientY will get the viewport coordinates, not the document coordinates as you want.
You can replace the
setPosition([event.clientX, event.clientY]);
by
setPosition([event.pageX, event.pageY]);
to get the coordinates relative to the entire document instead.
References:
https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/clientX
https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/pageX
I've been trying to create a wrapper function so that i could load a sprite map into it and have an auto adjusting box that maintains the correct dimensions, rather than stretch the inital image that the sprites were made from. The problem occurs when the children of the wrapper are expanded, it causes the container box to expand but doesnt allow the wrapper to auto adjust its dimensions. This causes one of the borders, either top and bottom, or left and right to be larger/smaller than the other pair, which is not the desired effect.
The wrapper:
const AdaptBox = ({patternID, borderWidth, children}) =>{
const parentRef = useRef(null);
const [pixelHSize, setPixelHSize] = useState(borderWidth);
const [pixelWSize, setPixelWSize] = useState(borderWidth);
const imgs = SPRITE_PATTERNS[patternID];
useEffect ( () => {
console.log("use effect");
if(parentRef.current){
//useEffect hook to get the parents dimensions and then adjust ratio of wrapper
let parentHeight = parentRef.current.scrollHeight;
let parentWidth = parentRef.current.scrollWidth;
console.log(parentHeight);
console.log(parentWidth);
if(parentHeight<parentWidth){
console.log("wide scale");
setPixelHSize(((parentWidth/parentHeight)*borderWidth)/(parentWidth/parentHeight));
setPixelWSize(((parentHeight/parentWidth)*borderWidth));
}
else if(parentHeight>parentWidth){
console.log("tall scale");
setPixelHSize(((parentWidth/parentHeight)*borderWidth));
setPixelWSize(((parentHeight/parentWidth)*borderWidth)*(parentHeight/parentWidth));
}
else{
setPixelHSize(borderWidth);
setPixelWSize(borderWidth);
}
}
},[parentRef, borderWidth]);
return (
<Box img={imgs[4]} ref={parentRef}> //all styled comps
<BoxRow h={pixelHSize} w={100}>
<RowStuff.../>
</BoxRow>
<BoxRow h={100-(2*pixelHSize)} w={100}>
<VSide img={imgs[3]} h={100} w={pixelWSize}/>
<Back >
{children}
</Back>
<VSide img={imgs[5]} h={100} w={pixelWSize}/>
</BoxRow>
<BoxRow h={pixelHSize} w={100}>
<RowStuff.../>
</BoxRow>
</Box>
);
}
Implimentation of the wrapper:
...
<MainBodyContainer> //styled comp
<MainBody img={BGimg}> //styled comp
<AdaptBox patternID={0} borderWidth={1}> //Wrapper
<CoeffDisplay ></CoeffDisplay> //Child comp
</AdaptBox>
</MainBody>
</MainBodyContainer>
...
The "CoeffDisplay" contains a number of boxes that can be clicked on to expand.
Im open to another method of achieving this however i'm a fan of this wrapper style.
I've triple checked the simple maths but i think the issue is related to the use effect not being called on rerender.
Thanks for the help.
I'm trying to build a react Chat app and I was going to use the infinite scroll but it doesn't work the way I wanted it to. So I built one myself. I used useref to determine the height and if it reaches to the top it will add more chats. kinda like fb messenger's chat. The problem now is that when I add more chats, once it reaches the top, the scroll bar will continue to go up and not stick in place unlike how react-infinite scroll works.
May I know how I can go through this? Here is my code.
for the scroll:
const onScroll = () => {
if (topDiv.current) {
const { scrollTop, scrollHeight, clientHeight } = topDiv.current;
if(scrollTop === 0){
props.fecthMoreChat()
}
}
};
return(
<div id='scrollableDiv' ref={topDiv} onScroll={()=> onScroll()} style={{height:'100%', padding:'20px', display:'flex', flexDirection:'column', backgroundColor:'#efefef', overflowY:'scroll'}}>
{/* <div style={{visibility:'hidden'}} ref={topDiv}></div> */}
{currentChat.map((chat, index)=>{
return(
<div key={index} style={{textAlign:chat.sender===username?'right':'left'}}>
{chat.message}
</div>
)
})}
<div style={{visibility:'hidden'}} ref={botMsg}></div>
</div>
)
So the answer was actually pretty simple. I just took the current scroll height, saved it in a variable, then waited for the re-render and took the new scroll height. Basically, just subtract the old scroll height with the new scroll height and use that number to set scroll by using scrollTo(). Here is the code:
const onScroll = async () => {
if (chatDiv.current) {
const { scrollTop, scrollHeight, clientHeight } = chatDiv.current;
if(scrollTop === 0){
const pastScroll = scrollHeight
await props.fecthMoreChat()
const currentScroll = (await chatDiv.current.scrollHeight-pastScroll)
await chatDiv.current.scrollTo(0, currentScroll)
}
}
};
I'm really bad at math, I am trying to calculate the top section of the scrollView as I want it to come on top of the top View to make more place for the scrollView.
I know that I could use Animate.View to accomplish this, But due to a component(RecyclerListView) Im unable to do that. So my Idee is while I scroll Down I move the component ItemList up until is at -150 and when I scroll up, I will move until the value scrollY hit 0.
There is already a post here on Stack(here) That displays what I want.
Here is my code.
<View> this is the View i want to cover/fold <View>
<ItemList style={{top:scrollY}}
onScroll={(nativeEvent, offsetX, offsetY)=>{
// when scroll down, scrollY should not exeede 0
// when i scroll up, scrollY should not be more -150
var maxScroll = 0;
var minScroll = -150;
// How should i calculate and set setScrollY()
}}
columnPerRaw={columnPerRar}
itemHeight={150}
onIni={(s: any) => {
setScroll(s);
}}
onItemPress={(item, index) => itemClick(item as item)}
items={data ?? []}
renderItem={renderItem}
onEndReached={() => {
if (globalContext.value.panination && !isLoading)
setEffectTrigger(Math.random());
}}
keyName="name"
/>
Update
I have done like #Nnay said but its not to smoth
var maxScroll = 0; // when i scroll up, scrollY should not be more then 0
var minScroll = -150; // when scroll down, scrollY should not exeede -150
var top = lastPos - offsetY;
if (top >minScroll && top <=maxScroll)
setScrollY(top);
lastPos = offsetY;
I have done it like
no matter how you choose to trigger this behaviour (be it scroll position or scroll direction), you should probably use conditional classes.
to get the scroll direction, you will need to store the previous scroll position.
if oldPosition - newPosition < 0 you have scrolled down, else you have scrolled up.
if you want the position of the element to trigger it, you should opt for setting a boolean to true or false which triggers the class. something like:
<View class={{this.show ? 'show' : ''}}>
where show is a boolean local to your component.
your show class can then handle the height of the element which will automatically push the following content down (provided it isn't positioned absolute) and you can smoothen this by using transition: .25s in css. otherwise it will jump
your scroll handler could then set the boolean depending on the scroll direction with something like:
this.show = oldPos - newPos > 0;
which will return true if the user has scrolled up which seems to be the behaviour you referenced in the link
let me know if this satisfies your question so I can edit and adapt the answer accordingly
ps: if you just set the top of your element while it has a static position, it won't work. also it will just jump to that position since you're not smoothing anything
EDIT: if CSS animations and transitions don't work and you cannot use react-native's Animate.View, the only option that doesn't require external packages is to set an interval animation if react-native doesn't support the AnimationsAPI
I haven't tried using the AnimationsAPI in react-native yet so I would advise to test that first since it's much more performant.
if you want to use the interval animation, I would still recommend sticking to the boolean solution above, but removing the class and adding a height variable in the style attribute of the <View>
make sure to store your interval in a variable so you can clear it.
if (this.height < 150 /* no idea what height you need, play around */ && show && !this.interval){
this.interval = setInterval(() => {
this.height += 1;
}, 10)
} else if (this.height > 0 && !show && !this.interval) {
this.interval = setInterval(() => {
this.height -= 1;
}, 10)
} else {
clearInterval(this.interval)
}
something like this should work. keep in mind that I haven't used interval animations in years so you might need to play around with some values
I've got the following React app where I'm using react-spring to animate between routes and animate different elements based on the current scroll position.
When I use overflow: scroll on the Home component I'm then unable to return anything from my handleScroll method (it just returns 0):
handleScroll(e) {
let windowScrollPosition = window.scrollY
this.setState({ windowScrollPosition: windowScrollPosition }, () => console.log(this.state.windowScrollPosition, 'this.state.windowScrollPosition'))
}
Is there a way around this?
I need to use overflow: scroll to solve this issue unfortunately.
Any help is much appreciated!
Well that would make sense. If you have set an element to scroll that is 100% height then the window would never scroll the element would.
So you need to get the scroll position from the element with elem.scrollTop
You can create a ref to the element in React
constructor(props) {
super(props);
this.scrollContainer = React.createRef();
}
return (
<div className="scroll-container" ref={this.scrollContainer}></div>
)
And then in your handle scroll method use:
handleScroll(e) {
let windowScrollPosition = this.scrollContainer.current.scrollTop | window.scrollY;
this.setState({ windowScrollPosition: windowScrollPosition }, () => console.log(this.state.windowScrollPosition, 'this.state.windowScrollPosition'))
}