Closed DaniGuardiola closed 2 years ago
Thanks for this! I too would be keen for us to expose a Virtualized
component.
I worked on something like this a few years back and the thinking was that it could be composed with any component that renders lists of things:
const [items, setItems] = React.useState([]);
const itemsPerPage = 50;
<DropdownMenu.Root>
<DropdownMenu.Trigger>Open</DropdownMenu.Trigger>
<DropdownMenu.Content>
<Virtualized.Root
itemsPerPage={itemsPerPage}
itemsCount={totalItemsLength}
onPageLoad={(page, updateItems) => {
const from = itemsPerPage * page;
// your API call to get more items (or just `slice` a static dataset).
getItems({ from, itemsPerPage }).then(items => setItems(updateItems(items)));
}}
>
{items.map((item) => (
<Virtualized.Item asChild key={item.id}>
<DropdownMenu.Item>{item.label}</DropdownMenu.Item>
</Virtualized.Item>
))}
</Virtualized.Root>
</DropdownMenu.Content>
</DropdownMenu.Root>
Nice! Yeah that'd be an awesome API!
I'm however confused about the pagination. I think it makes sense to have "infinite-scrolling"-like behavior for stuff that comes from a server, however that won't always be the case. For example, we have a timezone selector with a lot of entries, and they're all already loaded locally in our app.
As I said, it makes sense to me that you could be able to pull entries asynchronously with this pagination-like API, but what I had in mind was more related to just large lists where Radix would calculate how many need to be rendered in real-time and by being aware of the current scroll position and the height of the container. I'm guessing you can always mimic that yourself by plugging those lists to this pagination API and using a big enough itemsPerPage
limit, but it'd be awesome to be able to just pass the list and let the virtualization handle it. Just my two cents!
For example, we have a timezone selector with a lot of entries, and they're all already loaded locally in our app.
My comment in the code probably wasn't clear but for this you would do something like:
<DropdownMenu.Root>
<DropdownMenu.Trigger>Open</DropdownMenu.Trigger>
<DropdownMenu.Content>
<Virtualized.Root
itemsPerPage={itemsPerPage}
itemsCount={timezones.length}
onPageLoad={(page, updateItems) => {
const from = itemsPerPage * page;
const items = timezones.slice(from, from + itemsPerPage);
setItems(updateItems(items));
}}
>
{items.map((item) => (
<Virtualized.Item asChild key={item.id}>
<DropdownMenu.Item>{item.label}</DropdownMenu.Item>
</Virtualized.Item>
))}
</Virtualized.Root>
</DropdownMenu.Content>
</DropdownMenu.Root>
The itemsPerPage
isn't intended to mean "number of items that fit in the viewport" but rather, "the number of items you're happy for us to render before we attempt to render more". This can be much larger than the viewport but it's up to you based on how your particular dataset needs to be split for optimum performance.
Unfortunately, without that information it seems we would need to render all items first to determine how many "fit" in the viewport and then re-render to unmount those that don't which wouldn't be great for perf.
Having said all this though, this is pseudo-code so would likely be very different if we were to pick this up 😅
I see. I guess what I am expecting is something more versatile like react-window
. If you look at the examples here, you can see that there are many ways to approach this problem. One would be fixed size, but dynamic size is also possible by passing a setter.
Honestly I think that it could be ideal if Radix just made sure that list-like components are compatible with these libraries, rather than having it's own implementation -and some guides on how to compose them. Or at least have feature parity with the most common features, like being able to specify a size. Wrapping something like react-window
in a Radix-style API is also a good option IMHO.
I think (correct me if I'm wrong) that the approach you mention is not really virtualization, but rather "infinite scrolling" (be it with a remote or local data set). Virtualization, AFAIK, is often achieved using techniques like absolute positioning so that you can render some items in the middle, and not render anything before/after that for performance. If you're not aware of the size of the items, there's no way you can reliably keep track of where the items should be and you can't unmount previous elements without messing up the visible elements' position (or doing a double-render which, as you mention, is not ideal). That means that, while at first it does help, performance would degrade the more you scroll.
Twitter is a good example of what I mean. The web app has infinite scrolling, but also virtualization. Otherwise it would be impossible to use once you have scrolled down for a while.
Ideally Radix would support both, but virtualization is more pressing for us.
Also relevant what you can achieve with these additional libs: https://github.com/bvaughn/react-window#related-libraries
Our use case could take advantage of some of these as well. I wonder if Radix could just make sure that these can be composed with all list-like components?
I think (correct me if I'm wrong) that the approach you mention is not really virtualization, but rather "infinite scrolling"
I am talking about virtualization, which is why I mentioned needing to know how many items "fit" in the viewport. Ones that don't fit should be unmounted like you say but when I tried to build this previously, the itemsPerPage
helped me to create relatively positioned items while still unmounting things IIRC.
We're getting far too much into the implementation detail here for a hypothetical component though and it is likely that everything I have said wouldn't be the resulting API 😅 I was mostly raising that I think this could be a separate component to virtualize "all the things" and not something built in to each component.
Honestly I think that it could be ideal if Radix just made sure that list-like components are compatible with these libraries
I'm not sure why we're incompatible? Here is a DropdownMenu
with react-window
:
https://codesandbox.io/s/billowing-sound-pmmpp?file=/index.js
You cannot use the asChild
API here for the DropdownMenu.Content
though. ~I'm guessing List
isn't a React.forwardRef
component which would be something to raise with them~. It looks like they're overwriting the ref
with their own useImperativeHandle
API, which means consumers cannot grab the DOM node. You should be able to achieve what you need without asChild
prop there though.
Very good and simple API https://github.com/petyosi/react-virtuoso
Duplicate of #750
Feature request
Overview
Virtualization of long lists for components like
DropdownMenu
.Examples in other libraries
MUI,
react-aria
, etc support virtualization in their components.Who does this impact? Who is this for?
Once the lists start getting bigger, performance can degrade pretty fast, specially on low-end devices. I think it'll be specially critical for the combobox component, which is more likely to have a large amount of items.
Additional context
We're having performance issues in our product due to this.