From 8b8868bc669b62ed48956859b915581dd4b731bc Mon Sep 17 00:00:00 2001 From: "Adolfo R. Brandes" Date: Tue, 12 May 2026 16:35:52 -0300 Subject: [PATCH] fix: dedupe theme core in useThemeCore Under React StrictMode the effect ran twice on mount and appended a second core stylesheet after the variant link, letting core :root declarations win over variants that shared the same custom properties. Closes #261 Co-Authored-By: Claude --- .../react/hooks/theme/useThemeCore.test.ts | 30 +++++++++++++++++++ runtime/react/hooks/theme/useThemeCore.ts | 9 ++++++ 2 files changed, 39 insertions(+) diff --git a/runtime/react/hooks/theme/useThemeCore.test.ts b/runtime/react/hooks/theme/useThemeCore.test.ts index a8afc484..29765051 100644 --- a/runtime/react/hooks/theme/useThemeCore.test.ts +++ b/runtime/react/hooks/theme/useThemeCore.test.ts @@ -62,4 +62,34 @@ describe('useThemeCore', () => { expect(document.head.querySelectorAll('link').length).toBe(0); expect(onComplete).toHaveBeenCalledTimes(1); }); + + it('should not append a duplicate link when a core link for the same URL already exists', () => { + const existing = document.createElement('link'); + existing.href = testParams.themeCore.url; + existing.rel = 'stylesheet'; + existing.dataset.themeCore = 'true'; + document.head.appendChild(existing); + + renderHook(() => useThemeCore(testParams)); + + const coreLinks = document.head.querySelectorAll('link[data-theme-core="true"]'); + expect(coreLinks.length).toBe(1); + expect(coreLinks[0]).toBe(existing); + expect(onComplete).toHaveBeenCalledTimes(1); + }); + + it('should replace an existing core link when the URL changes', () => { + const existing = document.createElement('link'); + existing.href = 'https://example.com/old-core.min.css'; + existing.rel = 'stylesheet'; + existing.dataset.themeCore = 'true'; + document.head.appendChild(existing); + + renderHook(() => useThemeCore(testParams)); + + const coreLinks = document.head.querySelectorAll('link[data-theme-core="true"]'); + expect(coreLinks.length).toBe(1); + expect(coreLinks[0]).not.toBe(existing); + expect((coreLinks[0] as HTMLLinkElement).href).toBe(testParams.themeCore.url); + }); }); diff --git a/runtime/react/hooks/theme/useThemeCore.ts b/runtime/react/hooks/theme/useThemeCore.ts index 1c1a1074..02cc7278 100644 --- a/runtime/react/hooks/theme/useThemeCore.ts +++ b/runtime/react/hooks/theme/useThemeCore.ts @@ -27,6 +27,15 @@ const useThemeCore = ({ return; } + const existingThemeCoreLink: HTMLLinkElement | null = document.head.querySelector('link[data-theme-core="true"]'); + if (existingThemeCoreLink) { + if (existingThemeCoreLink.getAttribute('href') === themeCore.url) { + setIsThemeCoreComplete(true); + return; + } + existingThemeCoreLink.remove(); + } + const themeCoreLink = document.createElement('link'); themeCoreLink.href = themeCore.url; themeCoreLink.rel = 'stylesheet';