For such component
<template>
<div>
<router-link :to="{name:'section', params: { sectionId: firstSectionId }}">Start</router-link>
</div>
</template>
<script lang="ts">
import { mapActions } from "vuex"
export default {
mounted() {
this.getSectionId()
},
computed: {
firstSectionId() {
return this.$store.state.firstSectionId
}
},
methods: mapActions(["getSectionId"])
}
</script>
Store:
const store: any = new Vuex.Store({
state: {
firstSectionId: null
},
// actions,
// mutations
})
I have a web request in the getSectionId action and it asynchronously fetches data and calls a mutation that will fill firstSectionId in state. During the initial rendering firstSectionId is null and I get the warning that a required parameter is missing during rendering of router-link.
It is not a problem here to add v-if="firstSectionId". But in general what is the approach for fetching data from a server to be displayed? Currently all my components are checking if there is data present in the store before rendering, is it normal or is there a better way to wait for data to be loaded before rendering it?
One approach for asynchronously fetching data is to use promise in vuex store actions.
Vue.http.get(API_URL)
.then((response) => {
//use response object
})
.catch((error) => {
console.log(error.statusText)
});
To demonstrate that I make request to this route. You can see how response should looks like. Let's save response object in state.users array.
store.js
const store = new Vuex.Store({
state: {
users: []
},
mutations: {
FETCH_USERS(state, users) {
state.users = users
}
},
actions: {
fetchUsers({ commit }, { self }) {
Vue.http.get("https://jsonplaceholder.typicode.com/users")
.then((response) => {
commit("FETCH_USERS", response.body);
self.filterUsers();
})
.catch((error) => {
console.log(error.statusText)
});
}
}
})
export default store
You noticed that there is self.filteruser() method after commit. That is crucial moment. Before that we are committing a mutation, which is synchronous operation and we are sure that we will have our response in store.state that can be used in filterUsers() method (don't forget to pass self parm)
Users.vue
import store from "../store/store"
export default {
name: 'users',
created() {
this.$store.dispatch("fetchUsers", { self: this })
},
methods:{
filterUsers() {
//do something with users
console.log("Users--->",this.$store.state.users)
}
}
}
Better ways (ES6 & ES7)
ES6 Promises for asynchronous programming
//User.vue
created() {
this.$store.dispatch("fetchUser").then(() => {
console.log("This would be printed after dispatch!!")
})
}
//store.js
actions: {
fetchUser({ commit }) {
return new Promise((resolve, reject) => {
Vue.http.get("https://jsonplaceholder.typicode.com/users")
.then((response) => {
commit("FETCH_USERS", response.body);
resolve();
})
.catch((error) => {
console.log(error.statusText);
});
});
}
}
ES7: async/await
To get away from callback hell, and to improve asynchronous programming use async function, and you can await on a promise. Code looks much easier to follow (like it is synchronous), but code isn't readable for browsers so you'll need Babel transpiler to run it.
actions: {
async actionA ({ commit }) {
commit('gotData', await getData())
},
async actionB ({ dispatch, commit }) {
await dispatch('actionA') // wait for actionA to finish
commit('gotOtherData', await getOtherData())
}
}
In my experience, you can skip a few checks if you preset the state with an empty value of the same type as the expected result (if you know what to expect, of course), e.g. if you have an array of items, start with [] instead of null as it won't break v-for directives, .length checks and similar data access attempts.
But generally, adding v-if is a very normal thing to do. There's a section about this in the vue-router documentation and checking whether properties exist or not is exactly what it suggests. Another possible solution it mentions is fetching data inside beforeRouteEnter guard, which assures you will always get to the component with your data already available.
Ultimately, both solutions are correct, and the decision between them is more of a UX/UI question.
I had similar requirements for locations and the google map api. I needed to fetch my locations from the API, load them in a list, and then use those in a map component to create the markers. I fetched the data in a Vuex action with axios, loaded that in my state with a mutation, and then used a getter to retrieve the resulting array in the mounted life cycle hook. This resulted in an empty array as mounted fired before the async action resolved.
I used store.subscribe to solve it this way:
<template>
<div class="google-map" :id="mapName"></div>
</template>
<script>
import GoogleMapsLoader from 'google-maps';
import { mapGetters } from 'vuex';
export default {
name: 'google-map',
props: ['name'],
computed: {
...mapGetters({
locations: 'locations/locations',
}),
},
data() {
return {
mapName: `${this.name}-map`,
};
},
mounted() {
this.$store.subscribe((mutation, state) => {
if (mutation.type === 'locations/SAVE_LOCATIONS') {
GoogleMapsLoader.KEY = 'myKey';
GoogleMapsLoader.load((google) => {
/* eslint-disable no-new */
const map = new google.maps.Map(document.getElementById('locations-map'));
// loop through locations and add markers to map and set map boundaries
const bounds = new google.maps.LatLngBounds();
// I access the resulting locations array via state.module.property
state.locations.locations.forEach((location) => {
new google.maps.Marker({
position: {
lat: location.latitude,
lng: location.longitude,
},
map,
});
bounds.extend({
lat: location.latitude,
lng: location.longitude,
});
});
map.fitBounds(bounds);
});
}
});
},
};
Related
I have a couple of Pinia stores that should share a set of actions and getters and I’m not quite sure how to effectively achieve that.
I’m building an app that lets users manage a number of different media (Books, Movies, TV Shows, etc). The way I’m currently thinking about it is to have a store for each media type e.g. BookStore, MovieStore etc. A lot of the getters and actions (e.g., count and deleteOne) are exactly the same between those different stores.
How do I achieve DRY here? The Pinia documentation has examples that mostly focus around reusing actions and getters inside other stores but I don’t think that quite solves my use case of inheriting a set of getters and setters outright.
Is my attempted inheritance approach here an anti-pattern?
This is achievable using plugins docs
Example Movies:
You have multiple stores using shared naming scheme for each state:
item: single entity item (single movie details)
collection: collection of items (collection of all movies)
each store will have the same CRUD actions with only the URL changing
getCollection: get list of items from API and set response as collection (https://url.com/movies)
getItem: get single item from API and set response as item (https://url.com/movies/id)
handleError: displays alert to the user with error information
Create plugin:
function BaseStorePlugin () {
return {
collection: [],
item: {},
getCollection: function (url) {
api.get(url)
.then((response) => {
this.collection = response.data;
})
.catch((error) => {
this.handleError(error);
});
},
getItem: function (url) {
api.get(url)
.then((response) => {
this.item = response.data;
})
.catch((error) => {
this.handleError(error);
});
},
handleError: function (error) {
window.alert(error);
},
};
}
Give plugin to Pinia:
const pinia = createPinia();
pinia.use(BaseStorePlugin);
Example movieStore.js (using shared action & state)
import { defineStore } from 'pinia';
import { api } from 'src/boot/axios';
export const useMovieStore = defineStore({
id: 'movie',
state: () => ({
movieSpecificStateObject: {},
}),
actions: {
movieSpecificAction (url) {
console.log(this.item);
api.get(url)
.then((response) => {
// handle response
})
.catch((error) => {
this.handleError(error);
});
},
},
});
Example usage in component
<template>
<div
v-for="movie in movieStore.collection"
:key="movie.id"
>
<div>
{{ movie.name }}
</div>
</div>
</template>
<script setup>
import { onMounted } from 'vue';
import { useMovieStore } from 'src/stores/movieStore.js';
const movieStore = useMovieStore();
onMounted(() => {
movieStore.readCollection('http://url.com/movies');
});
</script>
Edit: 1
if you pass the context into the plugin you have access to the store and options being passed into it, from this you could check the store id and only return for specific stores like below
function BaseStorePlugin (context) {
const allowedStores = ['movie', 'album'];
if (allowedStores.includes(context.store.$id)) {
return {
collection: [],
getCollection: function () {
const fakeCollection = Array.from({length: 10}, () => Math.floor(Math.random() * 40));
fakeCollection.forEach((item) => {
this.collection.push({
id: item,
name: `name${item}`
});
});
},
};
};
}
I have created a very basic example using 3 stores and the above check available on codesandbox here
Lets say we injected this repository on a plugin/service-container.js
import nodeFetch from 'node-fetch'
import { AbortController as NodeAbortController } from 'node-abort-controller'
import HttpClient from '#/services/httpClient'
import PostRepository from '#/repositories/posts'
export default ({ app }, inject) => {
if (!process.client || app.context.env.NUXTJS_DEPLOY_TARGET === 'server') {
inject('postRepository', postRepository)
}
}
I have always acceded to API repositories from the asyncData method, like so:
export default {
async asyncData ({ $postRepository, }) {
const posts = await $postRepository.getAllPaginated(page, 11)
return {
posts,
}
}
}
But I need to access to it in a method, this is actually working but:
I doesn't look the right way because i'm caching in the component's data()
It fires this lint error:
Async method 'asyncData' has no 'await' expression.eslintrequire-await
What's the right way? I Can't find it online (the only examples I found involved using the Store)
export default {
async asyncData ({ $postRepository }) {
this.$postRepository = $postRepository
},
methods: {
async loadMore () {
if (this.page < this.posts.numPages) {
const posts = await this.$postRepository.getAllPaginated(this.page + 1, 11)
}
}
}
}
The error is coming from here
async asyncData ({ $postRepository }) {
this.$postRepository = [missing await here] $postRepository
},
From the documentation
This hook can only be used for page-level components. Unlike fetch, asyncData cannot access the component instance (this). Instead, it receives the context as its argument. You can use it to fetch some data and Nuxt will automatically shallow merge the returned object with the component data.
Hence, you cannot use any kind of this.loadMore in asyncData because it doesn't have access to the instance yet. So, inject is indeed the proper way of doing things.
With a plugin like that
export default ({ _ }, inject) => {
inject('customTest', async () => {
const response = await fetch('https://jsonplaceholder.typicode.com/todos/1')
return await response.json()
})
}
And a page like this
<template>
<div>
<pre>item: {{ item }}</pre>
</div>
</template>
<script>
export default {
async asyncData({ $customTest }) {
const item = await $customTest()
return { item }
},
}
</script>
It is not calling a method but you could totally use this.$nuxt.refresh() to fetch it again and increment the index of the repository call after an update in the store.
Which could be referenced like
await fetch(`https://jsonplaceholder.typicode.com/todos/${indexFromVuex}`)
You could of course keep it local too
<template>
<div>
<pre>item: {{ item }}</pre>
<button #click="fetchNewItem">fetch new item</button>
</div>
</template>
<script>
export default {
async asyncData({ $customTest }) {
const item = await $customTest()
return { item }
},
data() {
return {
index: 1,
}
},
methods: {
async fetchNewItem() {
this.index += 1
this.item = await this.$customTest(this.index)
},
},
}
</script>
So yeah, I don't think that there are other possible approaches with asyncData.
The fetch() hook is a bit more flexible but it's also totally different too regarding how it is working.
Anyway, with those 2 approaches you could totally have enough to solve the issue of your HTTP call.
It seems that an injected dependency can be accessed (in this case) with simply this.$postRepository inside any method so I didn't even need that asyncData
I use #nuxtjs/composition-api(0.15.1), but I faced some problems about accessing Vuex getters in computed().
This is my code in composition API:
import { computed, useContext, useFetch, reactive } from '#nuxtjs/composition-api';
setup() {
const { store } = useContext();
const products = computed(() => {
return store.getters['products/pageProducts'];
});
const pagination = computed(() => {
return store.getters['products/pagination'];
});
useFetch(() => {
if (!process.server) {
store.dispatch('products/getPage');
}
});
return {
products,
pagination,
};
}
And the console keeps reporting the warning:
[Vue warn]: Write operation failed: computed value is readonly.
found in
---> <Pages/products/Cat.vue> at pages/products/_cat.vue
<Nuxt>
<Layouts/default.vue> at layouts/default.vue
<Root>
I'm really confused. Because I didn't try to mutate the computed property, just fetching the Data with the AJAX and then simply assign the data to the state in the Vuex mutations.
But I rewrite the code in option API in this way:
export default {
components: {
ProductCard,
Pagination,
},
async fetch() {
if (process.server) {
await this.$store.dispatch('products/getPage');
}
},
computed: {
products() {
return this.$store.getters['products/pageProducts'];
},
pagination() {
return this.$store.getters['products/pagination'];
},
},
};
Everything works fine, there's no any errors or warnings. Is it the way I'm wrongly accessing the getters in the composition API or that's just a bug with the #nuxtjs/composition-api plugin?
fix: computed property hydration doesn't work with useFetch #207
This problem might not can be solved until the Nuxt3 come out.
But I found an alternative solution which use the middleware() instead of use useFetch(), if you want to the prevent this bug by fetching AJAX data with Vuex Actions and then retrieve it by Getters via the computed().
I make another clearer example which it's the same context like the question above.
~/pages/index.vue :
<script>
import { computed, onMounted, useContext, useFetch } from '#nuxtjs/composition-api';
export default {
async middleware({ store }) {
await store.dispatch('getUser');
},
setup() {
const { store } = useContext();
const user = computed(() => store.getters.user);
return {
user,
};
},
}
</script>
~/store/index.js (Vuex)
const state = () => ({
user: {},
});
const actions = {
async getUser({ commit }) {
const { data } = await this.$axios.get('https://randomuser.me/api/');
console.log(data.results[0]);
commit('SET_USER', data.results[0]);
},
};
const mutations = {
SET_USER(state, user) {
state.user = user;
},
};
const getters = {
user(state) {
return state.user;
},
};
If there's something wrong in my answer, please feel free to give your comments.
I'm trying to experiment on displaying data using VueX and a free API from rapidapi. Somehow I can't display or iterate through it properly in the component.
The console displays the objects correctly, but the component that's supposed to display it does not.
What am I doing wrong?
Here are the relevant codes:
store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex);
export default new Vuex.Store({
state: {
worldData:
fetch("https://covid-193.p.rapidapi.com/statistics", {
method: "GET",
headers: {
"x-rapidapi-host": "covid-193.p.rapidapi.com",
"x-rapidapi-key": "mySecretKey"
}
})
.then(response => response.json())
.then(data => {
data.response.sort((a, b) => (a.country > b.country ? 1 : -1));
console.log(data.response);
return data.response;
})
},
getters: {
worldData: state => state.worldData,
},
mutations: {
},
actions: {
},
modules: {
}
})
components/mycomponent.vue
<template>
<div >
<div v-for="myData in $store.getters.worldData" :key="myData">{{myData}}</div>
</div>
</template>
When you create a store, the state property is for initial / default values. You are currently setting yours to a Promise which is probably not what you want.
Performing asynchronous tasks should be done via actions and the results committed through mutations.
const store = new Vuex.Store({
state: {
worldData: [] // initial value
},
getters: {
worldData: state => state.worldData
},
mutations: {
setWorldData: (state, worldData) => state.worldData = worldData
},
actions: {
loadWorldData: async ({ commit }) => {
// load the data via fetch
const res = await fetch('https://covid-193.p.rapidapi.com/statistics', {
headers: {
"x-rapidapi-host": "covid-193.p.rapidapi.com",
"x-rapidapi-key": "mySecretKey"
}
})
// check for a successful response
if (!res.ok) {
throw res
}
// parse the JSON response
const worldData = (await res.json()).response
// commit the new value via the "setWorldData" mutation
commit('setWorldData', worldData.sort((a, b) => a.country.localeCompare(b.country)))
}
}
})
store.dispatch('loadWorldData') // dispatch the action to load async data
export default store
You can execute the dispatch anywhere at any time to load / reload the data.
I've several components using vue-tables-2 but one of them is not updating the table until I change the route.
component
<template>
//..
<div class="table-responsive" >
<v-client-table ref="table" name="vCardTable"
:data="vCardTableData.data"
:columns="vCardTableData.headers"
:options="vCardTableData.options"/>
</div>
//..
</template>
<script>
import { mapState } from "vuex";
import { mapGetters } from "vuex";
export default {
name: "VCard",
computed: {
...mapState("commons", ["user"]),
...mapGetters({ vCardTableData: "vCard/vCardTableData" })
},
mounted() {
var self = this;
self.$nextTick(() => {
self.$store.dispatch("vCard/getVCards"); <-- GET TABLE DATA
});
}
};
</script>
store
const state = {
vCardTableData: {
data: [],
headers: [
//..
],
options: {
filterable: false,
preserveState: false,
headings: {
//..
},
texts: {
//..
},
pagination: {
dropdown: true,
},
templates: {
//..
},
},
}
}
const getters = {
vCardTableData: state => state.vCardTableData
}
const actions = {
getVCards({commit, dispatch}) {
return api.request("get", "getvcards").then(response => {
setTimeout(() => {
commit("setVCardTableData", response.data.vcards);
}, 300);
}).catch(error => {
console.log(error);
});
}
}
const mutations = {
clearTableData: (state) => {
if (state.vCardTableData.data) {
state.vCardTableData.data = [];
}
},
setVCardTableData : (state, vCardTableData) => state.vCardTableData.data = vCardTableData
}
As you can see in this image the table has data:
But the view is refreshed when the route changes:
02/05/2018
Well now I've seen that if I modify the state directly in the component with promises it works:
this.$store.dispatch("vCard/getVCards", []).then((responseData)=>{
this.$store.state.vCard.vCardTableData.data = responseData;
});
Does anyone know why?
Thank you
My last answer was wrong, I did not remember that I had changed the vuex parameter of the table to false. I don't know why but doing a push it works:
setVCardTableData : (state, vCardTableData) => {
vCardTableData.forEach(tableData => {
state.vCardTableData.data.push(tableData);
});
}
This is a probably a reactivity issue. (See https://v2.vuejs.org/v2/guide/reactivity.html#Change-Detection-Caveats for detailed explanation.)
Changing how you set the object value in your mutation to this should solve the problem
setVCardTableData: (state, vCardTableData) => state.vCardTableData = {
...state.vCardTableData,
data: vCardTableData
}
Basically, this creates a new object so that Vue knows that the object has been updated. In Javasript, object is passed by reference, meaning that vCardTableData don't store the object, it stores the reference to the object. You could think of it as a pointer/address that points to the object in memory. When you change a child property in the object, the reference remains unchanged, so Vue does not know that the object has been updated. Creating a new object makes sure that the object reference is updated.
This is also explained in Mutations Follow Vue's Reactivity Rules
in https://vuex.vuejs.org/en/mutations.html
I have had similar issues. Like others have already mention it is probably a reactivity problem. You can use Vue.set() to ensure that your properties are reactive when setting the state values.
setVCardTableData: (state, vCardTableData) => {
Vue.set(state.vCardTableData, 'data', vCardTableData);
}
Check the official documentation for this method.
Since it's a deep object, you need to use Object.assign in your mutation
setVCardTableData: (state, vCardTableData) => Object.assign(state.vCardTableData.data, vCardTableData)