Back to Examples

alexop.dev

Master state management in Vue with Pinia! Learn to apply The Elm Architecture for better store structure and testability.

Lines
14,066
Sections
427

Want your own llms.txt file?

Generate a professional, AI-friendly file for your website in minutes!

llms.txt Preview

--- title: How to Write Better Pinia Stores with the Elm Pattern description: Learn how to combine The Elm Architecture (TEA) principles with Pinia's private store pattern for testable, framework-agnostic state management in Vue applications. tags: ['vue'] ---

# How to Write Better Pinia Stores with the Elm Pattern



## The Problem: Pinia Gives You Freedom, Not Rules

Pinia is a fantastic state management library for Vue, but it doesn't enforce any architectural patterns. It gives you complete freedom to structure your stores however you want. This flexibility is powerful, but it comes with a hidden cost: without discipline, your stores can become unpredictable and hard to test.

The core issue? Pinia stores are inherently mutable and framework-coupled. While this makes them convenient for rapid development, it creates three problems:

```ts
// Traditional Pinia approach - tightly coupled to Vue
export const useTodosStore = defineStore('todos', () => {
  const todos = ref<Todo[]>([])

  function addTodo(text: string) {
    todos.value.push({ id: Date.now(), text, done: false })
  }

  return { todos, addTodo }
})
```

The problem? Components can bypass your API and directly manipulate state:

```vue
<script setup lang="ts">
const store = useTodosStore()

// Intended way
store.addTodo('Learn Pinia')

// But this also works! Direct state manipulation
store.todos.push({ id: 999, text: 'Hack the state', done: false })
</script>
```

This leads to unpredictable state changes, makes testing difficult (requires mocking Pinia's entire runtime), and couples your business logic tightly to Vue's reactivity system.

```mermaid
graph TB
    C1[Component A] -->|"store.addTodo() ✓"| API[Intended API]
    C2[Component B] -->|"store.todos.push() ✗"| State[Direct State Access]
    C3[Component C] -->|"store.todos[0].done = true ✗"| State
    API --> Store[Store State]
    State --> Store
    Store -->|unpredictable changes| Debug[Difficult to Debug]
```

## The Solution: TEA + Private Store Pattern

What if we could keep Pinia's excellent developer experience while adding the predictability and testability of functional patterns? Enter The Elm Architecture (TEA) combined with the "private store" technique from [Mastering Pinia](https://masteringpinia.com/blog/how-to-create-private-state-in-stores) by Eduardo San Martin Morote (creator of Pinia).

This hybrid approach gives you:
- **Pure, testable business logic** that's framework-agnostic
- **Controlled state mutations** through a single dispatch function
- **Seamless Vue integration** with Pinia's reactivity
- **Full devtools support** for debugging

You'll use a private internal store for mutable state, expose only selectors and a dispatch function publicly, and keep your update logic pure and framework-agnostic.

<Aside type="tip" title="When Should You Use This Pattern?">
This pattern shines when you have complex business logic, need framework portability, or want rock-solid testing. For simple CRUD operations with minimal logic, traditional Pinia stores are perfectly fine. Ask yourself: "Would I benefit from testing this logic in complete isolation?" If yes, this pattern is worth it.
</Aside>

<Aside type="info" title="Historical Context">
The Elm Architecture emerged from the [Elm programming language](https://guide.elm-lang.org/architecture/), which pioneered a purely functional approach to building web applications. This pattern later inspired Redux's architecture in the JavaScript ecosystem, demonstrating the value of unidirectional data flow and immutable updates. While Elm enforces these patterns through its type system, we can achieve similar benefits in Vue with disciplined patterns.
</Aside>

## Understanding The Elm Architecture

Before we dive into the implementation, let's understand the core concepts of TEA:

1. **Model**: The state of your application
2. **Update**: Pure functions that transform state based on messages/actions
3. **View**: Rendering UI based on the current model

```mermaid
graph LR
    M[Model<br/>Current State] -->|renders| V[View<br/>UI Display]
    V -->|user interaction<br/>produces| Msg[Message/Action]
    Msg -->|dispatched to| U[Update Function<br/>Pure Logic]
    U -->|returns new| M

```

The key insight is that update functions are pure—given the same state and action, they always return the same new state. This makes them trivial to test without any framework dependencies.

## How It Works: Combining TEA with Private State

The pattern uses three key pieces: a private internal store for mutable state, pure update functions for business logic, and a public store that exposes only selectors and dispatch.

### The Private Internal Store

First, create a private store that holds the mutable model. This stays in the same file as your public store but is not exported:

```ts
// Inside stores/todos.ts - NOT exported!
Preview of alexop.dev's llms.txt file. View complete file (14,066 lines) →

Ready to create yours?

Generate a professional llms.txt file for your website in minutes with our AI-powered tool.

Generate Your llms.txt File