Vue useStorage (usevue) always starting clean rather than importing state in Pinia store - vue.js

I built my app on top of vitesse-nuxt3, and all is going well except for trying to use LocalStorage via vueuse.
Component:
<script setup lang="ts">
const { test } = useTestStore()
</script>
<template>
<div>
<pre>{{ test }}</pre>
<hr>
<input
:id="slug"
v-model="value"
type="text"
>
</div>
</template>
Pinia Store:
import { acceptHMRUpdate, defineStore } from 'pinia'
import { useStorage } from '#vueuse/core'
export const useTestStore = defineStore('test', () => {
const test = ref(
useStorage('test', {
initials: 'It is initials',
}),
)
return ({
test,
})
})
if (import.meta.hot)
import.meta.hot.accept(acceptHMRUpdate(useTestStore, import.meta.hot))
I watch it set the data (in Chrome's dev tools), but it always reloads the default data instead rather than persisting between refreshes.
Thank you.

The problem in your demo is that the component is being rendered server-side, which has no Local Storage, so useStorage() defaults to the given initial value.
One workaround is to render the component on the client only, using the <client-only> component:
<client-only>
<component-that-uses-local-storage />
</client-only>
demo

for store like this like #tony19 said
export const useAuthStore = defineStore({
id: 'auth.store',
state: () => {
token: {
accessToken: useStorage('accessToken', [XXXX], undefined, { serializer: StorageSerializers.object }),
refreshToken: useStorage('refreshToken', [YYYY], undefined, { serializer: StorageSerializers.object })
},
},
})
[XXXX] [YYYY] is default value
after ssr pinia.state.value become to
window.__INITIAL_SSR_CONTEXT__ = {
state: {
"auth.store": {
"token":{
"accessToken":[XXXX],
"refreshToken":[YYYY]
}
}
}
}
on client side reasign the json object to store like this
const ssr_state = (window as any)['__INITIAL_SSR_CONTEXT__']?.['state']
if (ssr_state) {
pinia.state.value = ssr_state
}
so the accessToken, refreshToken property changes to plan object on client side, you can change it but the storage don't update.
my solution:
add one action to store
actions:{
// ...
// call this once when isSSR is true on client side entry
reasignToken() {
this.token = {
accessToken: useStorage('accessToken', this.token.accessToken, undefined, {
serializer: StorageSerializers.object
}),
refreshToken: useStorage('refreshToken', this.token.refreshToken, undefined, {
serializer: StorageSerializers.object
})
}
},
// regular call on server side and client side
setToken(token) {
//...
}
}

Related

Nuxt3 + Pinia + VueUse -> useStorage() not working

Setup: I'm using Nuxt3 + Pinia + VueUse.
Goal:
I want to save a state of a pinia store to localstorage via VueUse: useStorage.
Problem:
For some reason no item is created in localstorage. I feel like I'm missing something here. In components I can use useStorage fine.
in stores/piniaStoreVueUse.js
import { defineStore } from 'pinia'
import { useStorage } from '#vueuse/core'
export const usePiniaStoreVueUse = defineStore('piniaStoreUseVue', {
state: () => {
return {
state: useStorage('my-state', 'empty'),
}
},
actions: {
enrollState() {
this.state = 'enroll';
},
emptyState() {
this.state = 'empty';
},
},
getters: {
}
});
in components/SampleComponentStatePiniaVueUse.vue
<script lang="ts" setup>
import { usePiniaStoreVueUse } from '~/stores/piniaStoreVueUse';
const piniaStoreVueUse = usePiniaStoreVueUse();
</script>
<template>
<div>
piniaStoreVueUse.state: {{ piniaStoreVueUse.state }}<br>
<button class="button" #click="piniaStoreVueUse.enrollState()">
enrollState
</button>
<button class="button" #click="piniaStoreVueUse.emptyState()">
clearState
</button>
</div>
</template>
<style scoped>
</style>
Live Version here
Thank you.
I found an answer to this:
Nuxt3 uses SSR by default. But since useStorage() (from VueUse) uses the browsers localstorage this can’t work.
Solution 1:
Disables SSR in your nuxt.config.js
export default defineNuxtConfig({
ssr: false,
// ... other options
})
Careful: This globally disables SSR.
Solution 2:
Wrap your component in <client-only placeholder="Loading…”>
<client-only placeholder="Loading...">
<MyComponent class="component-block"/>
</client-only>
I'd love to hear about other ways to deal with this. I feel like there should be a better way.
I'm folowed this topic two week. My resoleved use plugin pinia-plugin-persistedstate. I'm touch plugin/persistedstate.js and add persist: true, in Pinia defineStore()
First install plugin yarn add pinia-plugin-persistedstate or npm i pinia-plugin-persistedstate
#plugin/persistedstate.js
import { createNuxtPersistedState } from 'pinia-plugin-persistedstate'
export default defineNuxtPlugin(nuxtApp => {
nuxtApp.$pinia.use(createNuxtPersistedState(useCookie))
})
and
#story.js
export const useMainStore = defineStore('mainStore', {
state: () => {
return {
todos: useStorage('todos', []),
...
}
},
persist: true, #add this
getters: {...},
actions: {...}
})
I found a solution to this problem and it seems to work pretty well. I have not done extensive testing but it seems to work.
After loads of digging I came across a page in the Pinia documentation: Dealing with Composables
NOTES:
npm i -D #vueuse/nuxt #vueuse/core
My Tested Code:
//storageTestStore.js
import { defineStore, skipHydrate } from "pinia";
import { useLocalStorage } from '#vueuse/core'
export const useStorageTestStore = defineStore('storageTest', {
state: () => ({
user: useLocalStorage('pinia/auth/login', 'bob'),
}),
actions: {
setUser(user) {
this.user = user
}
},
hydrate(state, initialState) {
// in this case we can completely ignore the initial state since we
// want to read the value from the browser
state.user = useLocalStorage('pinia/auth/login', 'bob')
},
})
test.vue (~~/pages/test.vue)
<script setup>
import { ref, onMounted } from "vue";
import { useStorageTestStore } from "~~/stores/storageTestStore";
const storageTestStore = useStorageTestStore();
// create array with 10 random first names
const firstNames = [
"James",
"John",
"Robert",
"Michael",
"William",
"David",
"Richard",
"Charles",
"Joseph",
"Thomas",
];
const updateUser = () => {
storageTestStore.setUser(
firstNames[Math.floor(Math.random() * firstNames.length)]
);
};
</script>
<template>
<div class="max-w-[1152px] mx-auto">
<h1 class="text-xl">{{ storageTestStore.user }}</h1>
<button
class="text-lg bg-emerald-300 text-emerald-900 p-5 rounded-lg"
#click="updateUser()"
>
Change User
</button>
</div>
</template>
<style scoped></style>
The state fully persisted after browser reloads and navigating in the application.
you can use ref with useStorage()
import { defineStore } from 'pinia'
import { useStorage } from '#vueuse/core'
export const usePiniaStoreVueUse = defineStore('piniaStoreUseVue', {
state: () => {
return {
state: ref(useStorage('my-state', 'empty')),
}
},
actions: {
enrollState() {
this.state = 'enroll';
},
emptyState() {
this.state = 'empty';
},
},
getters: {
}
});

Vue3 reactive components on globalProperties

In vuejs 2 it's possible to assign components to global variables on the main app instance like this...
const app = new Vue({});
Vue.use({
install(Vue) {
Vue.prototype.$counter = new Vue({
data: () => ({ value: 1 }),
methods: {
increment() { this.value++ },
}
});
}
})
app.$mount('#app');
But when I convert that to vue3 I can't access any of the properties or methods...
const app = Vue.createApp({});
app.use({
install(app) {
app.config.globalProperties.$counter = Vue.createApp({
data: () => ({ value: 1 }),
methods: {
increment() { this.value++ }
}
});
}
})
app.mount('#app');
Here is an example for vue2... https://jsfiddle.net/Lg49anzh/
And here is the vue3 version... https://jsfiddle.net/Lathvj29/
So I'm wondering if and how this is still possible in vue3 or do i need to refactor all my plugins?
I tried to keep the example as simple as possible to illustrate the problem but if you need more information just let me know.
Vue.createApp() creates an application instance, which is separate from the root component of the application.
A quick fix is to mount the application instance to get the root component:
import { createApp } from 'vue';
app.config.globalProperties.$counter = createApp({
data: () => ({ value: 1 }),
methods: {
increment() { this.value++ }
}
}).mount(document.createElement('div')); 👈
demo 1
However, a more idiomatic and simpler solution is to use a ref:
import { ref } from 'vue';
const counter = ref(1);
app.config.globalProperties.$counter = {
value: counter,
increment() { counter.value++ }
};
demo 2
Not an exact answer to the question but related. Here is a simple way of sharing global vars between components.
In my main app file I added the variable $navigationProps to global scrope:
let app=createApp(App)
app.config.globalProperties.$navigationProps = {mobileMenuClosed: false, closeIconHidden:false };
app.use(router)
app.mount('#app')
Then in any component where I needed that $navigationProps to work with 2 way binding:
<script>
import { defineComponent, getCurrentInstance } from "vue";
export default defineComponent({
data: () => ({
navigationProps:
getCurrentInstance().appContext.config.globalProperties.$navigationProps,
}),
methods: {
toggleMobileMenu(event) {
this.navigationProps.mobileMenuClosed =
!this.navigationProps.mobileMenuClosed;
},
hideMobileMenu(event) {
this.navigationProps.mobileMenuClosed = true;
},
},
Worked like a charm for me.
The above technique worked for me to make global components (with only one instance in the root component). For example, components like Loaders or Alerts are good examples.
Loader.vue
...
mounted() {
const currentInstance = getCurrentInstance();
if (currentInstance) {
currentInstance.appContext.config.globalProperties.$loader = this;
}
},
...
AlertMessage.vue
...
mounted() {
const currentInstance = getCurrentInstance();
if (currentInstance) {
currentInstance.appContext.config.globalProperties.$alert = this;
}
},
...
So, in the root component of your app, you have to instance your global components, as shown:
App.vue
<template>
<v-app id="allPageView">
<router-view name="allPageView" v-slot="{Component}">
<transition :name="$router.currentRoute.name">
<component :is="Component"/>
</transition>
</router-view>
<alert-message/> //here
<loader/> //here
</v-app>
</template>
<script lang="ts">
import AlertMessage from './components/Utilities/Alerts/AlertMessage.vue';
import Loader from './components/Utilities/Loaders/Loader.vue';
export default {
name: 'App',
components: { AlertMessage, Loader }
};
</script>
Finally, in this way you can your component in whatever other components, for example:
Login.vue
...
async login() {
if (await this.isFormValid(this.$refs.loginObserver as FormContext)) {
this.$loader.activate('Logging in. . .');
Meteor.loginWithPassword(this.user.userOrEmail, this.user.password, (err: Meteor.Error | any) => {
this.$loader.deactivate();
if (err) {
console.error('Error in login: ', err);
if (err.error === '403') {
this.$alert.showAlertFull('mdi-close-circle', 'warning', err.reason,
'', 5000, 'center', 'bottom');
} else {
this.$alert.showAlertFull('mdi-close-circle', 'error', 'Incorrect credentials');
}
this.authError(err.error);
this.error = true;
} else {
this.successLogin();
}
});
...
In this way, you can avoid importing those components in every component.

vue.js 2 single file component with dynamic template

I need a single file component to load its template via AJAX.
I search a while for a solution and found some hints about dynamic components.
I crafted a combination of a parent component which imports a child component and renders the child with a dynamic template.
Child component is this:
<template>
<div>placeholder</div>
</template>
<script>
import SomeOtherComponent from './some-other-component.vue';
export default {
name: 'child-component',
components: {
'some-other-component': SomeOtherComponent,
},
};
</script>
Parent component is this
<template>
<component v-if='componentTemplate' :is="dynamicComponent && {template: componentTemplate}"></component>
</template>
<script>
import Axios from 'axios';
import ChildComponent from './child-component.vue';
export default {
name: 'parent-component',
components: {
'child-component': ChildComponent,
},
data() {
return {
dynamicComponent: 'child-component',
componentTemplate: null,
};
},
created() {
const self = this;
this.fetchTemplate().done((htmlCode) => {
self.componentTemplate = htmlCode;
}).fail((error) => {
self.componentTemplate = '<div>error</div>';
});
},
methods: {
fetchTemplate() {
const formLoaded = $.Deferred();
const url = '/get-dynamic-template';
Axios.get(url).then((response) => {
formLoaded.resolve(response.data);
}).catch((error) => {
formLoaded.reject(error);
}).then(() => {
formLoaded.reject();
});
return formLoaded;
},
},
};
</script>
The dynamic template code fetched is this:
<div>
<h1>My dynamic template</h1>
<some-other-component></some-other-component>
</div>
In general the component gets its template as expected and binds to it.
But when there are other components used in this dynamic template (some-other-component) they are not recognized, even if they are correctly registered inside the child component and of course correctly named as 'some-other-component'.
I get this error: [Vue warn]: Unknown custom element: some-other-component - did you register the component correctly? For recursive components, make sure to provide the "name" option.
Do I miss something or is it some kind of issue/bug?
I answer my question myself, because I found an alternative solution after reading a little bit further here https://forum.vuejs.org/t/load-html-code-that-uses-some-vue-js-code-in-it-via-ajax-request/25006/3.
The problem in my code seems to be this logical expression :is="dynamicComponent && {template: componentTemplate}". I found this approach somewhere in the internet.
The original poster propably assumed that this causes the component "dynamicComponent" to be merged with {template: componentTemplate} which should override the template option only, leaving other component options as defined in the imported child-component.vue.
But it seems not to work as expected since && is a boolean operator and not a "object merge" operator. Please somebody prove me wrong, I am not a JavaScript expert after all.
Anyway the following approach works fine:
<template>
<component v-if='componentTemplate' :is="childComponent"></component>
</template>
<script>
import Axios from 'axios';
import SomeOtherComponent from "./some-other-component.vue";
export default {
name: 'parent-component',
components: {
'some-other-component': SomeOtherComponent,
},
data() {
return {
componentTemplate: null,
};
},
computed: {
childComponent() {
return {
template: this.componentTemplate,
components: this.$options.components,
};
},
},
created() {
const self = this;
this.fetchTemplate().done((htmlCode) => {
self.componentTemplate = htmlCode;
}).fail((error) => {
self.componentTemplate = '<div>error</div>';
});
},
methods: {
fetchTemplate() {
const formLoaded = $.Deferred();
const url = '/get-dynamic-template';
Axios.get(url).then((response) => {
formLoaded.resolve(response.data);
}).catch((error) => {
formLoaded.reject(error);
}).then(() => {
formLoaded.reject();
});
return formLoaded;
},
},
};
</script>

Why is Vuex not detected after refresh page? (nuxt)

Vuex is not detected after refresh, but all data is output to the console. Also after refresh, some components behave incorrectly. For example, I use vee-validate and all the rules and fields I get from the back, after refresh the validation rules disappear, but the fields are displayed
Vuex works on all pages but after refresh only on the home page
stroe/index.js
export const state = () => ({});
const map = {
ru: "ru",
uk: "uk-ua"
};
export const getters = {
lang(state) {
return map[state.i18n.locale];
}
};
export const mutations = {};
export const actions = {
async nuxtServerInit({ state, dispatch }) {
try {
await dispatch('category/getCategories', {
});
} catch (err) {
console.log('nuxt server init error', err);
}
}
};
home page (everything works)
<template>
<div>
<main class="home-page">
<banner />
<section class="home_page">
<div class="container">
<phone-pay />
<card-pay />
<categories :categories="categories" :services="services" />
<main-banner />
</div>
</section>
</main>
</div>
</template>
<script>
import Banner from "#/components/Index/Banner";
import PhonePay from "#/components/Index/PhonePay";
import CardPay from "#/components/Index/CardPay";
import Categories from "#/components/Index/Categories";
import MainBanner from "#/components/Index/MainBanner";
export default {
components: {
Banner,
PhonePay,
CardPay,
Categories,
MainBanner
},
async asyncData({ store, app: { $api }, error, req }) {
try {
const {
data: { data: categories, included: services }
} = await $api.CategoryProvider.getPopularCategories({
params: {
include: "services"
}
});
return {
lang: store.getters.lang,
categories,
services
};
} catch (e) {
console.log("error index", e);
error({ statusCode: 404, message: "Page not found" });
}
}
};
</script>
category (does not work)
<template>
<services-viewer :initial-services="initialServices" :category="category" :init-meta="initMeta" />
</template>
<script>
import ServicesViewer from "#/components/UI/ServicesViewer";
export default {
components: {
ServicesViewer
},
async asyncData({ store, route, error, app: { $api } }) {
try {
const {
data: { data: initialServices, meta: initMeta }
} = await $api.ServiceProvider.getServices({
params: {
"filter[category_slug]": route.params.id,
include: "category"
// "page[size]": serviceConfig.SERVICE_PAGINATION_PAGE_SIZE
}
});
await store.dispatch("category/getCategories", {
params: {}
});
const category = store.state.category.categories.find(
({ attributes: { slug } }) => slug === route.params.id
);
return {
initialServices,
category,
initMeta
};
} catch (e) {
const statusCode = e && e.statusCode ? e.statusCode : 404;
error({ statusCode });
}
}
};
</script>
install the below package:
npm install --save vuex-persistedstate
then change your store like below, then your data will be available after refresh the page.
// store/index.js
import Vuex from 'vuex';
import createPersistedState from 'vuex-persistedstate'
const createStore = () =>
new Vuex.Store({
plugins: [createPersistedState()],
state: {
},
mutations: {
},
getters:{
}
});
export default createStore;
for more details you can read from here.
I solved it. It was my mistake. I have a parallax plugin that works on the home page, but if you go to another page and refresh, the plugin starts and cannot find the item and breaks the page.
follow this link for your question
The nuxtServerInit Action
If the action nuxtServerInit is defined in the store and the mode is universal, Nuxt.js will call it with the context (only from the server-side). It's useful when we have some data on the server we want to give directly to the client-side.
For example, let's say we have sessions on the server-side and we can access the connected user through req.session.user. To give the authenticated user to our store, we update our store/index.js to the following:
actions: {
nuxtServerInit ({ commit }, { req }) {
if (req.session.user) {
commit('user', req.session.user)
}
}
}

Vue content modified after serverPrefetch on client side, when using SSR

I am working with Vue, by means of Quasar, with the pages being rendered via SSR. This works well enough, but I have a component that doesn't seem to behaving properly.
The issue is that the content is rendered correctly on the server side (verified by checking network log in Chrome), with the axios call loading in the data into an element using v-html, but when we get to the browser the state seems to be reset and server side rendered content gets lost, when using the 'elements' tab in the inspector.
Any ideas?
The Vue component is as follows:
<template>
<div class="dy-svg" v-html="svgData"></div>
</template>
<script>
/**
* This provides a way of loading an SVG and embedding it straight into
* the page, so that it can have css applied to it. Note, since we are
* using XHR to load the SVG, any non-local resource will have to deal
* with CORS.
*/
import axios from 'axios';
export default {
props: {
src: String,
prefetch: {
type: Boolean,
default: true
}
},
data() {
return {
svgData: undefined,
};
},
async serverPrefetch() {
if (this.prefetch) {
await this.loadImage();
}
},
async mounted() {
// if (!this.svgData) {
// await this.loadImage();
// }
},
methods: {
async loadImage() {
try {
let url = this.src;
if (url && url.startsWith('/')) {
url = this.$appConfig.baseUrl + url;
}
const response = await axios.get(url);
let data = response.data;
const idx = data.indexOf('<svg');
if (idx > -1) {
data = data.substring(idx, data.length);
}
this.svgData = data;
} catch (error) {
console.error(error);
}
}
}
};
</script>
Note, I did try add the v-once attribute to the div, but it seems to have no impact.
Environment:
Quasar 1.1.0
#quasar/cli 1.0.0
#quasar/app 1.0.6
NodeJS 10.15.3
Vue 2.6.10 (dependency via Quasar)
The fetched data needs to live outside the view components, in a dedicated data store, or a "state container". On the server, you should pre-fetch and fill data into the store while rendering. For this you can use Vuex.
Example Vuex store file:
import Vue from 'vue'
import Vuex from 'vuex'
import axios from 'axios'
// import example from './module-example'
Vue.use(Vuex)
export default function ( /* { ssrContext } */ ) {
const Store = new Vuex.Store({
state: () => ({
entities: {}
}),
actions: {
async get({
commit
}) {
await axios.get('https://example.com/api/items')
.then((res) => {
if (res.status === 200) {
commit('set', res.data.data)
}
})
}
},
mutations: {
set(state, entities) {
state.entities = entities
},
},
modules: {},
// enable strict mode (adds overhead!)
// for dev mode only
strict: process.env.DEV
})
return Store
}
Example Vue page script:
export default {
name: 'PageIndex',
computed: {
// display the item from store state.
entities: {
get() {
return this.$store.state.entities
}
}
},
serverPrefetch() {
return this.fetchItem()
},
mounted() {
if (!this.entities) {
this.fetchItem()
}
},
methods: {
fetchItem() {
return this.$store.dispatch('get')
}
}
}
This should solve the issue you're facing.