Alpine.js Magic Properties and Store — Complete Guide
In this tutorial, you'll learn about Alpine.js magic properties and global store. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
Alpine.js magic properties are special variables prefixed with $ that give you access to store data, DOM elements, component state, and lifecycle utilities for advanced reactivity patterns.
What You'll Learn
By the end of this tutorial, you'll use $store for global state, $watch to observe changes, $dispatch for custom events, $nextTick for DOM timing, and $refs for element access.
Why It Matters
As your Alpine application grows, components need to communicate, share state, and coordinate timing. Magic properties provide the infrastructure for these advanced patterns without leaving the Alpine ecosystem.
Real-World Use
Doda Browser's multi-tab interface uses $store to share the current user session across all components. When the user logs out in one component, $store.user updates everywhere instantly via reactivity.
Where This Fits in Your Learning Path
flowchart LR
A["x-effect Directive"] --> B["**Magics & Store**"]
B --> C["Alpine Plugins"]
C --> D["Alpine Project"]
D --> E["Real Alpine Apps"]
style B fill:#f97316,stroke:#c2410c,color:#fff
style A fill:#e5e7eb,stroke:#9ca3af,color:#374151
style E fill:#22c55e,stroke:#16a34a,color:#fff
$store — Global Shared State
$store provides access to globally registered stores. All components sharing the same store see the same data.
<script>
document.addEventListener('alpine:init', () => {
Alpine.store('cart', {
items: [],
total: 0,
addItem(item) {
this.items.push(item)
this.total += item.price
}
})
})
</script>
<div x-data>
<button @click="$store.cart.addItem({ name: 'Widget', price: 9.99 })">Add Widget</button>
<p>Items: <span x-text="$store.cart.items.length"></span></p>
<p>Total: $<span x-text="$store.cart.total.toFixed(2)"></span></p>
</div>
Expected output: Clicking the button adds an item and updates both the count and total across all components using the store.
$watch — Observing Changes
$watch observes a specific property and runs a callback when it changes.
<div x-data="{ search: '' }"
x-init="$watch('search', (value, oldValue) => {
console.log(`Search changed from '${oldValue}' to '${value}'`)
})">
<input x-model="search" placeholder="Type to search">
</div>
Expected output: The console logs every change to the search property with old and new values.
$dispatch — Custom Events
$dispatch fires a custom DOM event that bubbles up to parent components.
<div x-data="{ notifications: [] }"
@notify="notifications.push($event.detail.message)">
<div x-data="{ count: 0 }">
<button @click="count++; $dispatch('notify', { message: `Count is ${count}` })">
Click <span x-text="count"></span> times
</button>
</div>
<ul>
<template x-for="(msg, i) in notifications" :key="i">
<li x-text="msg"></li>
</template>
</ul>
</div>
Expected output: Each click dispatches a custom 'notify' event that the parent catches and adds to the notification list.
$nextTick — Waiting for DOM Updates
$nextTick runs a callback after Alpine has updated the DOM, useful for focusing inputs or measuring elements after state changes.
<div x-data="{ editing: false }">
<template x-if="editing">
<input x-ref="editInput" type="text"
x-init="$nextTick(() => $refs.editInput.focus())"
@keydown.escape="editing = false">
</template>
<button @click="editing = true" x-show="!editing">Edit</button>
</div>
Expected output: Clicking Edit shows the input and immediately focuses it.
$el — Root Element Reference
$el gives you the root DOM element of the current component.
<div x-data x-init="console.log('Root element:', $el)">
<p>Check the console for the root element reference.</p>
</div>
Expected output: The console shows the div element itself.
Common Mistakes
1. Trying to use $store before it's registered
Always register stores inside the alpine:init event listener. Accessing $store before registration returns undefined.
2. Using $watch on computed properties
$watch only observes data properties, not computed values. Use x-effect for reactive side effects on computed data.
3. Forgetting that $dispatch events bubble up
Events dispatched via $dispatch bubble up the DOM tree. Child components cannot catch events dispatched by parents.
4. Using $nextTick unnecessarily
Most Alpine expressions update automatically. Only use $nextTick when you need the DOM to be in its final state.
5. Mutating $store data without reactivity
Always use methods or direct property assignment to update store data. Replacing the entire store object breaks reactivity.
Practice Questions
What is the $store magic property? It provides access to globally shared state registered via Alpine.store(). All components share the same store instance.
How does $dispatch work? It fires a custom event on the current element that bubbles up to parent components. The second argument is the event detail.
When would you use $nextTick? When you need the DOM to reflect the latest Alpine updates, such as after changing state that affects rendering.
What is the difference between $watch and x-effect? $watch observes a specific named property. x-effect automatically tracks all reactive dependencies used in its expression.
Can you access $el outside of Alpine expressions? No. $el is only available in Alpine expressions like x-init, x-effect, and event handlers.
Challenge
Build a shopping cart system using $store. Include add-to-cart buttons in multiple components, a cart summary component showing total items and price, and a checkout button that dispatches a custom event.
FAQ
Mini Project
Build a collaborative todo app using $store. Multiple components share the same todo list. One component adds todos, another displays them with completion toggles, and a third shows statistics (total, completed, pending).
<script>
document.addEventListener('alpine:init', () => {
Alpine.store('todos', {
items: [],
add(text) { this.items.push({ id: Date.now(), text, done: false }) },
toggle(id) { const t = this.items.find(i => i.id === id); if (t) t.done = !t.done },
remove(id) { this.items = this.items.filter(i => i.id !== id) },
get completed() { return this.items.filter(i => i.done).length },
get pending() { return this.items.filter(i => !i.done).length }
})
})
</script>
<div x-data="{ newTodo: '' }" class="p-4 space-y-4">
<form @submit.prevent="$store.todos.add(newTodo); newTodo = ''">
<input x-model="newTodo" required placeholder="Add todo" class="w-full p-2 border rounded">
</form>
<template x-for="todo in $store.todos.items" :key="todo.id">
<div class="flex items-center gap-2 p-2 bg-gray-50 rounded">
<input type="checkbox" :checked="todo.done" @click="$store.todos.toggle(todo.id)">
<span x-text="todo.text" :class="todo.done ? 'line-through' : ''"></span>
<button @click="$store.todos.remove(todo.id)" class="ml-auto text-red-500">Delete</button>
</div>
</template>
<div class="text-sm text-gray-500">
<span x-text="`${$store.todos.pending} pending of ${$store.todos.items.length} total`"></span>
</div>
</div>
What's Next
Continue with plugins and real-world projects:
| Tutorial | What You'll Learn |
|---|---|
| Alpine Plugins | Extend Alpine with plugins like Mask, Persist, and Focus |
| Alpine Project | Build a complete real-world application with Alpine |
Related topics: JavaScript event system, global state management patterns.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro