[BUG] Intermittent Page State Retention on Interactive WebAssembly
tvatavuk opened this issue ยท 11 comments
Oqtane Info
Version - 5.2.1
Render Mode - Interactive
Interactivity - Client
Database - SQL Server
Describe the bug
When navigating between pages in Oqtane, there are instances where the module content from the previous page persists and is displayed on the current page. This behavior suggests a potential state management or timing issue where the page state does not update correctly, leading to the display of stale content.
Expected Behavior
When navigating to a new page, the content should fully refresh and display the correct content for the current page, without retaining any data from the previous page.
Steps To Reproduce
- Create 5 pages with core one HTML or BLOG module per page and add module content that reflects page, or module title:
- h1 - html1,
- h2 - html2,
- b1 - blog1,
- b2 - blog2,
- b3 - blog3,
- h4 - html4,
- h5 - html5
- Navigate to page h1.
- Click on a link h2 to navigate to a different page.
- Observe if the module content from the previous page is displayed on the new page.
- Repeat the navigation multiple times to observe intermittent occurrences. Eg: h2, b1, b2, b3, h4, b3, b2, b1, etc.
- Here is video to demonstrate the issue: Oqtane 5.2.1 - [BUG] Intermittent Page State Retention - Previous Page Content Persisting
Anything else?
- Could be related to #4537
not sure whether this is related to #4232, need further investigation, just want to provide some insight.
@tvatavuk a few observations....
the Blog Module uses this pattern in its BlogService class:
public class BlogService : ServiceBase, IBlogService, IService
{
private readonly SiteState _siteState;
public BlogService(HttpClient http, SiteState siteState) : base(http)
{
_siteState = siteState;
}
private string Apiurl=> CreateApiUrl("Blog", _siteState.Alias);
BlogService and SiteState are registered as Scoped in the Client project (which essentially makes them acts like a Singleton when running on WebAssembly).
Most of the core framework uses this pattern:
public class HtmlTextService : ServiceBase, IHtmlTextService, IClientService
{
public HtmlTextService(HttpClient http, SiteState siteState) : base(http, siteState) {}
private string ApiUrl => CreateApiUrl("HtmlText");
Notice that there is no private SiteState variable above - it is using the siteState value in the base class.
The new default module template uses this pattern:
public class Test521Service : ServiceBase, ITest521Service
{
public Test521Service(IHttpClientFactory http, SiteState siteState) : base(http, siteState) { }
private string Apiurl => CreateApiUrl("Test521");
It uses IHttpClientFactory rather than IHttpClient - which is the recommended approach from Microsoft for .NET Core.
I am wondering if the scoping of these services - mostly likely HttpClient - which is causing the behavior you recorded when running on Interactive WebAssembly
@tvatavuk can you also please clarify if the site in the video was a standard site or a sub-site... it is difficult to see in the video but it appears to be a sub-site:
@tvatavuk I believe I found the solution... and it is something which has been logged as an issue previously. Basically when a site is configured to run fully interactive, it is running as a SPA. In a SPA, component instances are not disposed immediately when you navigate from page to page (there are not really "pages" - just one big canvas which reacts to changes in the Url). So upon navigation there may be components which were rendered on the previous page which are not part of the new page - however they are still active and their life cycle events will still be fired. And in the scenario where the same type of module is on 2 different pages, it means there are multiple instances of the same component which are active, and they will each react to changes in the Url upon navigation. Usually these types of modules implement OnParametersSetAsync() so that they are re-rendered when parameters change. However there is no need for a component instance which is unrelated to the current page to do any processing such as retrieving data, etc... So Oqtane maintains RenderId properties as part of PageState and ModuleState objects for this purpose so that it can determine if a component needs to re-render or not based on the parameters. ModuleBase contains a ShouldRender() method which internally utilizes these properties. The HtmlText module is an example of a module which uses ShouldRender() to ensure that components only load content when appropriate. The Blog module is similar to the HtmlText module in the sense that there can be multiple instances in a site. So it should use the ShouldRender() method:
protected override async Task OnParametersSetAsync()
{
if (ShouldRender())
{
// load blogs
}
}
Once I modified the Blog module to use this approach the issues with content not changing on navigation was resolved.
@sbwalker Awesome! ๐ I really appreciate your help, detailed explanations, all your work, and the support you provided. It is making a big difference. ๐
@tvatavuk One thing I would like to document related to this issue...
Prior to making the change in the Blog to include ShouldRender() the module was making a lot more HttpClient service calls. For a site with 2 pages which each had a Blog module instance, when I navigated from one of the pages to the other there were 6 calls to GetBlogs - 2 of the calls had the ModuleId for the page I was navigating from and 4 of the calls had the ModuleId for the page I was navigating to. In theory these calls were originating from 2 distinct Index component instances. All of the calls were successful with a 200 status. But this does not explain how the Blog component on the page I was navigating to received the wrong content. So there may still be an issue with the scoping of the services ie. the components are receiving content for an HttpClient call which originated from another component. I am going to experiment with using IHttpClientFactory instead of IHttpClient to determine if it is more reliable in this scenario.
By adding the ShouldRender() logic, the number of HttpClient calls was reduced to 2 rather than 6 - and both of the calls contained the ModuleId for the page I was navigating to. So it is possible that this "fix" simply masked the real problem.
Ok, so it turns out there are not 2 distinct Index component instances - Blazor re-uses components - so there is only 1 component instance which is responding to the changes in parameters. And that component instance is making 6 HttpClient calls (with different parameters). And since HttpClient calls are asynchronous there is no predictable order of when they will return results (this is true regardless of whether we use IHttpClient or IHttpClientFactory). So in some cases the HttpClient call for the wrong ModuleId is returned last - which means the wrong content is rendering in the UI. So it appears that using ShouldRender() is the right solution as it ensures that HttpClient calls are only made using the ModuleId on the page being navigated to (not from).
@tvatavuk the fix that I made was only to the Blog module. However after reading your original post again, it sounds like you were also experiencing the problem with the HtmlText module. If so, the HtmlText module is already using ShouldRender() so my solution above would not be 100% accurate. I am not able to reproduce this locally with the HtmlText module - only with the Blog module (prior to the fix I merged). Are you seeing this problem with the HtmlText module? Specifically I am referring to a scenario of a site with no Blog modules - only pages with HtmlText modules and navigating between pages.
@sbwalker No, I do not see any issues with the HtmlText module. The HtmlText module is functioning correctly in my tests across versions 5.1.2, 5.2.0, and 5.2.1, so everything is working as expected.
In a mixed scenario with other modules, there was only one strange case when navigating away from a page containing an HtmlText module to a page with another module that lacks proper ShouldRender support. In this case, the HtmlText module's endpoint was triggered with the ModuleId of module from the new page (which wasn't the HtmlText module). However, this behavior aligns with your findings and explanations and should be resolved once proper ShouldRender() support is added to the other modules, as you already did with Blog.
Btw, thanks @sbwalker for the explanation of the inner workings of the SPA implementation in Oqtane Interactive. Module developers should understand and expect this behavior, which is common when developing SPA apps with JavaScript on the client side, but not as intuitive when working on the server side.