Feed column keeps updating the visible range - but I'm not scrolling

I have an app using pn.layout.Feed to lazy load a long list of “things”
My test case has 20 items but there will be hundreds in my real app. About 5 fit on my window.

I am creating the feed as:

pn.Feed.objects = [list of stub items]

The stub items are empty FlexBox with a minimum height (so they take up space)

And when the “visible_range” changes, the callback fills in the stub item and updates the objects in the Flex box.

What I’m seeing is:

  1. The app loads and the first callback pulls down the first 3 items (load_buffer)
  2. Then it keeps calling the callback, with the “end” of the visible range incrementing by 2 every time - even though I’m not scrolling at all. This continues until all 20 items in my test case have loaded
  3. When I actually start scrolling, the visible range callback is not called at all

Are there gotchas to using this widget?

Also, not sure if this is related, but occasionally the whole app crashes with:

double free or corruption (fasttop)
Aborted (core dumped)

Can you share a full example that reproduces this issue? Also have you tried the panel_material_Ui version?

This is the structure of my app.

from __future__ import annotations

import asyncio
import sys
from typing import Any

import param
import panel as pn
import panel_material_ui as pmui


pn.extension(
    throttled=True,
    notifications=True,
    disconnect_notification="ERROR: Connection lost, try reloading the page...",
)


class FeedItem(pn.viewable.Viewer):
    """Panel view for one selectable group."""

    visible = param.Boolean(default=False, doc="Whether this event has been loaded.")

    def __init__(self, item_id: int, **params: Any) -> None:
        """Create a test event list item."""

        self._item_id = item_id

        with pn.config.set(sizing_mode="stretch_width"):
            self._button = pmui.MenuButton(
                label=f"Open item {self._item_id}",
                items=["thing1", "thing2", "thing3"],
                sizing_mode=None,
                align="center",
                disable_elevation=True,
            )
            self._button_row = pn.Row(
                self._button,
                sizing_mode="stretch_width",
            )
            self._details = pmui.FlexBox(
                align_items="flex-start",
                flex_wrap="wrap",
                gap="24px",
                min_height=160,
                sizing_mode="stretch_width",
                styles={
                    "border-top": "1px solid #d9d9d9",
                    "border-bottom": "1px solid #d9d9d9",
                    "padding": "8px 0",
                },
            )
            self._layout = pn.Column(
                self._button_row,
                self._details,
                sizing_mode="stretch_width",
                margin=(0, 0, 8, 0),
            )
        super().__init__(**params)

    @param.depends("visible", watch=True)
    async def on_visible(self) -> None:
        """Hook for loading additional data when the item becomes visible."""

        self._details.loading = True
        # Doing "some slow requests here"
        await asyncio.sleep(2)
        self._details.loading = False
        # Formatting the results of my requests here
        self._details.objects = [
            pn.pane.Markdown("# Detail 1\n\nSome stuff"),
            pn.pane.Markdown("# Detail 2\n\nSome more stuff"),
        ]

    def __panel__(self) -> pn.Column:
        """Return the Panel representation of this test event."""
        return self._layout


class FeedList(pn.viewable.Viewer):
    """Feed layout containing selectable test events."""

    def __init__(self, items: list[TestEventItem], **params: Any) -> None:
        """Create the test event feed."""

        self._items = items
        self._layout = pn.Column(scroll=False)
        self._feed = pmui.layout.Feed(
            *self._items,
            load_buffer=3,
            scroll=False,
            min_height=500,
            sizing_mode="stretch_width",
            scroll_position=0,
            view_latest=False,
        )

        super().__init__(**params)
        self._layout.objects = [self._feed]
        self._feed.objects = self._items

    @param.depends("_feed.visible_range", watch=True)
    def _visible_range_changed(self) -> None:
        """Run item hooks for visible feed objects."""

        start, end = self._feed.visible_range
        # visible range is inclusive so need to be careful to add one to end
        visible_items = self._items[start : end + 1]
        print(
            "len(items)=",
            len(self._items),
            "visible",
            len(visible_items),
            f"{start=} {end=}",
        )
        count = 0
        for item in visible_items:
            if not item.visible:
                item.visible = True
                count += 1
        print(f"toggled {count=}")

    def __panel__(self):
        """Return the Panel representation of the test event list."""

        return self._layout


class TestApp(pn.viewable.Viewer):
    def __init__(
        self,
        **params: Any,
    ) -> None:
        self._button = pn.widgets.Button(label="Open Feed")
        self._main_content = pn.Column(
            self._button, sizing_mode="stretch_width", scroll=False
        )
        self._template = pn.template.MaterialTemplate(
            title="TestApp",
            main=[self._main_content],
        )
        super().__init__(**params)
        self._button.on_click(self.load_feed)

    def load_feed(self, *args) -> None:
        if self._button in self._main_content.objects:
            items = [FeedItem(i) for i in range(50)]
            self._main_content.objects = [FeedList(items)]

    def __panel__(self) -> pn.template.MaterialTemplate:
        return self._template

    @classmethod
    def create_app(cls) -> Buoy:
        return cls()


if pn.state.served:
    TestApp.create_app().servable()

Can you share the code as a code snippet, not a file.

edited

What version and browser are you using?


len(items)= 50 visible 4 start=0 end=3
toggled count=4
len(items)= 50 visible 7 start=0 end=6
toggled count=3
len(items)= 50 visible 10 start=0 end=9
toggled count=3
len(items)= 50 visible 13 start=0 end=12
toggled count=3
len(items)= 50 visible 16 start=0 end=15
toggled count=3
len(items)= 50 visible 19 start=0 end=18
toggled count=3
len(items)= 50 visible 22 start=0 end=21
toggled count=3
len(items)= 50 visible 25 start=0 end=24
toggled count=3
len(items)= 50 visible 28 start=0 end=27
toggled count=3
len(items)= 50 visible 31 start=0 end=30
toggled count=3
len(items)= 50 visible 34 start=0 end=33
toggled count=3
len(items)= 50 visible 37 start=0 end=36
toggled count=3
len(items)= 50 visible 40 start=0 end=39
toggled count=3
len(items)= 50 visible 43 start=0 end=42
toggled count=3
len(items)= 50 visible 46 start=0 end=45
toggled count=3
len(items)= 50 visible 49 start=0 end=48
toggled count=3
len(items)= 50 visible 50 start=0 end=50
toggled count=1

Seems to work for me.

It works in the sense that the feed loads and displays - but it doesn’t work in the sense that if I’m not moving the scrollbar, new items are not becoming visible in the browser window and the visible_range callback should not be triggered.

Did your code print statements still happen regardless of whether you were interacting with the app? If so, that is not what I thought was supposed to happen.

Without interacting with it:


len(items)= 50 visible 4 start=0 end=3
toggled count=4
len(items)= 50 visible 7 start=0 end=6
toggled count=3
len(items)= 50 visible 10 start=0 end=9
toggled count=3
len(items)= 50 visible 13 start=0 end=12
toggled count=3
len(items)= 50 visible 16 start=0 end=15
toggled count=3
len(items)= 50 visible 19 start=0 end=18
toggled count=3
len(items)= 50 visible 22 start=0 end=21
toggled count=3
len(items)= 50 visible 25 start=0 end=24
toggled count=3
len(items)= 50 visible 28 start=0 end=27
toggled count=3
len(items)= 50 visible 31 start=0 end=30
toggled count=3
len(items)= 50 visible 34 start=0 end=33
toggled count=3
len(items)= 50 visible 37 start=0 end=36
toggled count=3
len(items)= 50 visible 40 start=0 end=39
toggled count=3
len(items)= 50 visible 43 start=0 end=42
toggled count=3
len(items)= 50 visible 46 start=0 end=45
toggled count=3
len(items)= 50 visible 49 start=0 end=48
toggled count=3
len(items)= 50 visible 50 start=0 end=50
toggled count=1

And were all 50 items visible on your screen? Or, would the correct visible_range value actually be a range shorter than from 0 to 50? Do you see what I’m saying?

Yes, you’re saying that it shouldn’t pre-load the rest of the visible, and it should only do visible_range ± load_buffer.

Can you submit a bug report on GitHub?

Logged as The Feed object is not reporting the correct visible_range · Issue #8661 · holoviz/panel · GitHub

1 Like

See Fix feed by ahuang11 · Pull Request #8667 · holoviz/panel · GitHub