vue3 + pinia: how to make reactive a value from the store
Asked Answered
T

2

13

I am using vue 3 with composition api and pinia

I have an auth store that is reading from the store a default email and a default password

import { useAuthStore } from "stores/auth";
const authStore = useAuthStore();

const email = authStore.loginUser;
const password = authStore.passwordUser;

Then I am using email and password as v-model.

The problem is that both are not reactive. If I change the value from the text input, the model is not updated

I ask kindly for an explanation of the problem and a solution.

Tylertylosis answered 20/1, 2023 at 9:35 Comment(1)
It's unclear what behavior you expect. Do you want to change authStore via v-model?Invitation
T
24
// ❌ loses reactivity
const email = authStore.loginUser 

creates an email constant with the current value of authStore.loginUser, losing reactivity. To keep reactivity, you could use computed:

import { computed } from 'vue'

// ✅ keeps reactivity
const email = computed({
  get() { return authStore.loginUser },
  set(val) { authStore.loginUser = val }
})
// email is now a computed ref

...or you could use the provided storeToRefs wrapper, designed for extracting/deconstructing store reactive props while keeping their reactivity (basically to avoid the above boilerplate):

import { storeToRefs } from 'pinia'
// ✅ keeps reactivity
const { 
  loginUser: email,
  passwordUser: password
} = storeToRefs(authStore)
// email & password are now refs

Important: you only want to deconstruct state and getters using storeToRefs. Actions should be used directly from the store object (authStore in your case) or deconstructed without the wrapper:

const { actionOne, actionTwo } = authStore

This is specified in docs linked above:

... so methods and non reactive properties are completely ignored.


In conclusion, you typically end up with two deconstructions from each store:

import { useSomeStore } from '@/store'
// reactive:
const { s1, s2, g1, g2 } = storeToRefs(useSomeStore())
// non-reactive:
const { a1, a2 } = useSomeStore()

where s1, s2 are state members, g1, g2 are getters and a1, a2 are actions.

Tadzhik answered 20/1, 2023 at 9:42 Comment(3)
IMO storeToRefs just replaces one boilerplate with another, worse kind. Now you have to do ritual storeToRefs for all your refs (which ARE refs inside store's definition), and you have to explicitly separate state from methods every time you use the stuff (more cognitive complexity in exchange for nothing). I just don't get why pinia thinks it's a good idea to break defined refs with reactive object (which is bad with object destruction) and have more trouble instead of less.Horseweed
Here's how I look at it: 1) pinia needs way less boilerplate than vuex. 2) a store's state is not exactly refs, it's reactive. If you want the inner props of a reactive to remain reactive you either access them through the reactive container (in this case: state.myProp) or you have to convert them to refs, just like you do when you decompose a reactive for template ...toRefs(myContainer). Last, but not least, pinia was created because someone felt about vuex the way you feel about pinia. Maybe something even better will come out of your dissatisfaction with pinia.Tadzhik
Anything related to vuex is irrelevant as pinia positions itself not as "newer version of vuex", but "the store for vue" - so general "to go" state management for vue. And yes I understand how ref and reactive works thanks, it still doesn't explain pinia's decisions on it's API. Maybe something even better will come out of your dissatisfaction with pinia. I feel like it already exists - composables. Just put your state to a composable, no troubles of pinia.Horseweed
H
1

This worked for me: authStore.js:

import { defineStore } from "pinia";
import { ref } from "vue";

export const useAuthStore = defineStore("authStore", {
  state: () => ({
    email: ref("[email protected]"),
    password: ref("secret"),
  }),
});

And then a Login.vue component:

<script setup>
import { useAuthStore } from "../stores/authStore.js";
const store = useAuthStore();
</script>

<template>
  <form>
    <input type="email" v-model="store.email" />
    <input type="password" v-model="store.password" />
  </form>
</template>
<style>
input {
  display: block;
  border: 1px solid black;
  margin: 3px;
}
</style>

Which seems simpler than the accepted answer.

Horrid answered 28/9, 2023 at 13:32 Comment(3)
Except the v-model doesn't update when the store.email has changedEaldorman
@HugoCox except it does: github.com/nielsbom/vue3-pinia-example/tree/main Have you run the code?Horrid
First of all, the store's state members used for v-model are wrapped in reactive wrappers twice: once by the store's state own reactivity and once by each of the ref()'s reactivity. It works, but one should expect lower performance when using this technique at scale. Secondly, I don't think having to wrap each store member in ref() and having to prefix each template usage with store. is a good trade-off for calling storeToRefs once.Tadzhik

© 2022 - 2024 — McMap. All rights reserved.