Replies: 3 comments 4 replies
-
|
Disclaimer: this is based from my insights based on the concerns and personal experiences as well. 1. When to actually switch frameworksThe real trigger isn't lines of code or file count — it's whether the app needs derived/computed state that depends on multiple sources, or async state coordination (loading/error/success across many components). Alpine's reactivity (via Concrete signal: if custom event buses are needed to keep 3+ components in sync on the same piece of state, that's the line. It's not "Alpine is too small" — it's "the state graph now has real topology" that needs a proper store. 2. Pattern for complex state in AlpineSkip scattered // stores/app.js
export const appStore = {
user: null,
notifications: [],
sidebarOpen: false,
init() {
// runs once when Alpine.store registers it
},
toggleSidebar() {
this.sidebarOpen = !this.sidebarOpen
},
get unreadCount() {
return this.notifications.filter(n => !n.read).length
}
}import { appStore } from './stores/app.js'
document.addEventListener('alpine:init', () => {
Alpine.store('app', appStore)
})Every component then references For cross-cutting concerns (auth, toasts, modals), use one store per concern rather than one giant store — it stays easier to navigate as the app grows. 3. Code splittingVite plus "no build step" is a bit of a contradiction — real code splitting requires Vite to bundle the Alpine entry point. Alpine doesn't require a build step, but it doesn't forbid one either. Pattern: // main.js
import Alpine from 'alpinejs'
window.Alpine = Alpine
Alpine.data('heavyComponent', () =>
import('./components/heavyComponent.js').then(m => m.default())
)
Alpine.start()Alternatively, use dynamic 4. State management librariesNothing mature and widely adopted beats hand-rolling the store-module pattern above — the ecosystem is intentionally thin because Alpine's whole pitch is "you shouldn't need this." Closest options:
5. TestingAlpine components are hard to unit test as directives, but the underlying data functions are just plain objects/functions — those can be tested in isolation with Vitest, without touching the DOM: import { describe, it, expect } from 'vitest'
import { appStore } from './stores/app.js'
it('toggles sidebar', () => {
const store = { ...appStore }
store.toggleSidebar()
expect(store.sidebarOpen).toBe(true)
})For actual DOM/directive behavior, Playwright or Cypress component tests are the realistic option — Alpine doesn't have a testing-library equivalent because its reactivity is deliberately DOM-coupled. The honest answer to "when do I switch"If the SPA sits inside a server-rendered app (e.g. a TALL-stack style setup with Livewire), it's worth checking whether moving state ownership server-side removes the problem entirely, letting Alpine handle only local UI micro-interactions. A lot of "Alpine can't share state" pain disappears once the source of truth lives on the server. If it's a standalone SPA with no server framework driving it, then yes — once a custom store/devtools/testing layer is being built just to make Alpine behave like a real framework, that's roughly 60% of Pinia already hand-rolled. At that point, Vue + Pinia is the more natural next step from Alpine's mental model than React — less of a rewrite, same template-first philosophy. |
Beta Was this translation helpful? Give feedback.
-
|
Well, your problems aren't really true.
There is Alpine.store
How so? Nothing really rerenders if it doesn't depend on state that changes. Nothing else would cause a "rerender" (more like "reevaluation" but yeah).
True, I did make an alpine test library that works with vitest and its...fine
there is @types/alpinejs
That has nothing to do with using Alpine. You can split your code however you want. |
Beta Was this translation helpful? Give feedback.
-
|
@geloquan This is exactly the kind of practical guidance I was looking for! The store-module pattern is clean: export const appStore = {
user: null,
notifications: [],
sidebarOpen: false,
toggleSidebar() { this.sidebarOpen = !this.sidebarOpen },
get unreadCount() { return this.notifications.filter(n => !n.read).length }
}Two follow-up questions:
// Option A: Import stores directly
import { notificationStore } from './notifications.js'
import { cartStore } from './cart.js'
export const authStore = {
logout() {
this.user = null
notificationStore.clear()
cartStore.clear()
}
}
// Option B: Use Alpine events
export const authStore = {
logout() {
this.user = null
Alpine.store('notifications').clear()
Alpine.store('cart').clear()
}
}Which is more maintainable as the app grows?
@ekwoka Fair points! You are right that Alpine.store exists and code splitting is not Alpine-specific. My concern was more about the DX when you have 20+ components all using different stores - the lack of type inference and autocompletion makes it harder to refactor safely. Have you found any good patterns for improving the TypeScript experience with Alpine stores? |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
Context
I am building a medium-complexity SPA with Alpine.js. As the app grows, I am hitting limitations with state management.
Current architecture
Problems
What I tried
Questions
Environment
I want to stay with Alpine if possible, but I need better state management patterns.
Beta Was this translation helpful? Give feedback.
All reactions