Do we need to consider ShouldRender() for custom modules? #4128
-
We are tracking down some bugs with module not rendering and found a new pattern in the HtmlText sample module.
We are in the process of updating our custom modules, which have been heavily reliant on OnInitializedAsync(), (following the examples used in the admin modules). We’ve observed that pre-rendering triggers OnInitializedAsync() to execute twice, which is intentional. Our current understanding suggests that we should transition away from using OnInitializedAsync() as the primary driver for the module. Instead, we should shift our focus to OnParametersSetAsync() and ensure that we’re evaluating the ShouldRender() function as see in the HTMLText sample module. I this indeed the best pattern to follow? |
Beta Was this translation helpful? Give feedback.
Replies: 9 comments 10 replies
-
Long answer.... Blazor is a SPA framework where each component is a stateless chunk of UI on the page. State is communicated to components in various ways - each with their own specific behavior. Oqtane leverages a cascading parameter called PageState to pass state from the router to all nested components in the UI hierarchy. Whenever a cascading parameter is changed, all components which reference it are immediately notified (note this is a Blazor behavior - not an Oqtane behavior). This results in components being re-rendered to reflect the latest state. This all makes sense in theory... however in practice it behaves a bit different than expected. In a default Oqtane installation it will display the Home "page" as part of the initial request (a "page" in Oqtane is simply a configuration which defines the modules which should be rendered dynamically). The site router constructs a PageState object which becomes a cascading parameter. The Home page contains 3 Html/Text modules and the Oqtane UI page rendering process will render 3 instances of the Html/Text Index.razor component. Each instance is related to a unique ModuleId so it is able to load the specific content for each module instance. If you then click on the Private link in the nav menu, the site router will recognize that you are navigating to a new "page". It will construct a new PageState object... and as soon as the cascading parameter is modified it triggers a notification to all existing components that the state has changed. The 3 Html/Text module instances on the Home page are active in the DOM at this point so they all receive the notification (despite the fact that they are not part of the Private page and will ultimately be disposed at the conclusion of the rendering process). Each component instance will take action on the notification and re-render itself. At this point the Oqtane UI page rendering process will kick in and will determine that it needs to create 1 new Html/Text module instance specific to the Private page. You will assume that it renders this new component instance and at the conclusion of the rendering process, Blazor will recognize that the 3 module instances from the Home page are no longer in the DOM and will dispose those components. In reality, what actually happens from a Blazor perspective is a bit different... Blazor does not actually create 1 new component and dispose 3 old components. It reuses components. So it actually reuses one of the initial 3 Html/Text components and disposes 2 components which are no longer needed. So based on what I described above, components which were rendered on the previous "page" will be re-rendered due to the cascading parameter behavior. This means that the rendering process is sometimes doing more work than necessary ie. it is re-rendering components prior to disposing them. You generally do not notice that this is happening because Blazor has a very efficient shadow DOM algorithm which only modifies the UI when there are changes. And since components are regenerating the exact same UI output you only see the final result. However, if you look more closely at the actual component workload you may see that a module is doing more work than necessary. For example, the Html/Text Index.razor component is making API service calls to retrieve content for module instances that will be disposed. This can lead to performance issues. #3611 optimized the page rendering process. Specifically it uses conditional logic to determine if a component which receives a cascading parameter notification based on PageState changes should actually re-render. This eliminates a lot of redundant processing as you navigate around a site. And even more impactful for performance, the Html/Text Index.razor component uses this technique to determine if it needs to call the API to get content. This is encapsulated within the ShouldRender method - which is a standard overridable Blazor life cycle method. So that is the background to the ShouldRender() logic in the Html/Text module. However, to answer your other question... there is no "default" way to implement a module - modules should be implemented based on their run-time requirements. Some modules need to respond to changes in their parameters - others do not. Some modules need to execute OnInitializedAsync twice, others do not. It really depends on what you are trying to accomplish. The Html/Text module is a common module used on many pages in a site so it needs to respond to changes in its parameters - which is why it implements OnParametersSetAsync rather than OnInitializedAsync. And it does not need to re-render instances which are going to be disposed, so it uses ShouldRender. |
Beta Was this translation helpful? Give feedback.
-
@sbwalker thanks again for this full explanation. We are moving some of the work we are doing from inti() to parmset() methods and are running into another problem of onparmset() getting called many times. If the component is potentially getting re-used, how can we know that it's a new module instance on the page and we should get new data? I feel like the best option is to keep a copy of the pageid and moduleid as instance vars in the module components and then only do the work when the page or the moduleid changes. Would you say this is a good strategy? Id this what ModuleState is all about? |
Beta Was this translation helpful? Give feedback.
-
We ended up creating an other base class to manage the instance of a module on a page and are testing it now. We are using ShouldRender() in a way that feels OK as in "This should render when the data is ready".
When in the control itself, we do this little dance
|
Beta Was this translation helpful? Give feedback.
-
Can we have the ability to turn off prerender per component in the module just like the standard blazor application has
This way the developer will have control over weather the component should be prerendered or not, preventing the twice-load issue |
Beta Was this translation helpful? Give feedback.
-
@vnetonline yes another IModuleControl property could be introduced (similar to RenderMode). The thing to remember is that if you turn off pre-rendering then your component's content will not be indexed by search engines - which may be fine if you are building an internal facing web application but if you are building a website, pre-rendering is essential for SEO. |
Beta Was this translation helpful? Give feedback.
-
hmmmm yes but it would benefit those who are building an Intranet, In my LearnOqtane.com site the course page needs to be indexed by SEO so need to find another way to stop the flickering. |
Beta Was this translation helpful? Give feedback.
-
@sbwalker I have a question: If I make my Index.razor
|
Beta Was this translation helpful? Give feedback.
-
@vnetonline RenderMode is a component level property - not a module level property. This is why it is defined in the component rather than the ModuleInfo class. In a public facing scenario your frontend Index component would often be Static but your backend admin components would be Interactive. |
Beta Was this translation helpful? Give feedback.
-
PersistentComponentState is the recommended approach from Microsoft for avoiding the execution of data access logic multiple times in mixed render modes: |
Beta Was this translation helpful? Give feedback.
Long answer....
Blazor is a SPA framework where each component is a stateless chunk of UI on the page. State is communicated to components in various ways - each with their own specific behavior. Oqtane leverages a cascading parameter called PageState to pass state from the router to all nested components in the UI hierarchy. Whenever a cascading parameter is changed, all components which reference it are immediately notified (note this is a Blazor behavior - not an Oqtane behavior). This results in components being re-rendered to reflect the latest state. This all makes sense in theory... however in practice it behaves a bit different than expected.
In a default Oqtane installation it wi…