Pinia
Pronounced like the fruit in Spanish, Piña
Piña is also an invalid package name... that's why it has to be pinia which sounds very similar
🍍Automatically Typed, Modular and lightweight (but Experimental) Store for Vue 3.x based on the composition api with devtools support
⚠️⚠️⚠️ This project is experimental, it's an exploration of what a Store could be like using the composition api. It works both for Vue 2.x and Vue 3.x and you are currently on the branch that supports Vue 3.x. Go here for the Vue 2.x compatible version.
What I want is to inspire others to think about ways to improve Vuex and come up with something that works very well with the composition api. Ideally it could also be used without it.
There are the core principles that I try to achieve with this experiment:
- Flat modular structure 🍍 No nesting, only stores, compose them as needed
- Light layer on top of Vue 💨 keep it very lightweight
- Only
state
, getters
👐 patch
is the new mutation - Actions are like methods ⚗️ Group your business there
- Import what you need, let webpack code split 📦 No need for dynamically registered modules
- SSR support ⚙️
- DevTools support 💻 Which is crucial to make this enjoyable
Help me keep working on Open Source in a sustainable way 🚀. Help me with as little as $1 a month, sponsor me on Github.
FAQ
A few notes about the project and possible questions:
Q: Does this replace Vuex, is it its successor?
A: No, or at least that's not the main intention
Q: What about dynamic modules?
A: Dynamic modules are not type safe, so instead we allow creating different stores that can be imported anywhere
Roadmap / Ideas
Installation
yarn add pinia@next
npm install pinia@next
Usage
Creating a Store
You can create as many stores as you want, and they should each exist in different files:
import { createStore } from 'pinia'
export const useMainStore = createStore({
id: 'main',
state: () => ({
counter: 0,
name: 'Eduardo',
}),
getters: {
doubleCount: (state, getters) => state.counter * 2,
doubleCountPlusOne: (state, { doubleCount }) => doubleCount.value * 2,
},
actions: {
reset() {
this.state.counter = 0
},
},
})
createStore
returns a function that has to be called to get access to the store:
import { useMainStore } from '@/stores/main'
export default defineComponent({
setup() {
const main = useMainStore()
return {
main,
state: main.state,
}
},
})
Note: the SSR implementation is yet to be decided on Pinia, but if you intend having SSR on your application, you should avoid using useStore
functions at the root level of a file to make sure the correct store is retrieved for your request.
Once you have access to the store, you can access the state
through store.state
and any getter directly on the store
itself as a computed property (meaning you need to use .value
to read the actual value on the JavaScript but not in the template):
export default defineComponent({
setup() {
const main = useMainStore()
const text = main.state.name
const doubleCount = main.doubleCount.value
return {}
},
})
state
is the result of a ref
while every getter is the result of a computed
.
Actions are invoked like methods:
export default defineComponent({
setup() {
const main = useMainStore()
main.reset()
return {}
},
})
Mutating the state
To mutate the state you can either directly change something:
main.state.counter++
or call the method patch
that allows you apply multiple changes at the same time with a partial state
object:
main.patch({
counter: -1,
name: 'Abalam',
})
The main difference here is that patch
allows you to group multiple changes into one single entry in the devtools (which are not yet available for Vue 3.x).
Replacing the state
Simply set it to a new object;
main.state = { counter: 666, name: 'Paimon' }
SSR
To be decided once SSR is implemented on Vue 3
Composing Stores
Composing stores may look hard at first glance but there is only one rule to follow really:
If multiple stores use each other or you need to use multiple stores at the same time, you must create a separate file where you import all of them.
If one store uses an other store, there is no need to create a new file, you can directly import it. Think of it as nesting.
Shared Getters
If you need to compute a value based on the state
and/or getters
of multiple stores, you may be able to import all the stores but one into the remaining store, but depending on how your stores are used across your application, this would hurt your code splitting because importing the store that imports all others stores, would result in one single big chunk with all of your stores.
To prevent this, we follow the rule above and we create a new file with a new store:
import { createStore } from 'pinia'
import { useUserStore } from './user'
import { useCartStore } from './cart'
export const useSharedStore = createStore({
id: 'shared',
getters: {
summary() {
const user = useUserStore()
const cart = useCartStore()
return `Hi ${user.state.name}, you have ${cart.state.list.length} items in your cart. It costs ${cart.price}.`
},
},
})
Shared Actions
When an actions needs to use multiple stores, we do the same, we create a new file with a new store:
import { createStore } from 'pinia'
import { useUserStore } from './user'
import { useCartStore } from './cart'
export const useSharedStore = createStore({
id: 'shared',
state: () => ({}),
actions: {
async orderCart() {
const user = useUserStore()
const cart = useCartStore()
try {
await apiOrderCart(user.state.token, cart.state.items)
cart.emptyCart()
} catch (err) {
displayError(err)
}
},
},
})
Creating Pinias
Not implemented. Still under discussion, needs more feedback as this doesn't seem necessary because it can be replaced by shared stores as shown above.
Combine multiple stores (gajos) into a new one:
import { pinia } from 'pinia'
import { useUserStore } from './user'
import { useCartStore, emptyCart } from './cart'
export const useCartUserStore = pinia(
{
user: useUserStore,
cart: useCartStore,
},
{
getters: {
combinedGetter: ({ user, cart }) =>
`Hi ${user.state.name}, you have ${cart.state.list.length} items in your cart. It costs ${cart.price}.`,
},
actions: {
async orderCart() {
try {
await apiOrderCart(this.user.state.token, this.cart.state.items)
this.cart.emptyCart()
} catch (err) {
displayError(err)
}
},
},
}
)
Related
License
MIT