Fix InitialItemIndex is intermittently ignored on fresh page load - #68114
Fix InitialItemIndex is intermittently ignored on fresh page load #68114ilonatommy wants to merge 3 commits into
InitialItemIndex is intermittently ignored on fresh page load #68114Conversation
…ition to interactivity.
There was a problem hiding this comment.
Pull request overview
Fixes a startup race in the Virtualize<TItem> initial-scroll path where the spacers’ IntersectionObserver initial callback can run before the initial programmatic alignment, intermittently resetting the window back to index 0 on fresh loads (especially with prerendering + interop latency).
Changes:
- Adds a new
suppressInitialSpacerCallbacksflag to the JSVirtualize.initinterop call so JS can ignore the observers’ initial callback until the component’s first programmatic-scroll decision is made. - Plumbs the new flag from
Virtualize→VirtualizeJsInterop→Virtualize.ts, enabling suppression whenInitialItemIndex > 0. - Adds coverage: a unit test verifying the init-argument roundtrip, plus an E2E test that validates
InitialItemIndexremains applied after transitioning to interactive server mode.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| src/Components/Web/test/Virtualization/VirtualizeTest.cs | Adds a unit test verifying InitialItemIndex controls initial spacer-callback suppression passed to JS init. |
| src/Components/Web/src/Virtualization/VirtualizeJsInterop.cs | Extends InitializeAsync to pass the suppression flag to Blazor._internal.Virtualize.init. |
| src/Components/Web/src/Virtualization/Virtualize.cs | Enables initial suppression when InitialItemIndex > 0 during first render initialization. |
| src/Components/Web.JS/src/Virtualize.ts | Adds suppressInitialSpacerCallbacks param and initializes suppressSpacerCallbacks from it to ignore the initial IO callback. |
| src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Interactivity/VirtualizationTransitionToInteractivity.razor | Adds a query-switched path to exercise VirtualizationAnchorMode during transition-to-interactivity. |
| src/Components/test/testassets/BasicTestApp/VirtualizationAnchorMode.razor | Makes InitialItemIndex a component parameter and wires it through to the Virtualize component and UI. |
| src/Components/test/E2ETest/ServerRenderingTests/VirtualizationRenderModesTest.cs | Adds an E2E assertion that InitialItemIndex is applied after prerender → interactive server load. |
| @@ -45,6 +45,22 @@ public void Virtualize_Works_WhenMultipleRenderModesAreActive() | |||
| Browser.True(() => GetRenderedItems(Browser.FindElement(By.Id("virtualize-webassembly"))).Contains("Item 50")); | |||
| } | |||
|
|
|||
| [Fact] | |||
| public void InitialItemIndex_IsAppliedOnPrerenderedInteractiveServerLoad() | |||
There was a problem hiding this comment.
Since this only reproduces with network latency, the test could pass even without the fix. For my own education - is it possible to emulate network latency before navigation so the initial observer callback fires first?
There was a problem hiding this comment.
Good catch, I was simplifying it to the point where I lost the fail-on-regression point. We can add an artificial gate in JS where we would detect the initial callback and hold it until the alignment signal comes, then release both and delay suppression of programmatic scroll.
oroztocil
left a comment
There was a problem hiding this comment.
Looks correct but also as a fix for the symptom rather than the cause.
Idea (maybe misguided): add a bool userInitiated argument to OnSpacerBeforeVisible and OnSpacerAfterVisible that passes information if they were initiated do to user scroll or programmatic scroll. The JS side should be able to recognize/track that. ShouldSuppressSpacerCallback can use then this information so it does not have to infer user scroll vs programmatic scroll based on timing (which introduces the race, if I understand things correctly) and the JS suppression added in this PR would not be needed.
Fixes #68099.
It's not visible with fast connection but with throttling the reproduction is stable, videos for reference.
Before the fix:
InitialItemIndex.-.Dan.race.mp4
After the fix:
InitialItemIndex.-.Dan.race.fixed.mp4
Fix
Pass
suppressInitialSpacerCallbacksto JS init method so that JS can skip observer's initial callback that the align-to-item method was racing with.Why we can do that safely:
We don't need the initial spacer callback because the later callbacks are these that really load the data. So we care only about callbacks that are triggered after
ScollToItemAsyncfinishes its job of placing the viewport in the requested position. The placing is done inalignToItems.alignToItemschangesscrollTop, which changes the spacer sizes, which in turn, triggers further spacer callbacks. These callbacks are not supressed anymore, they finish normally. That's how the items are eventually loaded.Why it fixes the issue:
Removing initial callback removes the race of "load items from index 0" (coming from the initial callback) and "shift the viewport to index Nth" (coming from the
IntialItemIndexthat callsScollToItemAsync). Now we have only the latter one.Why the tests are not in the component we normally used for
InitialItemIndextests:The test had to be added to a component that contains "to interactivity" transition so I reused the
VirtualizeAnchorModecomponent inVirtualizationTransitionToInteractivity.razor.