Founder of Vue School
Author of The Majesty of Vue.js
Vue.js Contributor
Enterprise Consultant
#1 Training Platform for Vue.js
750+ Video Lessons
140,000 users
Alex Kyriakidis
Daniel Kelly
Debbie O'Brien
Maria Lamardo
Roman Kuba
Filip Rakowski
Lydia Hallie
Rolf Haug
NEW
πΊ
Vue Video
Courses
π¨βπ«
Live Workshops
π¬
Bi-weekly Support
Sessions
π§βπ§
Database for hiring
Vue devs
π¨βπ« Instruction
π¬ Questions
π©βπ» Hands-on Exercises
(10 - 20 mins)
(0 - 10 mins)
(15 - 30 mins)
πΊ Solution
(5 - 10 mins)
Master Pinia
π Have fun
Teacher @ Vue School
Full Stack developer (10 years)
Husband and Father
https://vuejs.org/guide/scaling-up/state-management.html#pinia
<script setup>
import {ref} from "vue"
const count = ref(0)
</script>
<script>
export default {
data(){
return {
count: 0
}
}
}
</script>
Composition API
Options API
Props
Events
Β
Β
Global State
Local Data
Examples of local data
<!-- LoginForm.vue -->
<script setup>
import { reactive } from "vue";
const form = reactive({
username: "",
password: ""
})
</script>
<template>
<form @submit="$emit('login', form)">
<label>
Username
<input v-model="form.username">
</label>
<label>
Password
<input v-model="form.password">
</label>
</form>
</template>
Examples of local data
<!--TwitterFeed.vue-->
<script setup>
import { ref } from "vue";
const loading = ref(false);
function loadTweets(){
loading.value = true;
// load the data...
loading.value = false;
}
//...
</script>
<template>
<!-- tweets...-->
<AppSpinner v-if="loading"/>
</template>
Examples of global state
<!-- AppHeader.vue -->
<template>
<!--...-->
<a class="/me">
{{ user.name }}
</a>
</template>
<!-- MyPosts.vue -->
<script>
const fetchPosts = ()=>{
fetch(`https://myendpoint.com/user-posts/${user.id}`)
}
//...
</script>
<!-- Profile.vue -->
<!-- ProfileEditor.vue -->
<!-- AppFooter.vue -->
<!-- etc -->
Examples of global state
<!-- PostHero.vue -->
<template>
<!--...-->
<div class="hero">
{{ post.title }}
</div>
</template>
<!-- PostShow.vue -->
<script>
const fetchPost = ()=>{
fetch(`https://myendpoint.com/posts/${post.id}`)
}
//...
</script>
<!-- PostByLine.vue -->
<!-- PostComments.vue -->
<!-- PostBody.vue -->
<!-- etc -->
Pinia provides several advantages:
https://vueschool.io/articles/vuejs-tutorials/how-to-migrate-from-vuex-to-pinia/
npm init vue@3
for a new project
npm install pinia
// main.js
import { createApp } from "vue";
import App from "./App.vue";
import { createPinia } from "pinia";
createApp(App)
.use(createPinia())
.mount("#app");
in an existing project
Pinia is modular by default
Pinia is modular by default
Pinia is modular by default
Store
Store
Store
// stores/PostStore.js
import { defineStore } from 'pinia'
// stores/PostStore.js
import { defineStore } from 'pinia'
defineStore('PostStore')
// stores/PostStore.js
import { defineStore } from 'pinia'
defineStore('PostStore', {
// state
// actions
// getters
})
second argument is store options object
// stores/PostStore.js
import { defineStore } from 'pinia'
defineStore('PostStore', () => {
return {
// data
// functions
// etc
}
})
second argument can also be a function similar to component setup()
// stores/PostStore.js
import { defineStore } from 'pinia'
export const usePostStore = defineStore('PostStore', {
// state
// actions
// getters
})
// stores/PostStore.js
import { defineStore } from 'pinia'
export const usePostStore = defineStore('PostStore', {
// state
// actions
// getters
})
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(usePostStore, import.meta.hot));
}
support HMR but must provide a snippet
// App.vue
<script setup>
import { usePostStore } from "@/stores/PostStore"
const postStore = usePostStore();
</script>
use the store by importing the use function and calling it
Let's take a look at:
Goal: Practice installing Pinia, defining stores, and using stores in components
// stores/ProductStore.ts
import { defineStore } from "pinia";
export const useProductStore = defineStore("ProductStore", {
state: () => {
return {};
},
});
Must be a function
// stores/ProductStore.ts
import { defineStore } from "pinia";
export const useProductStore = defineStore("ProductStore", {
state: () => {
return {
name: "The Pineapple Stand",
products: [
"Plain Ol' Pineapple",
"Dried Pineapple",
"Pineapple Gum",
"Pineapple T-Shirt",
],
isAwesome: true,
};
},
});
// stores/App.vue
<script setup>
import { useProductStore } from "./stores/ProductStore";
const productStore = useProductStore();
console.log(productStore.name) // "The Pineapple Stand"
</script>
...
State available as property on store
State is TypeSafe
// stores/App.vue
<script setup>
import { useProductStore } from "./stores/ProductStore";
const { name } = useProductStore();
console.log(name) // "The Pineapple Stand"
</script>
...
Can de-structure state from store
// stores/App.vue
<script setup>
import { useProductStore } from "./stores/ProductStore";
import { storeToRefs } from "pinia";
const { name } = storeToRefs(useProductStore());
console.log(name.value) // "The Pineapple Stand"
</script>
...
When de-structuring convert store to refs to maintain reactivity
// stores/App.vue
<script setup>
import { useProductStore } from "./stores/ProductStore";
import { storeToRefs } from "pinia";
const { name } = storeToRefs(useProductStore());
name.value = "Hello new name";
</script>
State can be directly updated
// stores/App.vue
<script setup>
import { useProductStore } from "./stores/ProductStore";
import { storeToRefs } from "pinia";
const productStore = useProductStore();
productStore.name = "Hello new name";
</script>
No need for .value if you don't de-destructure
// stores/App.vue
<script setup>
import { useProductStore } from "./stores/ProductStore";
import { storeToRefs } from "pinia";
const { name } = storeToRefs(useProductStore());
</script>
<template>
<input v-model="name" type="text" />
</template>
Use with v-model is easy!
// stores/App.vue
<script setup>
import { useProductStore } from "./stores/ProductStore";
import { storeToRefs } from "pinia";
const { name: newName } = storeToRefs(useProductStore());
</script>
Can rename de-structured state
Useful feature you don't get if you roll your own stores with vanilla composition API.
Comes with some built in helpers
store.$reset()
// acccess the entire state or
// set it all at once
store.$state
https://tinyurl.com/38jpcarr
Goal: Practice defining and accessing state in a Pinia store
https://tinyurl.com/6h5d54uf
Live Slides
https://tinyurl.com/mtfzertx
Slides
import { defineStore } from "pinia";
import products from "@/data/products.json";
export const useProductStore = defineStore("ProductStore", {
state: //...
actions:{
fill(){}
}
});
Define actions as methods on the actions option
import { defineStore } from "pinia";
export const useProductStore = defineStore("ProductStore", {
state: ()=>{
return {
products:[],
}
},
actions:{
async fill(){
const res = await fetch('my_api_endpoint/products');
this.products = await res.json();
}
}
});
access state with `this`
Great for filling state with initial data
State is fully typed on `this` within actions
Call actions in components as store methods
<script setup>
const productStore = useProductStore();
productStore.fill();
</script>
Or de-structure it from the store
<script setup>
const { fill } = useProductStore();
fill();
</script>
You CANNOT get actions when using storeToRefs
<script setup>
const { fill } = storeToRefs(useProductStore());
fill(); // Error
</script>
β
won't work
import { defineStore } from "pinia";
export const useCartStore = defineStore("CartStore", {
state: () => {
return {
items: [],
};
},
actions: {
addItem(itemId, count) {
// set the count for the proper item in the state above
},
},
});
Useful for updating state based on user interaction
// App.vue
<button @click="addItem(product.id, $event)">Add Product to Cart</button>
Mutations grouped in the devtools
Alternate way of grouping mutations to state
productStore.$patch({
name: "Vue Conf US",
location: "Ft Lauderdale, FL",
});
Alternate way of grouping mutations to state
Can also use a callback function
cartStore.$patch((state) => {
state.items.push({ name: 'shoes', quantity: 1 })
state.hasChanged = true
})
Β
https://tinyurl.com/5xa26z9p
Goal: Practice defining and calling actions in a Pinia store
Β
equivalent of computed props on a component
// ProductStore.ts
import { defineStore } from "pinia";
import type { Product } from "@/types";
export const useProductStore = defineStore("ProductStore", {
state: () => {
return {
products: [] as Product[],
};
},
getters: {
count() {
return this.products.length;
},
},
actions: {
//...
},
});
access state with `this`
// ProductStore.ts
import { defineStore } from "pinia";
import type { Product } from "@/types";
export const useProductStore = defineStore("ProductStore", {
state: () => {
return {
products: [] as Product[],
};
},
getters: {
count(): number {
return this.products.length;
},
},
actions: {
//...
},
});
access state with `this`
// ProductStore.ts
import { defineStore } from "pinia";
import type { Product } from "@/types";
export const useProductStore = defineStore("ProductStore", {
state: () => {
return {
products: [] as Product[],
};
},
getters: {
count: (state) => {
return state.products.length;
},
},
actions: {
//...
},
});
access state with `state`
// ProductStore.ts
import { defineStore } from "pinia";
import type { Product } from "@/types";
export const useProductStore = defineStore("ProductStore", {
state: () => {
return {
products: [] as Product[],
};
},
getters: {
count: (state) => state.products.length,
},
actions: {
//...
},
});
encourages single
line arrow functions
// ProductStore.ts
import { defineStore } from "pinia";
import type { Product } from "@/types";
export const useProductStore = defineStore("ProductStore", {
state: () => {
return {
products: [] as Product[],
};
},
getters: {
count: (state) => state.products.length,
doubleCount(): number {
return this.count * 2;
},
},
actions: {
//...
},
});
access other
getters on `this`
// ProductStore.ts
import { defineStore } from "pinia";
import type { Product } from "@/types";
export const useProductStore = defineStore("ProductStore", {
state: () => {
return {
products: [] as Product[],
};
},
getters: {
count: (state) => state.products.length,
doubleCount: (state) => state.count * 2,
},
actions: {
//...
},
});
access other
getters on `this`
// ProductStore.ts
import { defineStore } from "pinia";
import type { Product } from "@/types";
export const useProductStore = defineStore("ProductStore", {
state: () => {
return {
products: [] as Product[],
};
},
getters: {
count: (state) => state.products.length,
doubleCount: (state) => state.count * 2,
productByName: (state) => (name) => {
return state.products.find((product) => product.name === name);
},
},
});
accept arguments
by returning a function
Dynamic Getters
// ProductStore.ts
import { defineStore } from "pinia";
import type { Product } from "@/types";
export const useProductStore = defineStore("ProductStore", {
state: () => {
return {
products: [] as Product[],
};
},
getters: {
count: (state) => state.products.length,
doubleCount: (state) => state.count * 2,
productByName(state) {
return function (name) {
return state.products.find((product) => product.name === name);
};
},
},
});
accept arguments
by returning a function
Dynamic Getters
// App.vue
<script setup>
const productStore = useProductStore();
console.log(productStore.count) // 4
</script>
Access getters as a property on the store
// App.vue
<script setup>
import { storeToRefs } from "pinia";
const { count } = storeToRefs(useProductStore());
console.log(count.value) // 4
</script>
Can de-structure getters from store but must use `storeToRefs`
https://tinyurl.com/2x6k5u9d
Goal: Practice defining and accessing Pinia getters
// CartStore.ts
import { defineStore } from "pinia";
import { useProductStore } from "./ProductStore";
export const useCartStore = defineStore("CartStore", {
// ...
getters: {
allProducts(){
const productStore = useProductStore();
return productStore.products
}
},
//...
});
Use state and getters from another store in a getter or an action
// CartStore.ts
import { defineStore } from "pinia";
import { useProductStore } from "./ProductStore";
export const useCartStore = defineStore("CartStore", {
//...
actions:{
reloadProducts(){
const productStore = useProductStore();
return productStore.fill()
}
}
//...
});
Use actions from another store in an action
// CartStore.ts
import { defineStore } from "pinia";
import { useProductStore } from "./ProductStore";
β const productStore = useProductStore();
export const useCartStore = defineStore("CartStore", {
//...
actions:{
reloadProducts(){
return productStore.fill()
}
}
//...
});
Use actions from another store in an action
// CartStore.ts
import { defineStore } from "pinia";
import { useProductStore } from "./ProductStore";
β const productStore = useProductStore();
export const useCartStore = defineStore("CartStore", {
//...
actions:{
reloadProducts(){
return productStore.fill()
}
}
//...
});
Use actions from another store in an action
https://tinyurl.com/5f33te88
Goal: Practice using stores in other stores
someStore.$onAction(
({
name, // name of the action
store, // store instance, same as `someStore`
args, // array of parameters passed to the action
after, // hook after the action returns or resolves
onError, // hook if the action throws or rejects
}) => {
console.log(name, store, args, after, onError);
}
);
Subscribe to actions
someStore.$onAction(
({
name, // name of the action
store, // store instance, same as `someStore`
args, // array of parameters passed to the action
after, // hook after the action returns or resolves
onError, // hook if the action throws or rejects
}) => {
if(name === 'fill'){
//... do the things here
}
}
);
Use conditional to run on select actions
someStore.$onAction(
({
name, // name of the action
store, // store instance, same as `someStore`
args, // array of parameters passed to the action
after, // hook after the action returns or resolves
onError, // hook if the action throws or rejects
}) => {
after( result =>{
console.log(`the action is complete and returned ${result}`)
})
onError(error =>{
console.log(`the action failed and the error thrown is ${error}`)
})
}
);
Example of after and onError
someStore.$onAction(
({
name, // name of the action
store, // store instance, same as `someStore`
args, // array of parameters passed to the action
after, // hook after the action returns or resolves
onError, // hook if the action throws or rejects
}) => {
after( result =>{
console.log(`the action is complete and returned ${result}`)
})
onError(error =>{
console.log(`the action failed and the error thrown is ${error}`)
})
}
);
Example of after and onError
someStore.$subscribe((mutation, state) => {
// 'direct' | 'patch object' | 'patch function'
mutation.type
// same as cartStore.$id
mutation.storeId
// only available with mutation.type === 'patch object'
mutation.payload // patch object passed to $patch()
})
Subscribing to the state
someStore.$subscribe((mutation, state) => {
// 'direct' | 'patch object' | 'patch function'
mutation.type
someStore.myState = "something else"
})
Subscribing to the state
someStore.$subscribe((mutation, state) => {
// 'direct' | 'patch object' | 'patch function'
mutation.type
someStore.$patch({
myState: "something else",
otherState: true
})
})
Subscribing to the state
someStore.$subscribe((mutation, state) => {
// 'direct' | 'patch object' | 'patch function'
mutation.type
someStore.$patch((state)=>{
state.myState = "something else"
})
})
Subscribing to the state
someStore.$subscribe((mutation, state) => {
// 'direct' | 'patch object' | 'patch function'
mutation.type
// same as cartStore.$id
mutation.storeId
// only available with mutation.type === 'patch object'
mutation.payload // patch object passed to $patch()
})
Subscribing to the state
someStore.$subscribe((mutation, state) => {
// 'direct' | 'patch object' | 'patch function'
mutation.type
// same as cartStore.$id
mutation.storeId
defineStore("someStore", {/*...*/})
})
Subscribing to the state
someStore.$subscribe((mutation, state) => {
// 'direct' | 'patch object' | 'patch function'
mutation.type
// same as cartStore.$id
mutation.storeId
// only available with mutation.type === 'patch object'
mutation.payload // patch object passed to $patch()
})
Subscribing to the state
someStore.$subscribe((mutation, state) => {
localStorage.setItem('myState', JSON.stringify(state))
})
Subscribing to the state
https://tinyurl.com/3sx3p7sd
Goal: Practice subscribing to Pinia actions and state
Just like Vue has plugins to quickly add advanced functionality...
...so does Pinia
One advantages of the standardization provided by an official store solution over vanilla CAPI store
What can they do?
How to build
//PiniaLocalStoragePlugin.ts
export function PiniaLocalStoragePlugin({
pinia,
app,
store,
options
}) {
}
// the pinia created with `createPinia()`
// the current app created with `createApp()` (Vue 3 only)
// the store the plugin is augmenting
// the options object defining the store passed to `defineStore()`
How to build
//PiniaLocalStoragePlugin.ts
export function PiniaLocalStoragePlugin({
pinia,
app,
store,
options
}) {
const localStorageKey = `PiniaStore_${store.$id}`;
store.$subscribe((mutation, state) => {
localStorage.setItem(localStorageKey, JSON.stringify(state));
});
const savedState = localStorage.getItem(localStorageKey);
if (savedState) {
store.$state = JSON.parse(savedState);
}
}
Applies to all stores by default
//PiniaLocalStoragePlugin.ts
export function PiniaLocalStoragePlugin({
pinia,
app,
store,
options
}) {
const localStorageKey = `PiniaStore_${store.$id}`;
store.$subscribe((mutation, state) => {
localStorage.setItem(localStorageKey, JSON.stringify(state));
});
const savedState = localStorage.getItem(localStorageKey);
if (savedState) {
store.$state = JSON.parse(savedState);
}
}
Making it opt-in
//CartStore.ts
export const useCartStore = defineStore("CartStore", {
localStorage: true,
state: ()=>{ /*...*/ },
getters: {/*...*/}
})
Making it opt-in
//PiniaLocalStoragePlugin.ts
export function PiniaLocalStoragePlugin({
pinia,
app,
store,
options
}) {
if (!options.localStorage) return;
const localStorageKey = `PiniaStore_${store.$id}`;
store.$subscribe((mutation, state) => {
localStorage.setItem(localStorageKey, JSON.stringify(state));
});
const savedState = localStorage.getItem(localStorageKey);
if (savedState) {
store.$state = JSON.parse(savedState);
}
}
Type the new option
import "pinia";
declare module "pinia" {
export interface DefineStoreOptionsBase<S, Store> {
// allow defining a boolean
// for local storeage plugin
localStorage?: boolean;
}
}
extend the DefineStoreOptionsBase
// main.ts
//...
import { PiniaLocalStoragePlugin } from "./plugins/...";
const pinia = createPinia()
.use(PiniaLocalStoragePlugin);
createApp(App)
.use(pinia)
.mount("#app");
Use the plugin
//PiniaAxiosPlugin.ts
import axios from "axios";
export function PiniaAxiosPlugin({
pinia,
app,
store,
options
}) {
store.axios = axios;
}
Another example: adding axios
// ProductStore.ts
export const useProductStore = defineStore("ProductStore", {
//...
actions: {
async fill() {
const res = await this.axios.get("/products.json");
this.products = res.data;
},
},
});
Another example: adding axios
//PiniaAxiosPlugin.ts
import axios from "axios";
import { markRaw } from "vue";
export function PiniaAxiosPlugin({
pinia,
app,
store,
options
}) {
store.axios = markRaw(axios);
}
Another example: adding axios
Typing axios on stores
import "pinia";
import type { Axios } from "axios";
declare module "pinia" {
export interface PiniaCustomProperties {
axios: Axios;
}
}
Typing axios on stores
3rd party pinia plugins
https://tinyurl.com/38j9kswt
Goal: Practice creating and using a Pinia plugin
//CounterStore.ts
export const useCounterStore = defineStore('CounterStore', () => {
const count = ref(0)
function increment() {
count.value++
}
return { count, increment }
})
Why?
Why?
//CounterStore.ts
export const useCounterStore = defineStore('CounterStore', () => {
const count = ref(0)
function increment() {
count.value++
}
watch(count, ()=>{
// save some data to the server or whatever
})
return { count, increment }
})
//CounterStore.ts
import { watchDebounced } from '@vueuse/core'
export const useCounterStore = defineStore('CounterStore', () => {
const count = ref(0)
function increment() {
count.value++
}
watchDebounced(count, ()=>{
// save some data to the server or whatever
}, { debounce: 500 })
return { count, increment }
})
https://tinyurl.com/2p9atauj
Goal: Practice using the setup function style of defining a store
NEW
πΊ
Vue Video
Courses
π¨βπ«
Live Workshops
π¬
Bi-weekly Support
Sessions
π§βπ§
Database for hiring
Vue devs