Preference Usage Guide

April 12, 2026 · View on GitHub

This guide covers how to use the Preference system in React components and services.

React Hooks

usePreference (Single Preference)

import { usePreference } from '@data/hooks/usePreference'

// Basic usage - optimistic updates (default)
const [theme, setTheme] = usePreference('app.theme.mode')

// Update the value
await setTheme('dark')

// With pessimistic updates (wait for confirmation)
const [apiKey, setApiKey] = usePreference('api.key', { optimistic: false })

usePreferences (Multiple Preferences)

import { usePreferences } from '@data/hooks/usePreference'

// Read multiple preferences at once
const { theme, language, fontSize } = usePreferences([
  'app.theme.mode',
  'app.language',
  'chat.message.font_size'
])

Update Strategies

Optimistic Updates (Default)

UI updates immediately, then syncs to database. Automatic rollback on failure.

const [theme, setTheme] = usePreference('app.theme.mode')

const handleThemeChange = async (newTheme: string) => {
  try {
    await setTheme(newTheme) // UI updates immediately
  } catch (error) {
    // UI automatically rolls back
    console.error('Theme update failed:', error)
  }
}

Best for:

  • Frequent changes (theme, font size)
  • Non-critical settings
  • Better perceived performance

Pessimistic Updates

Waits for database confirmation before updating UI.

const [apiKey, setApiKey] = usePreference('api.key', { optimistic: false })

const handleApiKeyChange = async (newKey: string) => {
  try {
    await setApiKey(newKey) // Waits for DB confirmation
    toast.success('API key saved')
  } catch (error) {
    toast.error('Failed to save API key')
  }
}

Best for:

  • Security-sensitive settings (API keys, passwords)
  • Settings that affect external services
  • When confirmation feedback is important

PreferenceService Direct Usage

For non-React code or batch operations.

Renderer Process

import { preferenceService } from '@data/PreferenceService'

// Get single preference
const theme = await preferenceService.get('app.theme.mode')

// Get multiple preferences
const settings = await preferenceService.getMultiple([
  'app.theme.mode',
  'app.language'
])
// Returns: { 'app.theme.mode': 'dark', 'app.language': 'en' }

// Get with default value
const fontSize = await preferenceService.get('chat.message.font_size') ?? 14

Set Preferences

// Set single preference (optimistic by default)
await preferenceService.set('app.theme.mode', 'dark')

// Set with pessimistic update
await preferenceService.set('api.key', 'secret', { optimistic: false })

// Set multiple preferences at once
await preferenceService.setMultiple({
  'app.theme.mode': 'dark',
  'app.language': 'en',
  'chat.message.font_size': 16
})

Subscribe to Changes

// Subscribe to preference changes (useful in services)
const unsubscribe = preferenceService.subscribe('app.theme.mode', (newValue) => {
  logger.info('Theme changed to:', { theme: newValue })
})

// Cleanup when done
unsubscribe()

Main Process

In the main process, PreferenceService is lifecycle-managed. Access it via application.get():

import { application } from '@application'

const preferenceService = application.get('PreferenceService')

// Get/set preferences
const theme = preferenceService.get('app.theme.mode')
preferenceService.set('app.theme.mode', 'dark')

// Subscribe to changes
const unsubscribe = preferenceService.subscribeChange('app.theme.mode', (newValue) => {
  logger.info('Theme changed to:', { theme: newValue })
})

Note: Do NOT import PreferenceService directly in main process code. The renderer @data/PreferenceService is a separate singleton for the renderer process only.

Common Patterns

Settings Form

function SettingsForm() {
  const [theme, setTheme] = usePreference('app.theme.mode')
  const [language, setLanguage] = usePreference('app.language')
  const [fontSize, setFontSize] = usePreference('chat.message.font_size')

  return (
    <form>
      <select value={theme} onChange={e => setTheme(e.target.value)}>
        <option value="light">Light</option>
        <option value="dark">Dark</option>
        <option value="system">System</option>
      </select>

      <select value={language} onChange={e => setLanguage(e.target.value)}>
        <option value="en">English</option>
        <option value="zh">中文</option>
      </select>

      <input
        type="number"
        value={fontSize}
        onChange={e => setFontSize(Number(e.target.value))}
        min={12}
        max={24}
      />
    </form>
  )
}

Feature Toggle

function ChatMessage({ message }) {
  const [showTimestamp] = usePreference('chat.display.show_timestamp')

  return (
    <div className="message">
      <p>{message.content}</p>
      {showTimestamp && <span className="timestamp">{message.createdAt}</span>}
    </div>
  )
}

Conditional Rendering Based on Settings

function App() {
  const [theme] = usePreference('app.theme.mode')
  const [sidebarPosition] = usePreference('app.sidebar.position')

  return (
    <div className={`app theme-${theme}`}>
      {sidebarPosition === 'left' && <Sidebar />}
      <MainContent />
      {sidebarPosition === 'right' && <Sidebar />}
    </div>
  )
}

Batch Settings Update

async function resetToDefaults() {
  await preferenceService.setMultiple({
    'app.theme.mode': 'system',
    'app.language': 'en',
    'chat.message.font_size': 14,
    'chat.display.show_timestamp': true
  })
}

Adding New Preference Keys

For a complete guide on adding new preferences, including naming conventions and design principles, see Preference Schema Guide.

Quick summary:

  1. Add custom types to preferenceTypes.ts (if needed)
  2. Add key to PreferenceSchemas interface in preferenceSchemas.ts
  3. Add default value to DefaultPreferences
  4. Use in code with full type safety

Best Practices

  1. Choose update strategy wisely: Optimistic for UX, pessimistic for critical settings
  2. Batch related updates: Use setMultiple when changing multiple related settings
  3. Provide sensible defaults: All preferences should have default values
  4. Keep values atomic: One preference = one logical setting
  5. Use consistent naming: Follow domain.feature.setting pattern

Preference vs Other Storage

ScenarioUse
User theme preferenceusePreference('app.theme.mode')
Window positionusePersistCache (can be lost without impact)
API keyusePreference with pessimistic updates
Search historyusePersistCache (nice to have)
Conversation historyDataApiService (business data)