Alpine.js state scope: x-data, store, or persist
Choose Alpine.js state scope: local x-data, shared Alpine.store, or persisted Alpine.persist for reload-safe UI preferences.
29 Sept 2025, 14:01 UTC

Decision: put the state where it belongs
You are building an Alpine.js page and need a counter, theme toggle, or cart value. The wrong scope makes state disappear on reload, leak between unrelated component instances, or create hidden dependencies that are hard to trace.
Useful takeaway: choose x-data for state owned by one component instance, Alpine.store for reactive state shared across components, and Alpine.persist only when the value must survive a page reload and does not contain secrets.
Assumptions and constraints
- Alpine.js v3.x, loaded by CDN script or bundler.
- The official Persist plugin, if used, must be registered with
Alpine.plugin(persist)beforeAlpine.start(). - State lives in the browser; these examples do not involve server round-trips.
Supported options compared
| Option | Scope | Survives reload | Shared across components | Best for |
|---|---|---|---|---|
x-data | One component instance | No | No | Local UI state such as open/closed dropdowns, per-instance counters, form drafts |
Alpine.store | Named global singleton | No, unless persisted separately | Yes | Cross-component UI state such as theme, cart contents, modal coordination |
Alpine.persist | Global store synced to localStorage or sessionStorage | Yes, within the same origin and storage quota | Yes, through the underlying store | User preferences that should return after reload, such as theme or collapsed sidebar |
Trade-offs
Local state with x-data
x-data initializes per component instance. Two elements with x-data='{ count: 0 }' get independent counters. Use a function that returns a fresh object for reusable component blueprints; do not pass the same external object reference to multiple instances unless sharing is intended.
Limitation: unrelated components cannot read it, and the state is lost when the element is removed or the page reloads.
Global state with Alpine.store
A store is a singleton. Mutating $store.theme.value in one component immediately updates every component reading that store. That is convenient for cross-component reactivity, but it creates hidden dependencies: a template change in one file can affect behavior in another without an obvious prop flow.
Limitation: stores do not survive reloads by default.
Persistent state with Alpine.persist
The Persist plugin syncs a named store to localStorage or sessionStorage and restores it on load. It is appropriate for preferences, not for tokens or PII, because any script on the page can read localStorage. Private browsing modes and storage quotas can restrict or block persistence.
Limitation: plugin registration order matters, and exact helper names vary by plugin version; verify against the installed README before production.
Concrete implementation: isolated counters with x-data
Where to run: any HTML page that loads Alpine.js v3. Permissions: none. Placeholder: replace alpinejs@3.x.x with a pinned v3 release.
<script defer src='https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js'></script>
<div x-data='{ count: 0 }'>
<button @click='count++'>Increment A</button>
<span x-text='count'></span>
</div>
<div x-data='{ count: 0 }'>
<button @click='count++'>Increment B</button>
<span x-text='count'></span>
</div>
Expected check: click Increment A; only counter A changes. Counter B stays at zero. Risk: if you extract { count: 0 } to a shared variable and reference it in both x-data attributes, the counters can share state instead of staying independent.
Concrete implementation: shared theme with Alpine.store
Where to run: module script in an Alpine v3 project. Permissions: none. Expected check: toggling the theme in one component updates the other without reloading.
<script type='module'>
import Alpine from 'alpinejs'
Alpine.store('theme', {
value: 'light',
toggle() {
this.value = this.value === 'light' ? 'dark' : 'light'
}
})
Alpine.start()
</script>
<div x-data='{}'>
<button @click='$store.theme.toggle()'>Toggle theme</button>
<span x-text='$store.theme.value'></span>
</div>
<div x-data='{}'>
<p x-text='`Theme: ${$store.theme.value}`'></p>
</div>
Note: x-data='{}' is used only so Alpine initializes the component; the store is owned globally, not by the component.
Adding persistence for reload survival
Where to run: terminal in the project root. Permissions: write access to package.json and node_modules. Command: npm install alpinejs @alpinejs/persist. Expected check: npm ls alpinejs @alpinejs/persist lists both packages. Risk: registry access or a major-version change can break the plugin API.
Register the plugin before Alpine.start(), define the named store, then mark it for persistence according to the installed plugin's README. After a theme toggle, reload the page and open DevTools > Application > Local Storage. Confirm the persisted key exists and the theme value returns after reload. If private mode blocks storage, expect errors or silent failure; check the Console and the Storage tab.
Cleanup: to remove test data, open Console and run localStorage.removeItem('your-persist-key'), replacing the placeholder with the actual key. This removes the persisted preference for that origin.
Diagnostic decision
- Does only one component instance need the value? Use
x-data. - Do multiple unrelated components need to react to the same value? Use
Alpine.store. - Must the value survive reload? Add
Alpine.persist, avoid sensitive data, and verify in DevTools.
Limitation
Global stores and persisted state make data flow less explicit. In large templates, prefer a single store file or events so the state graph remains traceable. There is no server state to roll back in these examples; clearing localStorage during testing is the only destructive cleanup.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.