After vue3 uses the vuex page to refresh, the store is lost - vuex

When I use vue3, I use store. When the page is loaded, I want to get the previously stored store, but after refreshing the page, it is undefined

this is my answer
setup() {
const store = useStore();
// 在页面加载时读取sessionStorage里的状态信息
if (sessionStorage.getItem('store')) {
// 存储状态
store.replaceState(Object.assign({}, store.state, JSON.parse(sessionStorage.getItem('store'))))
}
// 在页面刷新时将vuex里的信息保存到sessionStorage里
window.addEventListener('beforeunload', () => {
sessionStorage.setItem('store', JSON.stringify(store.state))
})
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.13/vue.js"></script>

Related

Nested useFetch in Nuxt 3

How do you accomplish nested fetching in Nuxt 3?
I have two API's. The second API has to be triggered based on a value returned in the first API.
I tried the code snippet below, but it does not work, since page.Id is null at the time it is called. And I know that the first API return valid data. So I guess the second API is triggered before the result is back from the first API.
<script setup>
const route = useRoute()
const { data: page } = await useFetch(`/api/page/${route.params.slug}`)
const { data: paragraphs } = await useFetch(`/api/page/${page.Id}/paragraphs`)
</script>
Obviously this is a simple attempt, since there is no check if the first API actually return any data. And it is not even waiting for a response.
In Nuxt2 I would have placed the second API call inside .then() but with this new Composition API setup i'm a bit clueless.
You could watch the page then run the API call when the page is available, you should paragraphs as a ref then assign the destructed data to it :
<script setup>
const paragraphs = ref()
const route = useRoute()
const { data: page } = await useFetch(`/api/page/${route.params.slug}`)
watch(page, (newPage)=>{
if (newPage.Id) {
useFetch(`/api/page/${newPage.Id}/paragraphs`).then((response)=>{
paragraphs.value = response.data
})
}
}, {
deep: true,
immediate:true
})
</script>
One solution is to avoid using await. Also, use references to hold the values. This will allow your UI and other logic to be reactive.
<script setup>
const route = useRoute()
const page = ref()
const paragraphs = ref()
useFetch(`/api/page/${route.params.slug}`).then(it=> {
page.value = it
useFetch(`/api/page/${page.value.Id}/paragraphs`).then(it2=> {
paragraphs.value = it2
}
}
</script>
You can set your 2nd useFetch to not immediately execute until the first one has value:
<script setup>
const route = useRoute()
const { data: page } = await useFetch(`/api/page/${route.params.slug}`)
const { data: paragraphs } = await useFetch(`/api/page/${page.value?.Id}/paragraphs`, {
// prevent the request from firing immediately
immediate: false,
// watch reactive sources to auto-refresh
watch: [page]
})
</script>
You can also omit the watch option there and manually execute the 2nd useFetch.
But for it to get the updates, pass a function that returns a url instead:
const { data: page } = await useFetch(`/api/page/${route.params.slug}`)
const { data: paragraphs, execute } = await useFetch(() => `/api/page/${page.value?.Id}/paragraphs`, {
immediate: false,
})
watch(page, (val) => {
if (val.Id === 69) {
execute()
}
})
You should never call composables inside hooks.
More useFetch options can be seen here.

vue/vuex: Can you re-render a page from another page?

With the first login in my app, users get a possibility to leave their address. When this address is stored, the user are pushed to their dashboard. Second login the user go straight to the dashboard.
I have 2 Vuex states that are updated with the response.data. 'Signed' leads to address page, 'Frequent' leads to 'dashboard'.
//PROMPT.VUE
mounted () {
this.getPrompt()
},
computed: {
promptStatus () {
return this.$store.getters.getPrompt
}
},
methods: {
async getPrompt() {
try{
await //GET axios etc
// push prompt status in Store
let value = response.data
this.$store.commit('setPrompt', value)
if (this.promptStatus === 'signed') {
this.$router.push({path: '/adres'})
}
if (this.promptStatus === 'frequent') {
this.$router.push({path: '/dashboard'})
}
When user leaves the address I reset the vuex.state from 'signed' to 'frequent'.
//ADRES.VUE
//store address
let value = 'frequent'
this.$store.commit('setPrompt', value)
this.$router.push({name: 'Prompt'})
The Vuex.store is refreshed. But the Prompt.vue wil not re-render with the new vuex.status. Many articles are written. Can 't find my solution. Maybe I organize my pages the wrong way.
In views, it is not recommended to mutate data (call commit) outside vuex. Actions are created for these purposes (called from the component using dispatch). In your case, you need to call action "getPrompt" from the store, but process routing in the authorization component. This is more about best practice
To solve your problem, you need to make a loader when switching to dashboard. Until the data is received, you do not transfer the user to the dashboard page
Example
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
name: "DashboardLayout",
components: { ..., ... },
data: () => ({
isLoad: false
}),
async created() {
this.isLoad = false;
try {
await this.$store.dispatch('getData');
this.isLoad = true;
} catch (error) {
console.log(error)
}
}
});
</script>
Data is received and stored in the store in the "getData" action.
The referral to the dashboard page takes place after authorization. If authorization is invalid, the router.beforeEach handler (navigation guards) in your router/index.js should redirect back to the login page.
Learn more about layout in vuejs
Learn more about navigation guards

Vuex populate data from API call at the start

apologies for the simple question, I'm really new to Vue/Nuxt/Vuex.
I am currently having a vuex store, I wish to be able to populate the list with an API call at the beginning (so that I would be able to access it on all pages of my app directly from the store vs instantiating it within a component).
store.js
export const state = () => ({
list: [],
})
export const mutations = {
set(state, testArray) {
state.list = testArray
}
}
export const getters = {
getArray: state => {
return state.list
},
}
I essentially want to pre-populate state.list so that my components can call the data directly from vuex store. This would look something like that
db.collection("test").doc("test").get().then(doc=> {
let data = doc.data();
let array = data.array; // get array from API call
setListAsArray(); // put the array result into the list
});
I am looking for where to put this code (I assume inside store.js) and how to go about chaining this with the export. Thanks a lot in advance and sorry if it's a simple question.
(Edit) Context:
So why I am looking for this solution was because I used to commit the data (from the API call) to the store inside one of my Vue components - index.vue from my main page. This means that my data was initialized on this component, and if i go straight to another route, my data will not be available there.
This means: http://localhost:3000/ will have the data, if I routed to http://localhost:3000/test it will also have the data, BUT if i directly went straight to http://localhost:3000/test from a new window it will NOT have the data.
EDIT2:
Tried the suggestion with nuxtServerInit
Updated store.js
export const state = () => ({
list: [],
})
export const mutations = {
set(state, dealArray) {
state.list = dealArray
}
}
export const getters = {
allDeals: state => {
return state.list
},
}
export const actions = {
async nuxtServerInit({ commit }, { req }) {
// fetch your backend
const db = require("~/plugins/firebase.js").db;
let doc = await db.collection("test").doc("test").get();
let data = doc.data();
console.log("deals_array: ", data.deals_array); // nothing logged
commit('set', data.deals_array); // doesn't work
commit('deals/set', data.deals_array); // doesn't work
}
}
Tried actions with nuxtServerInit, but when logging store in another component it is an empty array. I tried to log the store in another component (while trying to access it), I got the following:
store.state: {
deals: {
list: []
}
}
I would suggest to either:
calling the fetch method in the default.vue layout or any page
use the nuxtServerInit action inside the store directly
fetch method
You can use the fetch method either in the default.vue layout where it is called every time for each page that is using the layout. Or define the fetch method on separate pages if you want to load specific data for individual pages.
<script>
export default {
data () {
return {}
},
async fetch ({store}) {
// fetch your backend
var list = await $axios.get("http://localhost:8000/list");
store.commit("set", list);
},
}
</script>
You can read more regarding the fetch method in the nuxtjs docs here
use the nuxtServerInit action inside the store directly
In your store.js add a new action:
import axios from 'axios';
actions: {
nuxtServerInit ({ commit }, { req }) {
// fetch your backend
var list = await axios.get("http://localhost:8000/list");
commit('set', list);
}
}
}
You can read more regarding the fetch method in the nuxtjs docs here
Hope this helps :)

Transfer Data From One Component to Another

I have a component which makes a call to my backend API. This then provides me with data that I use for the component. I now want to create another component which also uses that data. While I could just do another api call that seems wasteful.
So, in Profile.vue i have this in the created() function.
<script>
import axios from 'axios';
import { bus } from '../main';
export default {
name: 'Profile',
data() {
return {
loading: false,
error: null,
profileData: null,
getImageUrl: function(id) {
return `http://ddragon.leagueoflegends.com/cdn/9.16.1/img/profileicon/` + id + `.png`;
}
}
},
beforeCreate() {
//Add OR Remove classes and images etc..
},
async created() {
//Once page is loaded do this
this.loading = true;
try {
const response = await axios.get(`/api/profile/${this.$route.params.platform}/${this.$route.params.name}`);
this.profileData = response.data;
this.loading = false;
bus.$emit('profileData', this.profileData)
} catch (error) {
this.loading = false;
this.error = error.response.data.message;
}
}
};
</script>
I then have another child component that I've hooked up using the Vue router, this is to display further information.
MatchHistory compontent
<template>
<section>
<h1>{{profileDatas.profileDatas}}</h1>
</section>
</template>
<script>
import { bus } from '../main';
export default {
name: 'MatchHistory',
data() {
return {
profileDatas: null
}
},
beforeCreate() {
//Add OR Remove classes and images etc..
},
async created() {
bus.$on('profileData', obj => {
this.profileDatas = obj;
});
}
};
</script>
So, I want to take the info and display the data that I have transferred across.
My assumption is based on the fact that these components are defined for two separate routes and an event bus may not work for your situation based on the design of your application. There are several ways to solve this. Two of them listed below.
Vuex (for Vue state management)
Any local storage option - LocalStorage/SessionStorage/IndexDB e.t.c
for more information on VueX, visit https://vuex.vuejs.org/.
for more information on Localstorage, visit https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage.
for more information on session storage, visit https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage
The flow is pretty much the same for any of the options.
Get your data from an API using axios as you did above in Profile.vue
Store the retrieved data with VueX or Local/Session storage
Retrieve the data from Vuex or local/session storage in the created method of MatchHistory.vue component
For the local / session storage options, you will have to convert your object to a json string as only strings can be stored in storage. see below.
in Profile.vue (created)
const response = await axios.get(........)
if(response){
localStorage.setItem('yourstoragekey', JSON.stringify(response));
}
In MatchHistory.Vue (created)
async created() {
var profileData = localStorage.getItem('yourstoragekey')
if(profileData){
profileData = JSON.parse(profileData );
this.profileData = profileData
}
}
You can use vm.$emit to create an Eventbus
// split instance
const EventBus = new Vue({})
class IApp extends Vue {}
IApp.mixin({
beforeCreate: function(){
this.EventBus = EventBus
}
})
const App = new IApp({
created(){
this.EventBus.$on('from-mounted', console.log)
},
mounted(){
this.EventBus.$emit('from-mounted', 'Its a me! Mounted')
}
}).$mount('#app')
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app"></div>
further readings
You can make use of the VUEX which is a state management system for Vue.
When you make api call and get the data you need, you can COMMIT a MUTATION and pass your data to it. What it will do, it will update your STATE and all of your components will have access to its state (data)
In your async created(), when you get response, just commit mutation to your store in order to update the state. (omitted example here as the vuex store will need configuration before it can perform mutations)
Then in your child component,
data(){
return {
profileDatas: null
}
},
async created() {
this.profileDatas = $store.state.myData;
}
It might seem like an overkill in your case, but this approach is highly beneficial when working with external data that needs to be shared across multiple components

Vue test-utils how to test a router.push()

In my component , I have a method which will execute a router.push()
import router from "#/router";
// ...
export default {
// ...
methods: {
closeAlert: function() {
if (this.msgTypeContactForm == "success") {
router.push("/home");
} else {
return;
}
},
// ....
}
}
I want to test it...
I wrote the following specs..
it("should ... go to home page", async () => {
// given
const $route = {
name: "home"
},
options = {
...
mocks: {
$route
}
};
wrapper = mount(ContactForm, options);
const closeBtn = wrapper.find(".v-alert__dismissible");
closeBtn.trigger("click");
await wrapper.vm.$nextTick();
expect(alert.attributes().style).toBe("display: none;")
// router path '/home' to be called ?
});
1 - I get an error
console.error node_modules/#vue/test-utils/dist/vue-test-utils.js:15
[vue-test-utils]: could not overwrite property $route, this is usually caused by a plugin that has added the property asa read-only value
2 - How I should write the expect() to be sure that this /home route has been called
thanks for feedback
You are doing something that happens to work, but I believe is wrong, and also is causing you problems to test the router. You're importing the router in your component:
import router from "#/router";
Then calling its push right away:
router.push("/home");
I don't know how exactly you're installing the router, but usually you do something like:
new Vue({
router,
store,
i18n,
}).$mount('#app');
To install Vue plugins. I bet you're already doing this (in fact, is this mechanism that expose $route to your component). In the example, a vuex store and a reference to vue-i18n are also being installed.
This will expose a $router member in all your components. Instead of importing the router and calling its push directly, you could call it from this as $router:
this.$router.push("/home");
Now, thise makes testing easier, because you can pass a fake router to your component, when testing, via the mocks property, just as you're doing with $route already:
const push = jest.fn();
const $router = {
push: jest.fn(),
}
...
mocks: {
$route,
$router,
}
And then, in your test, you assert against push having been called:
expect(push).toHaveBeenCalledWith('/the-desired-path');
Assuming that you have setup the pre-requisities correctly and similar to this
Just use
it("should ... go to home page", async () => {
const $route = {
name: "home"
}
...
// router path '/home' to be called ?
expect(wrapper.vm.$route.name).toBe($route.name)
});