-
-
Notifications
You must be signed in to change notification settings - Fork 452
fix: address settings-related hydration issues using prehydrate #1952
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 5 commits
2c22012
70eb969
5da2e86
a682a43
9bcf33c
885aec6
4729a53
afe15cc
f751a9a
e1466f8
bf7ab9d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,3 @@ | ||
| import type { RemovableRef } from '@vueuse/core' | ||
| import { useLocalStorage } from '@vueuse/core' | ||
| import { ACCENT_COLORS } from '#shared/utils/constants' | ||
| import type { LocaleObject } from '@nuxtjs/i18n' | ||
| import { BACKGROUND_THEMES } from '#shared/utils/constants' | ||
|
|
@@ -73,22 +71,86 @@ const DEFAULT_SETTINGS: AppSettings = { | |
|
|
||
| const STORAGE_KEY = 'npmx-settings' | ||
|
|
||
| // Shared settings instance (singleton per app) | ||
| let settingsRef: RemovableRef<AppSettings> | null = null | ||
| /** | ||
| * Read settings from localStorage and merge with defaults. | ||
| */ | ||
| function normaliseSettings(input: AppSettings): AppSettings { | ||
| return { | ||
| ...input, | ||
| searchProvider: input.searchProvider === 'npm' ? 'npm' : 'algolia', | ||
| sidebar: { | ||
| ...input.sidebar, | ||
| collapsed: Array.isArray(input.sidebar?.collapsed) | ||
| ? input.sidebar.collapsed.filter((v): v is string => typeof v === 'string') | ||
| : [], | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| function readFromLocalStorage(): AppSettings { | ||
| try { | ||
| const raw = localStorage.getItem(STORAGE_KEY) | ||
| if (raw) { | ||
| const stored = JSON.parse(raw) | ||
| return normaliseSettings({ | ||
| ...DEFAULT_SETTINGS, | ||
| ...stored, | ||
| connector: { ...DEFAULT_SETTINGS.connector, ...stored.connector }, | ||
| sidebar: { ...DEFAULT_SETTINGS.sidebar, ...stored.sidebar }, | ||
| chartFilter: { ...DEFAULT_SETTINGS.chartFilter, ...stored.chartFilter }, | ||
| }) | ||
| } | ||
| } catch {} | ||
| return { ...DEFAULT_SETTINGS } | ||
| } | ||
|
|
||
| let syncInitialized = false | ||
|
|
||
| /** | ||
| * Composable for managing application settings with localStorage persistence. | ||
| * Settings are shared across all components that use this composable. | ||
| * Composable for managing application settings. | ||
| * | ||
| * Uses useState for SSR-safe hydration (server and client agree on initial | ||
| * values during hydration) and syncs with localStorage on the client. | ||
| * The onPrehydrate script in prehydrate.ts handles DOM-level patches | ||
| * (accent color, bg theme, collapsed sections, etc.) to prevent visual | ||
| * flash before hydration. | ||
| */ | ||
| export function useSettings() { | ||
| if (!settingsRef) { | ||
| settingsRef = useLocalStorage<AppSettings>(STORAGE_KEY, DEFAULT_SETTINGS, { | ||
| mergeDefaults: true, | ||
| }) | ||
| const settings = useState<AppSettings>(STORAGE_KEY, () => ({ ...DEFAULT_SETTINGS })) | ||
|
|
||
| if (import.meta.client && !syncInitialized) { | ||
| syncInitialized = true | ||
|
|
||
| // Read localStorage eagerly but apply after mount to prevent hydration | ||
| // mismatch. During hydration, useState provides server-matching defaults. | ||
| // After mount, we swap in the user's actual preferences from localStorage. | ||
| // Uses nuxtApp.hook('app:mounted') instead of onMounted so it works even | ||
| // when useSettings() is first called from a plugin (no component context). | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think this is the right fix. right now, with onPrehydrate, in many cases we're able to reflect the user's preferences immediately without any flash after mounting.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, good point. What approach do you think we could look toward here? I tried a few iterations but was only able to get satisfactory results using the The core issue is that The Open to suggestions if you have a different pattern in mind, I might be missing something in the Nuxt hydration lifecycle that could help here. |
||
| const stored = readFromLocalStorage() | ||
| const nuxtApp = useNuxtApp() | ||
|
|
||
| if (nuxtApp.isHydrating) { | ||
| nuxtApp.hook('app:mounted', () => { | ||
| settings.value = stored | ||
| }) | ||
| } else { | ||
| settings.value = stored | ||
| } | ||
|
|
||
| // Persist future changes back to localStorage | ||
| watch( | ||
| settings, | ||
| value => { | ||
| try { | ||
| localStorage.setItem(STORAGE_KEY, JSON.stringify(value)) | ||
| } catch {} | ||
| }, | ||
| { deep: true }, | ||
| ) | ||
| } | ||
|
|
||
| return { | ||
| settings: settingsRef, | ||
| settings, | ||
| } | ||
| } | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.