# How Folia Persists Settings with useSettingsUiStore: A Complete Guide to localStorage and Zustand

> Learn how Folia uses useSettingsUiStore with Zustand and localStorage to persist UI settings across sessions. This guide explains initialization and update mechanisms.

- Repository: [冬霧/folia-major](https://github.com/chthollyphile/folia-major)
- Tags: deep-dive
- Published: 2026-07-06

---

**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`](https://github.com/chthollyphile/folia-major/blob/main/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:

```typescript
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:

```typescript
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:

```typescript
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`:

```typescript
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:

```typescript
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):

```typescript
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:

```tsx
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:

```tsx
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:

```typescript
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 `useSettingsUiStore` reads persisted values from `localStorage` through specialized helper functions like `readStoredAudioQuality` and `readStoredBackgroundOpacity` when the Zustand store initializes.
- **State Management**: All settings reside in a single Zustand store at [`src/stores/useSettingsUiStore.ts`](https://github.com/chthollyphile/folia-major/blob/main/src/stores/useSettingsUiStore.ts), providing a single source of truth for React components.
- **Persistence**: Boolean values use `setStoredBoolean` and `getStoredBoolean` helpers, while complex objects use `JSON.stringify` and `JSON.parse` for 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`](https://github.com/chthollyphile/folia-major/blob/main/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`](https://github.com/chthollyphile/folia-major/blob/main/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.