State Management Overview
CPR uses three layers of state management, each suited to a different scope and lifetime. Choosing the right layer keeps components simple and avoids unnecessary global state.
The Three Layers
| Layer | Tool | Scope | Lifetime | Example |
|---|---|---|---|---|
| Global store | Pinia defineStore | Entire app | Until page refresh | usePatientStore (patient list + pagination) |
| Shared singleton | Module-level ref in a composable | Entire app | Until page refresh | useDialog (one dialog visible at a time) |
| Local component | ref / reactive inside <script setup> | Single component | Until component unmounts | A form's reactive({...}) fields |
Decision Tree
Use this flowchart when adding new state to decide where it belongs.
Does more than one component need this state?
├── NO --> Local component state (ref / reactive in <script setup>)
└── YES
├── Is it domain data fetched from the API (lists, pagination, single records)?
│ └── YES --> Pinia store (e.g., useDoctorStore)
├── Is it UI-only state shared across the app (dialogs, snackbars, confirm prompts)?
│ └── YES --> Module-level shared composable (e.g., useDialog)
├── Is it auth / session data that must survive navigation?
│ └── YES --> Pinia store with hydrate/reset (useAuthStore)
└── Is it app-wide configuration (theme, defaults)?
└── YES --> Pinia store with localStorage persistence (useSettingsStore)Layer Details
1. Pinia Stores (app/stores/)
Pinia stores hold server-sourced domain data that multiple pages or components read. Most domain stores follow a minimal list + pagination shape. The auth store and settings store are exceptions with richer state and persistence logic.
Stores are data holders only. They do not contain API calls or business logic. That work belongs in domain composables (see below).
Component --> Domain Composable --> Service --> API
|
v
Pinia Store (writes list + pagination)
^
|
Other Components (read store state)See Pinia Store Patterns for conventions and examples.
2. Shared Composable State (app/composables/)
Some composables declare ref values outside the function body, at the module level. Because ES modules are singletons, every caller of the composable shares the same reactive state. This is the right pattern for app-wide UI state that does not come from the API:
useDialog-- modal dialog visibility and componentuseSnackBar-- toast notification message and typeuseConfirmDialog-- confirmation prompt with promise-based resolution
These composables are lightweight alternatives to a Pinia store when you need shared state but no devtools integration or persistence.
See Composable-Based State for the full pattern and examples.
3. Local Component State
State that only one component uses belongs inside that component's <script setup>. This includes:
- Form field values (
reactive<Partial<Doctor>>({...})) - UI toggles (
ref(false)for showing/hiding a section) - Temporary loading and error states consumed inline
<script setup lang="ts">
const showFilters = ref(false);
const form = reactive<Partial<Doctor>>({
first_name: '',
last_name: '',
status: 'active',
});
</script>Do not promote local state to a store unless a second component actually needs to read or write it.
Data Flow Architecture
The overall data flow in CPR follows a unidirectional pattern:
┌──────────────┐ ┌───────────────────┐ ┌──────────────────┐
│ Component │────>│ Domain Composable │────>│ Service Class │
│ (template) │ │ (useDoctor) │ │ (doctorService) │
└──────┬───────┘ └────────┬──────────┘ └────────┬─────────┘
│ reads │ writes │ fetches
v v v
┌──────────────┐ ┌───────────────────┐ ┌──────────────────┐
│ Pinia Store │<────│ Store update │ │ Backend API │
│ (list, page) │ │ (list = data) │ │ /api/doctors │
└──────────────┘ └───────────────────┘ └──────────────────┘- Component calls a domain composable function (e.g.,
getDoctors()). - Domain composable calls the service class, manages
loading/error/successrefs. - Service class performs the HTTP request via
useApiFetch. - On success, the composable writes the result into the Pinia store.
- Components reactively read from the store -- they never write to it directly.
When to Reach for Each Tool
| Scenario | Use |
|---|---|
Patient list displayed on /patients page | usePatientStore (Pinia) |
| Showing a create-doctor modal from a button anywhere | useDialog (shared composable) |
| Form fields for creating a new doctor | reactive({...}) in the component |
| Current authenticated user and token | useAuthStore (Pinia + sessionStorage) |
| Toast after saving a record | useSnackBar (shared composable) |
| "Are you sure?" before deleting | useConfirmDialog (shared composable) |
| Theme preference (dark/light) | useSettingsStore (Pinia + localStorage) |
| Dropdown enum options (doctor roles, statuses) | useEnumsStore (Pinia + API fetch) |
| Loading state for a single API call | ref(false) in a domain composable |
| Search/filter values for a list page | useListFilters composable (local to page) |
Related Pages
- Pinia Store Patterns -- store conventions, naming, and examples
- Composable-Based State -- shared state composables and domain composables
- API Integration -- service layer and data fetching
- Coding Standards -- reactivity rules and error handling
