UPDATE
CodeSandbox
What i try to do:
i am working on a permissionerMixin which should handle a websites permission for authenticated users.
for example if a logged in user has no permission to see a specific part of the website i handle the components with a v-if="allowedToSee". So far so good. i store the whole user permission object in my vuex store.
the data came from a rest api and looks like this:
const rights = [
{
name: 'findMe1',
value: false,
},
{
name: 'findMe2',
value: false,
},
{
name: 'findMe3',
value: false,
},
{
name: 'findMe4',
value: false,
}
]
now back to my mixin and and how i load the data from the api:
import axios from 'axios';
export const permissionerMixin = {
methods: {
async getRightsFromActiveUser() {
axios.get(`/not/the/real/path/${this.$store.state.User.activeUser.id}`)
.then((response) => {
return this.$store.commit('addActiveUserRights', response.data);
})
.catch((error) => {
console.log(error.response);
});
},
async permissionFor(rightName) {
const rights = await this.getRightsFromActiveUser();
for (const right of rights) {
if (right.name == rightName) {
return right.value;
}
}
}
}
}
as u can see i have two functions which work together.
getRightsFromActiveUser() is simply a getter for the right object i mentioned at the beginning.
it takes the actual data and puts it in the vuex store with a mutation:
const state = {
activeUser: {
id: 0,
permissions: {}
}
};
const getters = {};
const actions = {};
const mutations = {
addActiveUserId (state, id) {
state.activeUser.id = id;
},
addActiveUserRights (state, rights) {
state.activeUser.permissions = rights;
}
};
export default {
state,
getters,
actions,
mutations,
};
right after this we have the actual init function permissionFor(rightName) which should do the magic and should give me a boolean return value to handle the permissionings.
the one big problem now is that i instead of getting a boolean return, i get a [object Promise], thats because i am stupid and i don't get that promise thing in my head.
at the end i simply want to add this function to a vue component with an
v-if="permissionFor('whatEver')" to solve the permission handling.
pulled the return into it's own statement following the commit. Not sure what your response object looks like from the back end but this look a cleaner to me personally and able to be read later. Check it out and see how things change, if at all.
import axios from 'axios';
export const permissionerMixin = {
methods: {
async getRightsFromActiveUser() {
try {
let returnData = await axios.get(`/not/the/real/path/${this.$store.state.User.activeUser.id}`)
this.$store.commit('addActiveUserRights', response.data);
return returnData.data
} catch (error) {
console.log(error.response);
}
},
async permissionFor(rightName) {
try {
const rights = await this.getRightsFromActiveUser();
for (const right of rights) {
if (right.name == rightName) {
return right.value;
}
}
} catch(error){
console.log(error.response);
}
}
}
}
Related
I am using react-query in my TS project:
useOrderItemsForCardsInList.ts:
import { getToken } from '../../tokens/getToken';
import { basePath } from '../../config/basePath';
import { getTokenAuthHeaders } from '../../functions/sharedHeaders';
import { useQuery } from 'react-query';
async function getOrderItemsForCardsInList(listID: string) {
const token = await getToken();
const response = await fetch(`${basePath}/lists/${listID}/order_items/`, {
method: 'GET',
headers: getTokenAuthHeaders(token)
});
return response.json();
}
export default function useOrderItemsForCardsInList(listID: string) {
if (listID != null) {
return useQuery(['list', listID], () => {
return getOrderItemsForCardsInList(listID);
});
}
}
I use my query result over here:
import { useCardsForList } from '../../hooks/Cards/useCardsForList';
import useOrderItemsForCardsInList from '../../hooks/Lists/useOrderItemsForCardsInList';
import usePaginateCardsInList from '../../hooks/Cards/usePaginateCardsInList';
export default function CardsListFetch({ listID }: { listID: string }) {
const { isLoading, isError, error, data } = useCardsForList(listID);
const { orderItems } = useOrderItemsForCardsInList(listID);
const pagesArray = usePaginateCardsInList(orderItems, data);
return (
...
);
}
However, on my const { orderItems } = useOrderItemsForCardsInList(listID); line, I get the following error:
Property 'orderItems' does not exist on type 'UseQueryResult<any, unknown> | undefined'.
How can I resolve this? I don't really know how to consume the result of my query on Typescript, any help is be appreciated
The property on useQuery that you need to consume where you find your data is called data, so it should be:
const { data } = useOrderItemsForCardsInList(listID);
if that data has a property called orderItems, you can access it from there.
However, two things I'm seeing in your code:
a conditional hook call of useQuery (which is forbidden in React)
your queryFn returns any because fetch is untyped, so even though you are using TypeScript, you won't get any typesafety that way.
If you are using React with react-query, I suggest you install this devDependency called: "#types/react-query". When using VS Code or any other smart text editor that will help, because it will help you with type suggestions.
npm i --save-dev #types/react-query
Then go to your code and fix couple things:
remove condition from useOrderItemsForCardsInList(). Don’t call React Hooks inside loops, conditions, or nested functions. See React hooks rules.
import UseCategoryResult and define interface for your return object. You can call it OrderItemsResult or similar. Add type OrderType with the fields of the order object or just use orderItems: any for now.
Add return type UseQueryResult<OrderItemsResult> to useOrderItemsForCardsInList() function.
Fix return value of getOrderItemsForCardsInList(), it should not be response.json() because that would be Promise, not actual data. Instead use await response.json().
So your function useOrderItemsForCardsInList() will return UseQueryResult which has properties like isLoading, error and data. In your second code snipper, you already use data in one place, so instead rename data to orderData and make sure you define default orderItems to empty array to avoid issues: data: orderData = {orderItems: []}
useOrderItemsForCardsInList.ts:
import { getToken } from '../../tokens/getToken';
import { basePath } from '../../config/basePath';
import { getTokenAuthHeaders } from '../../functions/sharedHeaders';
import { useQuery, UseCategoryResult } from 'react-query';
type OrderType = {
id: string;
name: string;
// whatever fields you have.
}
interface OrderItemsResult {
orderItems: OrderType[],
}
async function getOrderItemsForCardsInList(listID: string) {
const token = await getToken();
const response = await fetch(`${basePath}/lists/${listID}/order_items/`, {
method: 'GET',
headers: getTokenAuthHeaders(token)
});
const data = await response.json();
return data;
}
export default function useOrderItemsForCardsInList(listID: string): UseQueryResult<OrderItemsResult> {
return useQuery(['list', listID], () => {
return getOrderItemsForCardsInList(listID);
});
}
Use your query result:
import { useCardsForList } from '../../hooks/Cards/useCardsForList';
import useOrderItemsForCardsInList from '../../hooks/Lists/useOrderItemsForCardsInList';
import usePaginateCardsInList from '../../hooks/Cards/usePaginateCardsInList';
export default function CardsListFetch({ listID }: { listID: string }) {
const { isLoading, isError, error, data } = useCardsForList(listID);
const { data: orderData = { orderItems: []}} = useOrderItemsForCardsInList(listID);
const pagesArray = usePaginateCardsInList(orderItems, data);
return (
...
);
}
Since Nuxt's fetch hooks cannot run in parallel, I needed a way to cancel requests done in fetch hook when navigating to some other route so users don't have to wait for the first fetch to complete when landed on the homepage navigated to some other. So I found this approach: How to cancel all Axios requests on route change
So I've created these plugin files for Next:
router.js
export default ({ app, store }) => {
// Every time the route changes (fired on initialization too)
app.router.beforeEach((to, from, next) => {
store.dispatch('cancel/cancel_pending_requests')
next()
})
}
axios.js
export default function ({ $axios, redirect, store }) {
$axios.onRequest((config) => {
const source = $axios.CancelToken.source()
config.cancelToken = source.token
store.commit('cancel/ADD_CANCEL_TOKEN', source)
return config
}, function (error) {
return Promise.reject(error)
})
}
and a small vuex store for the cancel tokens:
export const state = () => ({
cancelTokens: []
})
export const mutations = {
ADD_CANCEL_TOKEN (state, token) {
state.cancelTokens.push(token)
},
CLEAR_CANCEL_TOKENS (state) {
state.cancelTokens = []
}
}
export const actions = {
cancel_pending_requests ({ state, commit }) {
state.cancelTokens.forEach((request, i) => {
if (request.cancel) {
request.cancel('Request canceled')
}
})
commit('CLEAR_CANCEL_TOKENS')
}
}
Now this approach works fine and I can see requests get canceled with 499 on route change, however, it is flooding my devtools console with "Error in fetch()" error. Is there some preferred/better way to do this?
Example of fetch hook here:
async fetch () {
await this.$store.dispatch('runs/getRunsOverview')
}
Example of dispatched action:
export const actions = {
async getRunsOverview ({ commit }) {
const data = await this.$axios.$get('api/frontend/runs')
commit('SET_RUNS', data)
}
}
Edit: I forgot to mention that I'm using fetch here with fetchOnServer set to False to display some loading placeholder to users.
The main problem is the flooded console with error, but I can also see that it also enters the $fetchState.error branch in my template, which displays div with "Something went wrong" text before route switches.
Edit 2:
Looked closer where this error comes from and it's mixin file fetch.client.js in .nuxt/mixins directory. Pasting the fetch function code below:
async function $_fetch() {
this.$nuxt.nbFetching++
this.$fetchState.pending = true
this.$fetchState.error = null
this._hydrated = false
let error = null
const startTime = Date.now()
try {
await this.$options.fetch.call(this)
} catch (err) {
if (process.dev) {
console.error('Error in fetch():', err)
}
error = normalizeError(err)
}
const delayLeft = this._fetchDelay - (Date.now() - startTime)
if (delayLeft > 0) {
await new Promise(resolve => setTimeout(resolve, delayLeft))
}
this.$fetchState.error = error
this.$fetchState.pending = false
this.$fetchState.timestamp = Date.now()
this.$nextTick(() => this.$nuxt.nbFetching--)
}
Have also tried to have everything using async/await as #kissu suggested in comments but with no luck :/
I am getting the error below, when I use getServerSideProps function to retrieve data from Binance API.
import binance from "../config/binance-config";
export async function getServerSideProps() {
const res = await binance.balance((error, balances) => {
console.info("BTC balance: ", balances.BTC.available);
});
return {
props: {
res,
},
};
}
import Binance from "node-binance-api"
const binance = new Binance().options({
APIKEY: 'xxx',
APISECRET: 'xxx'
});
export default binance;
Error output:
Error: Error serializing `.res` returned from `getServerSideProps` in "/dashboard".
Reason: `undefined` cannot be serialized as JSON. Please use `null` or omit this value.
I'm not sure how to resolve this error. I would just like to be able to mine (and display) the response by sending it as props in another component.
Thank you!
Here is how I solved it in NextJs
// Get Data from Database
export async function getServerSideProps(ctx) {
const { params } = ctx;
const { slug } = params;
await dbConnect.connect();
const member = await Member.findOne({ slug }).lean();
await dbConnect.disconnect();
return {
props: {
member: JSON.parse(JSON.stringify(member)), // <== here is a solution
},
};
}
Convert your data into json format when you are fetching it through an Api,
export async function getServerSideProps(context) {
const res = await fetch(`https://.../data`)
const data = await res.json()
if (!data) {
return {
redirect: {
destination: '/',
permanent: false,
},
}
}`enter code here`
return {
props: {}, // will be passed to the page component as props
}
}
You can read more detail on this link, https://nextjs.org/docs/basic-features/data-fetching#getserversideprops-server-side-rendering
put res from API in Curly Brackets
const { res } = await binance.balance((error, balances) => {
console.info("BTC balance: ", balances.BTC.available);
});
return {
props: {
res,
},
};
This is actually a simple error. The props that are being returned from getServerSideProps must be wrapped in curly brackets as shown below:
return {props: {res}}
This will clear the serialization error provided no nulls are being returned in response
CONTEXT
I have two store modules : "Meetings" and "Demands".
Within store "Demands" I have "getDemands" action, and within store "Meetings" I have "getMeetings" action. Prior to access meetings's data in Firestore, I need to know demands's Id (ex.: demands[i].id), so "getDemands" action must run and complete before "getMeetings" is dispatched.
Vuex documentation dispatching-action is very complete, but still, I don't see how to fit it in my code. There are also somme other good answered questions on the topic here :
Vue - call async action only after first one has finished
Call an action from within another action
I would like to know the best way to implement what I'm trying to accomplish. From my perspective this could be done by triggering one action from another, or using async / await, but I'm having trouble implementing it.
dashboard.vue
computed: {
demands() {
return this.$store.state.demands.demands;
},
meetings() {
return this.$store.state.meetings.meetings;
}
},
created() {
this.$store.dispatch("demands/getDemands");
//this.$store.dispatch("meetings/getMeetings"); Try A : Didn't work, seems like "getMeetings" must be called once "getDemands" is completed
},
VUEX store
Module A – demands.js
export default {
namespaced: true,
state: {
demands:[], //demands is an array of objects
},
actions: {
// Get demands from firestore UPDATED
async getDemands({ rootState, commit, dispatch }) {
const { uid } = rootState.auth.user
if (!uid) return Promise.reject('User is not logged in!')
const userRef = db.collection('profiles').doc(uid)
db.collection('demands')
.where('toUser', "==", userRef)
.get()
.then(async snapshot => {
const demands = await Promise.all(
snapshot.docs.map(doc =>
extractDataFromDemand({ id: doc.id, demand: doc.data() })
)
)
commit('setDemands', { resource: 'demands', demands })
console.log(demands) //SECOND LOG
})
await dispatch("meetings/getMeetings", null, { root: true }) //UPDATE
},
...
mutations: {
setDemands(state, { resource, demands }) {
state[resource] = demands
},
...
Module B – meetings.js
export default {
namespaced: true,
state: {
meetings:[],
},
actions: {
// Get meeting from firestore UPDATED
getMeetings({ rootState, commit }) {
const { uid } = rootState.auth.user
if (!uid) return Promise.reject('User is not logged in!')
const userRef = db.collection('profiles').doc(uid)
const meetings = []
db.collection('demands')
.where('toUser', "==", userRef)
.get()
.then(async snapshot => {
await snapshot.forEach((document) => {
document.ref.collection("meetings").get()
.then(async snapshot => {
await snapshot.forEach((document) => {
console.log(document.id, " => ", document.data()) //LOG 3, 4
meetings.push(document.data())
})
})
})
})
console.log(meetings) // FIRST LOG
commit('setMeetings', { resource: 'meetings', meetings })
},
...
mutations: {
setMeetings(state, { resource, meetings }) {
state[resource] = meetings
},
...
Syntax:
dispatch(type: string, payload?: any, options?: Object): Promise<any
Make the call right
dispatch("meetings/getMeetings", null, {root:true})
Before I was getting movie detail from the component's script. The function first check whether the movie ID of the store is same as of the route's param movie ID. If its same then don't get the movie from the server API, or else get the movie from the server API.
It was working fine. But now I am trying to get the movie details from the store's mutation. However I am getting error
Uncaught TypeError: Cannot read property '$route' of undefined
How to use vue-router ($route) to access the params and vue-resource ($http) to get from the server API in vuex store?
store.js:
export default new Vuex.Store({
state: {
movieDetail: {},
},
mutations: {
checkMovieStore(state) {
const routerMovieId = this.$route.params.movieId;
const storeMovieId = state.movieDetail.movie_id;
if (routerMovieId != storeMovieId) {
let url = "http://dev.site.com/api/movies/movie-list/" + routerMovieId + "/";
this.$http.get(url)
.then((response) => {
state.movieDetail = response.data;
})
.catch((response) => {
console.log(response)
});
}
},
},
});
component script:
export default {
computed: {
movie() {
return this.$store.state.movieDetail;
}
},
created: function () {
this.$store.commit('checkMovieStore');
},
}
To use $http or $router in your vuex store, you would need to use the main vue instance. Although I don't recommend using this, I'll add what I recommend after answering the actual question.
In your main.js or wherever you are creating your vue instance like:
new Vue({
el: '#app',
router,
store,
template: '<App><App/>',
components: {
App
}
})
or something similar, you might also have added the vue-router and vue-resource plugins too.
Doing a slight modification to this:
export default new Vue({
el: '#app',
router,
store,
template: '<App><App/>',
components: {
App
}
})
I can now import it in vuex stores like so:
//vuex store:
import YourVueInstance from 'path/to/main'
checkMovieStore(state) {
const routerMovieId = YourVueInstance.$route.params.movieId;
const storeMovieId = state.movieDetail.movie_id;
if (routerMovieId != storeMovieId) {
let url = "http://dev.site.com/api/movies/movie-list/" + routerMovieId + "/";
YourVueInstance.$http.get(url)
.then((response) => {
state.movieDetail = response.data;
})
.catch((response) => {
console.log(response)
});
}
}
and as the answer by Austio goes, this method should be an action as mutations are not designed to handle async.
Now coming to the recommended way of doing it.
Your component can access the route params and provide it to the action.
methods: {
...mapActions({
doSomethingPls: ACTION_NAME
}),
getMyData () {
this.doSomethingPls({id: this.$route.params})
}
}
The action then makes the call through an abstracted API service file (read plugins)
[ACTION_NAME]: ({commit}, payload) {
serviceWhichMakesApiCalls.someMethod(method='GET', payload)
.then(data => {
// Do something with data
})
.catch(err => {
// handle the errors
})
}
Your actions do some async job and provide the result to a mutation .
serviceWhichMakesApiCalls.someMethod(method='GET', payload)
.then(data => {
// Do something with data
commit(SOME_MUTATION, data)
})
.catch(err => {
// handle the errors
})
Mutations should be the only ones to modify your state.
[SOME_MUTATION]: (state, payload) {
state[yourProperty] = payload
}
Example
A file which contains a list of endpoints, you might need it if you have different stages of deployment which have different api endpoints like: test, staging, production, etc.
export const ENDPOINTS = {
TEST: {
URL: 'https://jsonplaceholder.typicode.com/posts/1',
METHOD: 'get'
}
}
And the main file which implements Vue.http as a service:
import Vue from 'vue'
import { ENDPOINTS } from './endpoints/'
import { queryAdder } from './endpoints/helper'
/**
* - ENDPOINTS is an object containing api endpoints for different stages.
* - Use the ENDPOINTS.<NAME>.URL : to get the url for making the requests.
* - Use the ENDPOINTS.<NAME>.METHOD : to get the method for making the requests.
* - A promise is returned BUT all the required processing must happen here,
* the calling component must directly be able to use the 'error' or 'response'.
*/
function transformRequest (ENDPOINT, query, data) {
return (ENDPOINT.METHOD === 'get')
? Vue.http[ENDPOINT.METHOD](queryAdder(ENDPOINT.URL, query))
: Vue.http[ENDPOINT.METHOD](queryAdder(ENDPOINT.URL, query), data)
}
function callEndpoint (ENDPOINT, data = null, query = null) {
return new Promise((resolve, reject) => {
transformRequest(ENDPOINT, query, data)
.then(response => { return response.json() })
.then(data => { resolve(data) })
.catch(error => { reject(error) })
})
}
export const APIService = {
test () { return callEndpoint(ENDPOINTS.TEST) },
login (data) { return callEndpoint(ENDPOINTS.LOGIN, data) }
}
The queryAdder in case it is important, I was using this to add params to the url.
export function queryAdder (url, params) {
if (params && typeof params === 'object' && !Array.isArray(params)) {
let keys = Object.keys(params)
if (keys.length > 0) {
url += `${url}?`
for (let [key, i] in keys) {
if (keys.length - 1 !== i) {
url += `${url}${key}=${params[key]}&`
} else {
url += `${url}${key}=${params[key]}`
}
}
}
}
return url
}
So a few things, the $store and $route are properties of the Vue instance, which is why accessing them inside of Vuex instance is not working. Also, mutations are synchonous what you need are actions
Mutations => A function that given state and some arguments mutates the state
Action => Do async things like http calls and then commit results to a mutation
So create an action that dispatches the http. Keep in mind this is pseudocode.
//action in store
checkMovieStore(store, id) {
return $http(id)
.then(response => store.commit({ type: 'movieUpdate', payload: response })
}
//mutation in store
movieUpdate(state, payload) {
//actually set the state here
Vue.set(state.payload, payload)
}
// created function in component
created: function () {
return this.$store.dispatch('checkMovieStore', this.$route.params.id);
},
Now your created function dispatches the checkMovieStore action with the id, which does the http call, once that is complete it updates the store with the value.
In your vuex store:
import Vue from 'vue'
Vue.http.post('url',{})
Not like in normal vue components:
this.$http.post(...)
I highly recommend importing axios on the vuex module (store and submodules), and using it for your http requests
To access the vue instance in the store use this._vm.
But as Amresh advised do not use things like $router in vuex