# How TypeWords Configures User Settings Using a Component-Based Architecture

> Learn how TypeWords configures user settings with a component based architecture using a centralized Pinia store and reactive two way binding for efficient management.

- Repository: [Zyronon/TypeWords](https://github.com/zyronon/TypeWords)
- Tags: architecture
- Published: 2026-09-03

---

**TypeWords uses a centralized Pinia store paired with focused, single-responsibility Vue components located in `app/components/setting/` to manage all user configuration through reactive two-way binding.**

The TypeWords typing practice application organizes its settings system around a clean separation of concerns: a single **Pinia store** acts as the source of truth, while a collection of specialized **Vue components** handle the presentation and interaction for each logical settings group. This architecture ensures that theme preferences, sound volumes, practice modes, and keyboard shortcuts remain synchronized across the entire application.

## Central Settings Store

All configurable values live in [`app/core/stores/setting.ts`](https://github.com/zyronon/TypeWords/blob/main/app/core/stores/setting.ts). The store defines a flat `SettingState` interface that encompasses every user-adjustable option, from visual themes to FSRS algorithm parameters.

```ts
// app/core/stores/setting.ts
export const useSettingStore = defineStore('setting', {
  state: (): SettingState => ({
    theme: 'auto',
    keyboardSound: true,
    keyboardSoundFile: '机械键盘2',
    shortcutKeyMap: { … },
    // … additional fields for word practice, article practice, etc.
  }),
  actions: {
    async init() { /* load from local storage & sync with backend */ },
    setState(patch) { this.$patch(patch) },
  },
})

```

The `init()` action hydrates state from local storage and pushes to a Supabase backend, while `setState()` provides a convenient patch method for bulk updates.

## Component Structure for Settings UI

The settings interface in [`app/pages/setting.vue`](https://github.com/zyronon/TypeWords/blob/main/app/pages/setting.vue) composes multiple focused components from `app/components/setting/`. Each component owns a distinct domain:

- **CommonSetting.vue** – General UI preferences including theme selection, font size controls, and first-run flags
- **FsrsSetting.vue** – Spaced-repetition algorithm configuration (intervals, retention targets)
- **WordSetting.vue** – Word-practice parameters such as practice mode, review ratio thresholds, and auto-add behavior
- **ArticleSetting.vue** – Article-practice options for sound feedback and typing speed targets
- **SoundSetting.vue** – Global sound toggles, volume sliders, and sound-file selection
- **SettingItem.vue** – Reusable label-control wrapper used across all setting panels
- **SettingDialog.vue** – Modal wrapper for complex edits like shortcut key remapping
- **Log.vue** – Changelog display for version updates

This component-based approach keeps each file under 200 lines, making individual settings domains easy to test and modify without side effects.

## Data Flow Between Store and Components

The architecture follows a strict unidirectional-right-to-left pattern with Pinia enabling reactive propagation:

1. **Store injection** – Components import `useSettingStore` from `@/core/stores/setting`
2. **Direct binding** – UI controls use `v-model` or `:model-value` bound to store fields
3. **Reactive propagation** – Changes instantly reflect across all consumers (e.g., the theme hook reading `settingStore.theme`)
4. **Automatic persistence** – The store's actions handle local storage and backend sync transparently

```vue
<script setup lang="ts">
import { useSettingStore } from '@/core/stores/setting'
const settingStore = useSettingStore()
</script>

<template>
  <SettingItem :label="$t('theme')">
    <select v-model="settingStore.theme">
      <option value="auto">{{ $t('auto') }}</option>
      <option value="light">{{ $t('light') }}</option>
      <option value="dark">{{ $t('dark') }}</option>
    </select>
  </SettingItem>
</template>

```

The `SettingItem` wrapper component standardizes the layout—label on the left, control on the right—across all settings panels.

## Handling Complex Settings with Dialogs

For multi-step or structured data like keyboard shortcuts, [`SettingDialog.vue`](https://github.com/zyronon/TypeWords/blob/main/SettingDialog.vue) provides a modal abstraction:

```vue
<script setup lang="ts">
import { useSettingStore } from '@/core/stores/setting'
import SettingDialog from '@/components/setting/SettingDialog.vue'

const settingStore = useSettingStore()
function saveShortcut(key: string, value: string) {
  settingStore.shortcutKeyMap[key] = value
}
</script>

<template>
  <SettingDialog @confirm="saveShortcut('Next', $event)" />
</template>

```

The dialog emits confirmation events rather than mutating state directly, maintaining clean component boundaries.

## Application-Wide Initialization

Settings persistence activates during app startup via [`app/core/composables/useInit.ts`](https://github.com/zyronon/TypeWords/blob/main/app/core/composables/useInit.ts):

```ts
// app/core/composables/useInit.ts
import { useSettingStore } from '@/core/stores/setting'

export async function initApp() {
  const settingStore = useSettingStore()
  await settingStore.init()  // hydrates from localStorage, syncs with Supabase
  // additional initialization …
}

```

This ensures user preferences are available before any UI renders, preventing flash-of-unstyled-content or incorrect defaults.

## Key Files in the Settings System

| File | Responsibility |
|------|---------------|
| [`app/core/stores/setting.ts`](https://github.com/zyronon/TypeWords/blob/main/app/core/stores/setting.ts) | Pinia store: state definition, actions, persistence logic |
| [`app/pages/setting.vue`](https://github.com/zyronon/TypeWords/blob/main/app/pages/setting.vue) | Settings page shell assembling all component sections |
| [`app/components/setting/CommonSetting.vue`](https://github.com/zyronon/TypeWords/blob/main/app/components/setting/CommonSetting.vue) | General UI preferences |
| [`app/components/setting/FsrsSetting.vue`](https://github.com/zyronon/TypeWords/blob/main/app/components/setting/FsrsSetting.vue) | Spaced-repetition algorithm tuning |
| [`app/components/setting/WordSetting.vue`](https://github.com/zyronon/TypeWords/blob/main/app/components/setting/WordSetting.vue) | Word-practice configuration |
| [`app/components/setting/ArticleSetting.vue`](https://github.com/zyronon/TypeWords/blob/main/app/components/setting/ArticleSetting.vue) | Article-practice settings |
| [`app/components/setting/SoundSetting.vue`](https://github.com/zyronon/TypeWords/blob/main/app/components/setting/SoundSetting.vue) | Audio feedback controls |
| [`app/components/setting/SettingItem.vue`](https://github.com/zyronon/TypeWords/blob/main/app/components/setting/SettingItem.vue) | Reusable label-control wrapper |
| [`app/components/setting/SettingDialog.vue`](https://github.com/zyronon/TypeWords/blob/main/app/components/setting/SettingDialog.vue) | Modal for complex setting edits |
| [`app/core/composables/useInit.ts`](https://github.com/zyronon/TypeWords/blob/main/app/core/composables/useInit.ts) | Boot-time store hydration |

## Summary

- **Single source of truth**: All settings reside in the Pinia store at [`app/core/stores/setting.ts`](https://github.com/zyronon/TypeWords/blob/main/app/core/stores/setting.ts)
- **Component decomposition**: Each logical group (Common, Sound, Word, Article, FSRS) has its own focused Vue component
- **Reactive binding**: Components bind directly to store state; Pinia handles cross-app synchronization
- **Transparent persistence**: The store's `init()` and `setState()` actions manage local storage and backend sync without component awareness
- **Reusable primitives**: `SettingItem` and `SettingDialog` provide consistent UI patterns across all settings panels

## Frequently Asked Questions

### How does TypeWords persist settings between sessions?

The `setting` store's `init()` action loads state from browser local storage during app startup and synchronizes with a Supabase backend, as implemented in [`app/core/composables/useInit.ts`](https://github.com/zyronon/TypeWords/blob/main/app/core/composables/useInit.ts). Subsequent changes trigger automatic saves through the store's reactive watchers.

### Can I add custom settings without modifying multiple files?

Yes. Extend the `SettingState` interface in [`app/core/stores/setting.ts`](https://github.com/zyronon/TypeWords/blob/main/app/core/stores/setting.ts), then create a new component under `app/components/setting/` following the pattern of existing components like [`SoundSetting.vue`](https://github.com/zyronon/TypeWords/blob/main/SoundSetting.vue). Import and register your component in [`app/pages/setting.vue`](https://github.com/zyronon/TypeWords/blob/main/app/pages/setting.vue).

### Why does TypeWords use Pinia instead of Vue's Composition API for global state?

While the Composition API provides `provide/inject`, Pinia offers structured state management with DevTools integration, automatic persistence plugins, and clear action/mutation boundaries. The TypeWords codebase leverages these features for complex operations like partial state patching via `setState()`.

### How are theme changes applied instantly across the application?

The theme hook in [`app/core/hooks/theme.ts`](https://github.com/zyronon/TypeWords/blob/main/app/core/hooks/theme.ts) reads `settingStore.theme` reactively. When any component updates this value, dependent computed properties and watchers trigger CSS class changes or style injections without requiring explicit event emission or prop drilling.