Pinia Composition
A composition API for Pinia
Motivation
Pinia's defineStore
API makes you organize code based on whether it defines state, getters, or actions. You can see this clearly in an example from the docs:
import { defineStore } from 'pinia'
export const useTodos = defineStore({
id: 'todos',
// All state is organized here, regardless of its purpose.
state: () => ({
todos: [],
filter: 'all',
nextId: 0,
}),
// Getters go here, not always close to the state they read.
getters: {
finishedTodos(state) {
return state.todos.filter((todo) => todo.isFinished)
},
unfinishedTodos(state) {
return state.todos.filter((todo) => !todo.isFinished)
},
filteredTodos(state) {
if (this.filter === 'finished') {
return this.finishedTodos
} else if (this.filter === 'unfinished') {
return this.unfinishedTodos
}
return this.todos
},
},
// Actions also have their own space, often far away from
// the state they read and write.
actions: {
addTodo(text) {
this.todos.push({ text, id: this.nextId++, isFinished: false })
},
},
})
In many cases, though, it's useful to organize your code based on logical concern. Got a few actions that mutates a certain piece of state? Put all that code right underneath the line that defines the state with its initial value. Then, anyone reading your code can more easily get a bird's eye view of all the different ways your state could change during user interactions.
Solution
In the spirit of Vue 3, I hacked up a composition API for Pinia.
Instead of calling defineStore
to set up your store, call the createPiniaComposition
function, passing your new store's id
. The function returns some useful hooks for setting up your store.
Use those hooks to add new state, getters, and actions to your store's definition. Feel free to organize code by logical concern!
const {
useState,
useGetter,
useAction,
useStore: useTodos,
} = createPiniaComposition('todos')
/* ~~~ */
useState('todos', [])
useState('nextId', 0)
useAction(
'addTodo',
store =>
text => {
store.todos.push({ text, id: store.nextId++, isFinished: false })
}
)
/* ~~~ */
useGetter(
'finishedTodos',
state => state.todos.filter((todo) => todo.isFinished)
)
useAction(
'finishTodo',
store =>
findTodo => store.todos.find(findTodo).isFinished = true
)
/* ~~~ */
useGetter(
'unfinishedTodos',
state => state.todos.filter((todo) => !todo.isFinished)
)
useAction(
'unfinishTodo',
store =>
findTodo => store.todos.find(findTodo).isFinished = false
)
Then, inside your Vue component's setup
function, call the useStore
hook to actually create an instance of your configured store:
<script setup>
import { ref } from 'vue'
const store = useStore()
const newTodoText = ref('')
function handleButtonClick () {
store.addTodo(newTodoText.value)
}
</script>
Try it out
Download and install:
npx degit alexvipond/pinia-composition && npm install && npm run dev
This is a Vite app that will be running on http://localhost:3000
. Open up Vue DevTools to see the Pinia store react in real time as you play with the UI.
Caveats
This is not a serious implementation of what a Pinia composition API should be. It's got some big problems!
- TypeScript support is 100% broken
- Support for
actions
works entirely on the$onAction
API, which might change in the future - It's not efficient—the store actually gets fully redefined, not just created, every single time you call the
useStore
hook in anysetup
function.
This is just an exploration of the idea, and my thoughts on what a composition-based DX might look like.