From de75354d9e1c67ea7d05a9b753dffd83229c467d Mon Sep 17 00:00:00 2001 From: Sebastian Huus <54155988+sebastianhuus@users.noreply.github.com> Date: Sat, 2 May 2026 22:55:46 +0200 Subject: [PATCH] Fix Svelte 5 reactive race when closing project settings modal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When closeSettings() sets modalProps to null, Svelte 5 can propagate updated props to child components before the outer {#if} guard destroys the block, causing TypeErrors like "null is not an object (evaluating '$.get(modalProps).state')" and "undefined is not an object (evaluating 'data.projectId')". Introduce stableModalData — a $state updated only in $effect.pre when modalProps is non-null. Child components receive stableModalData (frozen at the last valid value during teardown) rather than reading modalProps directly, breaking the reactive propagation chain. --- .../components/views/GlobalModalRouter.svelte | 38 ++++++++++++------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/apps/desktop/src/components/views/GlobalModalRouter.svelte b/apps/desktop/src/components/views/GlobalModalRouter.svelte index d64e2c3d24..1904d8670a 100644 --- a/apps/desktop/src/components/views/GlobalModalRouter.svelte +++ b/apps/desktop/src/components/views/GlobalModalRouter.svelte @@ -83,6 +83,18 @@ const modalProps = $derived(mapModalStateToProps(uiState.global.modal.current)); + // Svelte 5 can propagate prop changes into the child block before the outer + // {#if} re-evaluates and unmounts it, causing crashes like + // "undefined is not an object (evaluating 'data.projectId')". stableModalData + // latches the last non-null modalProps so the children always see valid data; + // the {#if modalProps && stableModalData} gate handles visibility. + let stableModalData = $state(null); + $effect.pre(() => { + if (modalProps !== null) { + stableModalData = modalProps; + } + }); + let modal = $state(); // Show the modal whenever modalProps becomes truthy. @@ -98,7 +110,7 @@ // If the login confirmation modal is closed without explicit user action (e.g., via ESC), // we should reject the incoming user to maintain state consistency. // We check if there's still an incoming user to avoid calling reject after accept/reject buttons. - if (modalProps?.state.type === "login-confirmation") { + if (stableModalData?.state.type === "login-confirmation") { if (userService.incomingUserLogin) { userService.rejectIncomingUser(); } @@ -121,23 +133,23 @@ } -{#if modalProps} +{#if modalProps && stableModalData} close()} > - {#if modalProps.state.type === "commit-failed"} - - {:else if modalProps.state.type === "author-missing"} - - {:else if modalProps.state.type === "general-settings"} - - {:else if modalProps.state.type === "project-settings"} - - {:else if modalProps.state.type === "login-confirmation"} - + {#if stableModalData.state.type === "commit-failed"} + + {:else if stableModalData.state.type === "author-missing"} + + {:else if stableModalData.state.type === "general-settings"} + + {:else if stableModalData.state.type === "project-settings"} + + {:else if stableModalData.state.type === "login-confirmation"} + {/if} {/if}