How Folia Persists Settings with useSettingsUiStore: A Complete Guide to localStorage and Zustand
Folia's useSettingsUiStore combines Zustand for state management with localStorage for persistence, using helper functions to read values at initialization and write updates on every change.
Folia, an open-source music player available in the chthollyphile/folia-major repository, implements a robust settings persistence layer using the useSettingsUiStore hook. This Zustand-based store manages user preferences—from audio quality to visualizer modes—ensuring they survive browser refreshes and app restarts. The implementation follows a clear read-write pattern: values are hydrated from localStorage when the store initializes, and every user interaction triggers an immediate update to both the Zustand state and the browser's persistent storage.
The Three-Step Persistence Flow
The useSettingsUiStore implementation in src/stores/useSettingsUiStore.ts follows a distinct three-phase pattern to ensure settings persist across sessions.
Step 1: Reading Stored Values at Initialization
When the store first loads, dedicated helper functions read raw strings from localStorage and convert them to typed values. These functions guard against server-side rendering by checking typeof window and provide sensible defaults when no saved value exists.
For example, the readStoredAudioQuality function retrieves the audio quality setting:
const readStoredAudioQuality = (): AudioQuality => {
if (typeof window === 'undefined') return 'exhigh';
const saved = localStorage.getItem('default_audio_quality');
return saved === 'lossless' || saved === 'hires' ? saved : 'exhigh';
};
// Source: src/stores/useSettingsUiStore.ts#L73-L80
Similar helpers exist for readStoredBackgroundOpacity (lines 82-90) and readStoredVisualizerMode, each handling type coercion and validation.
Step 2: Exposing Values Through Zustand
The create function builds the initial state by invoking these reader functions, making persisted values immediately available to any React component:
export const useSettingsUiStore = create<SettingsUiState>((set, get) => ({
audioQuality: readStoredAudioQuality(),
backgroundOpacity: readStoredBackgroundOpacity(),
visualizerMode: readStoredVisualizerMode(),
// … additional settings
}));
// Source: src/stores/useSettingsUiStore.ts#L55-L63
This design ensures that components receive the saved values on the first render without asynchronous loading states.
Step 3: Writing Updates Back to Storage
Every user interaction triggers a store action that updates both the Zustand state and localStorage. For boolean toggles, the store uses the generic setStoredBoolean helper:
handleToggleCoverColorBg: (enable) => {
setStoredBoolean('use_cover_color_bg', enable);
set({ useCoverColorBg: enable });
notify(get, { type: 'info', text: enable ? '添加封面色彩' : '使用默认色彩' });
},
// Source: src/stores/useSettingsUiStore.ts#L1004-L1011
For numeric values like opacity, the action writes directly to localStorage:
handleSetBackgroundOpacity: (opacity) => {
if (typeof window !== 'undefined') {
localStorage.setItem('background_opacity', String(opacity));
}
set({ backgroundOpacity: opacity });
},
// Source: src/stores/useSettingsUiStore.ts#L1444-L1450
Helper Functions for Boolean Persistence
The store encapsulates repetitive localStorage logic in two generic utilities that prevent SSR crashes and ensure consistent formatting:
const getStoredBoolean = (key: string, fallback: boolean) => {
if (typeof window === 'undefined') return fallback;
const saved = localStorage.getItem(key);
return saved !== null ? saved === 'true' : fallback;
};
const setStoredBoolean = (key: string, value: boolean) => {
if (typeof window !== 'undefined') {
localStorage.setItem(key, String(value));
}
};
// Source: src/stores/useSettingsUiStore.ts#L40-L53
All toggle handlers—including handleToggleCoverColorBg, minimize-to-tray, and static mode—delegate to these helpers, ensuring uniform persistence format across the application.
Persisting Complex Objects with JSON
For structured settings like CadenzaTuning, the store uses JSON serialization. The reader handles potential key migrations (noting the legacy cadenze_tuning key):
const readStoredCadenzaTuning = (): CadenzaTuning => {
// …
const saved = localStorage.getItem('cadenza_tuning') ?? localStorage.getItem('cadenze_tuning');
if (!saved) return DEFAULT_CADENZA_TUNING;
const parsed = JSON.parse(saved) as Partial<CadenzaTuning>;
// …
};
// Source: src/stores/useSettingsUiStore.ts#L76-L92
Corresponding setter methods stringify the object before storage, ensuring complex configuration objects survive page reloads.
Practical Implementation Examples
Example 1: Toggling a Boolean Setting
Components consume the store through a simple hook call, reading the current state and invoking the handler on change:
import { useSettingsUiStore } from '@/stores/useSettingsUiStore';
function CoverColorToggle() {
const { useCoverColorBg, handleToggleCoverColorBg } = useSettingsUiStore();
return (
<label>
<input
type="checkbox"
checked={useCoverColorBg}
onChange={(e) => handleToggleCoverColorBg(e.target.checked)}
/>
Use cover‑color background
</label>
);
}
When the user toggles the checkbox, handleToggleCoverColorBg immediately updates localStorage via setStoredBoolean and refreshes the Zustand state, keeping the UI synchronized with persistent storage.
Example 2: Adjusting Numeric Opacity
Slider components for opacity settings follow the same pattern, using type-specific handlers:
import { useSettingsUiStore } from '@/stores/useSettingsUiStore';
import Slider from '@mui/material/Slider';
function BackgroundOpacitySlider() {
const { backgroundOpacity, handleSetBackgroundOpacity } = useSettingsUiStore();
return (
<Slider
min={0}
max={1}
step={0.01}
value={backgroundOpacity}
onChange={(_, val) => handleSetBackgroundOpacity(val as number)}
/>
);
}
The handleSetBackgroundOpacity method converts the numeric value to a string for localStorage and updates the store, ensuring the slider position persists after page reloads.
Example 3: Loading Complex Configuration
For object-based settings like audio tuning, components import the store and invoke JSON-aware setters:
import { useSettingsUiStore } from '@/stores/useSettingsUiStore';
// Component initialization
const { cadenzaTuning, handleSetCadenzaTuning } = useSettingsUiStore();
useEffect(() => {
fetch('/api/cadenza-preset')
.then((r) => r.json())
.then((preset) => handleSetCadenzaTuning(preset));
}, []);
Internally, handleSetCadenzaTuning serializes the object to JSON before writing to localStorage, while readStoredCadenzaTuning handles parsing and validation on application startup.
Summary
- Initialization: The
useSettingsUiStorereads persisted values fromlocalStoragethrough specialized helper functions likereadStoredAudioQualityandreadStoredBackgroundOpacitywhen the Zustand store initializes. - State Management: All settings reside in a single Zustand store at
src/stores/useSettingsUiStore.ts, providing a single source of truth for React components. - Persistence: Boolean values use
setStoredBooleanandgetStoredBooleanhelpers, while complex objects useJSON.stringifyandJSON.parsefor serialization. - SSR Safety: Every read and write operation checks
typeof window !== 'undefined'to prevent errors during server-side rendering. - Decoupled Architecture: UI components interact only with Zustand actions, never directly with
localStorage, making the persistence mechanism easy to modify or replace.
Frequently Asked Questions
How does useSettingsUiStore handle server-side rendering?
Every helper function in src/stores/useSettingsUiStore.ts checks typeof window === 'undefined' before accessing localStorage. If the code runs in a Node.js environment, the functions return default values immediately, preventing ReferenceError crashes during SSR or static site generation.
What happens when a user changes a setting?
The component calls a store action like handleToggleCoverColorBg or handleSetBackgroundOpacity. This action performs two operations atomically: it writes the new value to localStorage (or calls setStoredBoolean for booleans), then updates the Zustand state using the set function. This ensures the UI and persistent storage remain synchronized.
Where are complex settings like tuning configurations stored?
Complex objects such as CadenzaTuning are serialized to JSON strings before storage. The readStoredCadenzaTuning function (lines 76-92) retrieves the string from localStorage, parses it with JSON.parse, and validates the structure against default values. This approach allows nested configuration objects to persist across browser sessions.
Can I use useSettingsUiStore outside of React components?
Yes. Because the store uses Zustand's create function exported from src/stores/useSettingsUiStore.ts, you can access the current state or dispatch actions from non-React code using useSettingsUiStore.getState(). This is useful for utility functions or event listeners that need to read settings without rendering a component.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →