nowaalex/af-utils

Problems scrolling to bottom

syllith opened this issue · 33 comments

First of all, I wanted to thank you for solving a month long problem I've had rendering large amounts of data. This package is by FAR the easiest, most understandable, and performant virtual list out there. I am however having some trouble scrolling the list to the bottom once I load information from my API. I use a state array of JSX elements as my data for the list, however, after I set the state, I need to immediately scroll to the bottom. After I set the state, I run model.scrollTo(myState.length).

However, it is not scrolling to the bottom I set the state, so I assume the list hasn't actually rendered in yet by the time I run the scrollTo function. I get no errors, just no scrolling. However, if I put it in a setTimeout, it only scrolls me most of the way to the bottom, so this can't be the proper way. Can you please tell me the right way I'm supposed to do this in very basic terms. I've only been using React for a few months now, so I'd appreciate it if you could explain it clearly. Thank you so much. Please let me know if you need additional information, and again thank you for this wonderful tool. You are a life saver

Thank you very much! So pleased to read such a review) I am updating scrollTo functionality now, should be ready in 1-2 days. Problem is, that after element is added, it gets remeasured asynchronously and scrollHeight is changed only after that.

Sounds great, it means a lot you're taking such swift action. If you don't mind, please let me know once you have this implemented and I'll give it a shot.

@syllith I've upgraded packages to 0.0.4
check https://af-virtual-scroll.vercel.app/examples/list/ScrollToItem
Does that perform like you wanted?

This is what my code looks like when performing the scroll to bottom, although I don't think that's what is causing the glitch. Just making sure I did this right.
image

@syllith could you please provide full code?
I want to see complete component with hooks + I want to see how you process data

And one more thing: frequent itemCount change may be optimized further, so I'll also do it...for now I did everything for performant scroll, but frequent additions may be little slow, but it is optimizable, so idea is good, thx.

@nowaalex Absolutely, I can provide the entire project, however, I cannot post it here, as it contains confidential information my employer would not want me to share publicly. Would you consider allowing me to contact you via Discord? If not, I can try to post only the relevant parts, but I'd imagine you'd want to see everything working as a whole. Please let me know what you'd like to do, thanks.

Could you write email nowaalex@gmail.com?

Yes, I'll send you the project shortly. I'll send some additional information in the email as well. I'm currently reworking the code so it's easier to read and has comments. It's messed up quite a bit right now. Maybe an hour or so?

@nowaalex I have just sent the email including some basic setup instructions. Thank you!

Hey there, I noticed something before I got started implementing those changes we talked about. It looks like the scroll position doesn't stay persistent when appending new items. For example, in this GIF, I have it set to only scrolls to the bottom on the initial load. When I load in the rest of the data, the position isn't maintained. Even after implementing the fixes we talked about, I feel this will still be a problem. Is there anything you can do to keep the scroll position where it was when new data is added?
screen-capture

You must put a condition like if(model.to !== model.itemCount) into useEffect to scroll to bottom only when needed. Or you should scroll down only once. This can be achieved by [list.length === 0] dep passed to useEffect

In the GIF I posted above, I am only scrolling down once when the initial data is loaded. When the second half loads in, I am not scrolling at all, but the scroll bar position still changes.

On the first initial load, lets say the scroll height of the entire list is 5,000 and the user is scrolled to the bottom, making their scroll top position 5,000 as well.

When the rest of the data loads in, and the scroll height of the list now changes to 15,000, the users scroll top position remains at 5,000, making it look like they just got moved to a random place in the list when the second set of data is loaded. This is what I'm trying to prevent. Does that make sense? It's a little difficult to explain.

@syllith it is a little difficult to discuss this stuff without minimal code templates...maybe it is possible to make a separate SMALL component, that uses virtual scrolling in the way you want, and discuss it with code examples here?

@nowaalex Agreed, I've put together a basic sample component that should hopefully demonstrate the issue.

import { useState, useEffect, memo } from "react";
import { useVirtual, areItemPropsEqual, List } from "@af-utils/react-virtual-list";
import './css/SampleComp.css';

var scrolledToBottom = false;
var countryList = ["Afghanistan", "Albania", "Algeria", "American Samoa", "Andorra", "Angola", "Anguilla", "Antarctica", "Antigua and Barbuda", "Argentina", "Armenia", "Aruba", "Australia", "Austria", "Azerbaijan", "Bahamas (the)", "Bahrain", "Bangladesh", "Barbados", "Belarus", "Belgium", "Belize", "Benin", "Bermuda", "Bhutan", "Bolivia (Plurinational State of)", "Bonaire, Sint Eustatius and Saba", "Bosnia and Herzegovina", "Botswana", "Bouvet Island", "Brazil", "British Indian Ocean Territory (the)", "Brunei Darussalam", "Bulgaria", "Burkina Faso", "Burundi", "Cabo Verde", "Cambodia", "Cameroon", "Canada", "Cayman Islands (the)", "Central African Republic (the)", "Chad", "Chile", "China", "Christmas Island", "Cocos (Keeling) Islands (the)", "Colombia", "Comoros (the)", "Congo (the Democratic Republic of the)", "Congo (the)", "Cook Islands (the)", "Costa Rica", "Croatia", "Cuba", "Curaçao", "Cyprus", "Czechia", "Côte d'Ivoire", "Denmark", "Djibouti", "Dominica", "Dominican Republic (the)", "Ecuador", "Egypt", "El Salvador", "Equatorial Guinea", "Eritrea", "Estonia", "Eswatini", "Ethiopia", "Falkland Islands (the) [Malvinas]", "Faroe Islands (the)", "Fiji", "Finland", "France", "French Guiana", "French Polynesia", "French Southern Territories (the)", "Gabon", "Gambia (the)", "Georgia", "Germany", "Ghana", "Gibraltar", "Greece", "Greenland", "Grenada", "Guadeloupe", "Guam", "Guatemala", "Guernsey", "Guinea", "Guinea-Bissau", "Guyana", "Haiti", "Heard Island and McDonald Islands", "Holy See (the)", "Honduras", "Hong Kong", "Hungary", "Iceland", "India", "Indonesia", "Iran (Islamic Republic of)", "Iraq", "Ireland", "Isle of Man", "Israel", "Italy", "Jamaica", "Japan", "Jersey", "Jordan", "Kazakhstan", "Kenya", "Kiribati", "Korea (the Democratic People's Republic of)", "Korea (the Republic of)", "Kuwait", "Kyrgyzstan", "Lao People's Democratic Republic (the)", "Latvia", "Lebanon", "Lesotho", "Liberia", "Libya", "Liechtenstein", "Lithuania", "Luxembourg", "Macao", "Madagascar", "Malawi", "Malaysia", "Maldives", "Mali", "Malta", "Marshall Islands (the)", "Martinique", "Mauritania", "Mauritius", "Mayotte", "Mexico", "Micronesia (Federated States of)", "Moldova (the Republic of)", "Monaco", "Mongolia", "Montenegro", "Montserrat", "Morocco", "Mozambique", "Myanmar", "Namibia", "Nauru", "Nepal", "Netherlands (the)", "New Caledonia", "New Zealand", "Nicaragua", "Niger (the)", "Nigeria", "Niue", "Norfolk Island", "Northern Mariana Islands (the)", "Norway", "Oman", "Pakistan", "Palau", "Palestine, State of", "Panama", "Papua New Guinea", "Paraguay", "Peru", "Philippines (the)", "Pitcairn", "Poland", "Portugal", "Puerto Rico", "Qatar", "Republic of North Macedonia", "Romania", "Russian Federation (the)", "Rwanda", "Réunion", "Saint Barthélemy", "Saint Helena, Ascension and Tristan da Cunha", "Saint Kitts and Nevis", "Saint Lucia", "Saint Martin (French part)", "Saint Pierre and Miquelon", "Saint Vincent and the Grenadines", "Samoa", "San Marino", "Sao Tome and Principe", "Saudi Arabia", "Senegal", "Serbia", "Seychelles", "Sierra Leone", "Singapore", "Sint Maarten (Dutch part)", "Slovakia", "Slovenia", "Solomon Islands", "Somalia", "South Africa", "South Georgia and the South Sandwich Islands", "South Sudan", "Spain", "Sri Lanka", "Sudan (the)", "Suriname", "Svalbard and Jan Mayen", "Sweden", "Switzerland", "Syrian Arab Republic", "Taiwan", "Tajikistan", "Tanzania, United Republic of", "Thailand", "Timor-Leste", "Togo", "Tokelau", "Tonga", "Trinidad and Tobago", "Tunisia", "Turkey", "Turkmenistan", "Turks and Caicos Islands (the)", "Tuvalu", "Uganda", "Ukraine", "United Arab Emirates (the)", "United Kingdom of Great Britain and Northern Ireland (the)", "United States Minor Outlying Islands (the)", "United States of America (the)", "Uruguay", "Uzbekistan", "Vanuatu", "Venezuela (Bolivarian Republic of)", "Viet Nam", "Virgin Islands (British)", "Virgin Islands (U.S.)", "Wallis and Futuna", "Western Sahara", "Yemen", "Zambia", "Zimbabwe", "Åland Islands"];
var numbers = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23", "24", "25"];

const SampleComp = () => {
    const [entries, setEntries] = useState({ entryList: [] });

    const Item = memo(
        ({ i }) => (
            <div>
                {entries.entryList[i]}
            </div>
        ),
        areItemPropsEqual
    );

    const model = useVirtual({
        itemCount: entries.entryList.length,
        overscanCount: 10,
        estimatedWidgetSize: 0,
        estimatedItemSize: 18
    });

    useEffect(() => {
        start();
    }, []);

    function start() {
        setEntries(oldState => ({ entryList: [...countryList, ...oldState.entryList] }));

        setTimeout(() => {
            // This is where the problem occurs. After 3 seconds, when this next line is executed, you can see that the position of the scroll bar changes, and does not remain persistent with the users current scroll position before the update happened
            console.log("This is where the problem occurs!")
            setEntries(oldState => ({ entryList: [...numbers, ...oldState.entryList] }));
        }, 3000);
    }

    useEffect(() => {
        if (scrolledToBottom === false && entries.entryList.length > 0) {
            scrolledToBottom = true;
            setTimeout(() => {
                model.scrollTo(entries.entryList.length - 1);
            }, 0);
        }
    }, [entries.entryList]);

    return (
        <List model={model} itemData={entries.entryList}>
            {Item}
        </List>
    );
};

export default SampleComp;

This would not be an issue if the data was being appended to the END of the array, but it's a requirement for my project to have the scrolling go from the bottom up, so the data must be appended to the beginning of the array. Here's a GIF better demonstrating the issue. I need the users scroll position, wherever that may be, to stay the same, no matter where they are scrolled to when the new data gets added.
screen-capture

@syllith , scroll position is not maintained automatically when items are prepended. And it should not be. Analogy: imagine you have an array [ "Peter", "Josh", "Kate"]. Peter has 0 index, Josh - 1, Kate - 2. When smth is appended to this array, current indexes for Peter, Josh and Kate are saved. But if you prepend items - indexes would be lost.

You must sync scroll position manually when smth is prepended. Smth like

const offsetRef = useRef();

useEffect(() => {
    offsetRef.current = model.getOffset( model.from );
    const items = await query();
    setItems( cur => [ ...items, ...cur ]);
}, [ dependenciesForQuery ]);

useEffect(() => {
    model.scrollTo( model.getIndex( offsetRef.current ) );
}, [ items.length ]);

Also

const Item = memo(
        ({ i }) => (
            <div>
                {entries.entryList[i]}
            </div>
        ),
        areItemPropsEqual
    );

this should be put outside of SampleComp component. data prop is provided to Item, not only i. data prop in Item is itemData. Smth like

const Item = memo(
        ({ i, data }) => (
            <div>
                {data[i]}
            </div>
        ),
        areItemPropsEqual
    );

Would it be possible to get an example of this working with the sample component I sent? I'm having trouble implementing this on my side.

Ok, I ll do it nearest time

Check this example
In order to use it you must upgrade @af-utils/* packages.

I have it working! Thank you so much, I'm sorry if I was a pain to work with. I'm getting a bit of delay when scrolling after loading data, but that's more than likely because I haven't reworked my code to be more efficient yet, so it's still quite slow. Not too worried about that at the moment. Thank you again so much

Everything is great, thx) Now I have new example)

Sorry to bother you again, but for some reason I'm getting this issue:
image

Any reason you can think of as to why this might be happening?
image

Everything is ok, it seems to be another issue. Could you provide full code? Problem is not in these 3 lines you have provided. I assume you are using List component. Internally List renders a div with overflow: auto, which is given ref={model.setOuterNode}. setOuterNode callback gets fired in 2 cases with one argument:

  • div gets mounted, with mounted element argument,
  • div gets unmounted. with null argument.

This error is happening, when you try to scroll with outerNode not being mounted.

I tried a test regarding the delay when prepending data. Instead of creating genuine message, I simply returned a string, which should have been much faster (13 ms in this case), however, it didn't seem to have changed anything, so I think something is still wrong here.
screen-capture

After further investigation, it appears as if the example on your website is behaving the same way as mine. It's most noticeable when scrolled to the very bottom. Not only is there a delay when correcting the scroll position, but it also doesn't scroll all the way to the bottom again when data is prepended. Any ideas as to why this might be happening?
screen-capture-1

Hmm, I see, I will check this, thx for noticing

Did you have a chance to look into this yet? No rush, just curious if you found anything out.

Will look in 1-2 days, little busy

Any luck with this? If need be, I can probably get away with appending my data, and scrolling from the top down, rather than prepending and scrolling from the bottom up. But since you've already started investigating this, I will definitely use it if you can figure it out. I'm not sure how complicated of a problem this actually is. I don't want to overwork you for something I don't absolutely have to have.

@syllith thx for waiting, check new version
Example

Closing this issue, fixed in version 0.0.7