From cb5c2fd2b9fd7faca02b52a65ca5a0b0070789fb Mon Sep 17 00:00:00 2001 From: Daniel Harvey Date: Thu, 21 May 2026 06:22:55 -0400 Subject: [PATCH 1/2] fix(public): move DeepDrftMenu dark-mode sync to OnAfterRenderAsync to unhang SSR prerender under interactive Routes --- .../Layout/DeepDrftMenu.razor | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/DeepDrftPublic.Client/Layout/DeepDrftMenu.razor b/DeepDrftPublic.Client/Layout/DeepDrftMenu.razor index 4c7ce0c..ad495c5 100644 --- a/DeepDrftPublic.Client/Layout/DeepDrftMenu.razor +++ b/DeepDrftPublic.Client/Layout/DeepDrftMenu.razor @@ -77,23 +77,21 @@ private bool _mobileMenuOpen; private Guid _viewportSubscriptionId; - protected override async Task OnInitializedAsync() - { - // During SSR prerender the dark-mode state is already seeded by the server-side - // DarkModeService (via IHttpContextAccessor + DarkModeSettings + PersistentComponentState). - // Invoking the EventCallback here during prerender triggers a re-render cycle on the - // SSR renderer that never completes, hanging the page. Guard to interactive-only so - // the cookie sync only runs when the component is actually mounted in the browser. - if (!RendererInfo.IsInteractive) return; - - IsDarkMode = DarkModeCookieService.GetDarkModeAsync(); - await IsDarkModeChanged.InvokeAsync(IsDarkMode); - } - protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { + // Dark-mode cookie sync runs here (not OnInitializedAsync) because + // OnAfterRenderAsync(firstRender) is guaranteed not to execute during any + // SSR prerender pass. During prerender (now interactive across all of Routes), + // awaiting IsDarkModeChanged.InvokeAsync triggers a parent re-render cycle + // that cannot complete on the prerender renderer and hangs the response. + // Server-side DarkModeService has already seeded DarkModeSettings via + // PersistentComponentState, so the prerender paint is already correct; + // this call just reconciles with the live cookie once on the client. + IsDarkMode = DarkModeCookieService.GetDarkModeAsync(); + await IsDarkModeChanged.InvokeAsync(IsDarkMode); + _viewportSubscriptionId = Guid.NewGuid(); await BrowserViewportService.SubscribeAsync( _viewportSubscriptionId, From 2fac9e51e6540f73ad28774b699a6fe6021481b8 Mon Sep 17 00:00:00 2001 From: Daniel Harvey Date: Thu, 21 May 2026 06:29:13 -0400 Subject: [PATCH 2/2] =?UTF-8?q?refactor(client):=20rename=20GetDarkModeAsy?= =?UTF-8?q?nc=E2=86=92GetDarkMode,=20drop=20commented=20JS=20interop,=20fi?= =?UTF-8?q?x=20misleading=20comment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Layout/DeepDrftMenu.razor | 18 +++++++++--------- .../Services/DarkModeCookieService.cs | 5 +---- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/DeepDrftPublic.Client/Layout/DeepDrftMenu.razor b/DeepDrftPublic.Client/Layout/DeepDrftMenu.razor index ad495c5..00b5c06 100644 --- a/DeepDrftPublic.Client/Layout/DeepDrftMenu.razor +++ b/DeepDrftPublic.Client/Layout/DeepDrftMenu.razor @@ -81,15 +81,15 @@ { if (firstRender) { - // Dark-mode cookie sync runs here (not OnInitializedAsync) because - // OnAfterRenderAsync(firstRender) is guaranteed not to execute during any - // SSR prerender pass. During prerender (now interactive across all of Routes), - // awaiting IsDarkModeChanged.InvokeAsync triggers a parent re-render cycle - // that cannot complete on the prerender renderer and hangs the response. - // Server-side DarkModeService has already seeded DarkModeSettings via - // PersistentComponentState, so the prerender paint is already correct; - // this call just reconciles with the live cookie once on the client. - IsDarkMode = DarkModeCookieService.GetDarkModeAsync(); + // Runs here (not OnInitializedAsync) because OnAfterRenderAsync(firstRender) + // is guaranteed not to execute during any SSR prerender pass. During prerender + // (now interactive across all of Routes), awaiting IsDarkModeChanged.InvokeAsync + // triggers a parent re-render cycle that cannot complete on the prerender renderer + // and hangs the response. Server-side DarkModeService has already seeded + // DarkModeSettings via PersistentComponentState, so the prerender paint is + // already correct; this call reads that persisted value and propagates it to the + // parent so the menu's IsDarkMode parameter stays consistent with DarkModeSettings. + IsDarkMode = DarkModeCookieService.GetDarkMode(); await IsDarkModeChanged.InvokeAsync(IsDarkMode); _viewportSubscriptionId = Guid.NewGuid(); diff --git a/DeepDrftPublic.Client/Services/DarkModeCookieService.cs b/DeepDrftPublic.Client/Services/DarkModeCookieService.cs index 0b12689..177894e 100644 --- a/DeepDrftPublic.Client/Services/DarkModeCookieService.cs +++ b/DeepDrftPublic.Client/Services/DarkModeCookieService.cs @@ -7,12 +7,9 @@ public class DarkModeCookieService(DarkModeSettings darkModeSetting, IJSRuntime { private const int EXPIRY_DAYS = 365; - public bool GetDarkModeAsync() + public bool GetDarkMode() { return darkModeSetting.IsDarkMode; - // var value = await js.InvokeAsync("eval", - // $"document.cookie.split('; ').find(c => c.startsWith('{COOKIE_NAME}='))?.split('=')[1]"); - // return value == "true"; } public async ValueTask SetDarkModeAsync(bool isDarkMode)