How TypeWords Configures User Settings Using a Component-Based Architecture
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. The store defines a flat SettingState interface that encompasses every user-adjustable option, from visual themes to FSRS algorithm parameters.
// 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 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:
- Store injection – Components import
useSettingStorefrom@/core/stores/setting - Direct binding – UI controls use
v-modelor:model-valuebound to store fields - Reactive propagation – Changes instantly reflect across all consumers (e.g., the theme hook reading
settingStore.theme) - Automatic persistence – The store's actions handle local storage and backend sync transparently
<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 provides a modal abstraction:
<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:
// 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 |
Pinia store: state definition, actions, persistence logic |
app/pages/setting.vue |
Settings page shell assembling all component sections |
app/components/setting/CommonSetting.vue |
General UI preferences |
app/components/setting/FsrsSetting.vue |
Spaced-repetition algorithm tuning |
app/components/setting/WordSetting.vue |
Word-practice configuration |
app/components/setting/ArticleSetting.vue |
Article-practice settings |
app/components/setting/SoundSetting.vue |
Audio feedback controls |
app/components/setting/SettingItem.vue |
Reusable label-control wrapper |
app/components/setting/SettingDialog.vue |
Modal for complex setting edits |
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 - 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()andsetState()actions manage local storage and backend sync without component awareness - Reusable primitives:
SettingItemandSettingDialogprovide 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. 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, then create a new component under app/components/setting/ following the pattern of existing components like SoundSetting.vue. Import and register your component in 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 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.
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 →