I am fetching som data from my backend and iterate over a component, that uses that data in the child component. The name of each does always render though the image only renderes sometimes and somestimes not. I'm guessing the reason behind this is the fact that Vue iterates over the data before it's all fetched.
onMounted(() => {
const getGradients = async () => {
const {data: usersLovedGradients } = await supabase.from("users").select().eq('username', user.value.username).single()
lovedArr = usersLovedGradients.loved
const {data: progradients } = await supabase.from("progradients").select("gradient_id")
const arrGradients = []
progradients.forEach( async (progradient, index) => {
if (lovedArr.includes(progradient.gradient_id)) {
const {data: gradients } = await supabase.from("progradients").select().eq('gradient_id', progradient.gradient_id).single()
arrGradients.push(gradients)
}
if (lovedArr.length === arrGradients.length) {
lovedGradients.value = arrGradients
}
})
}
getGradients()
});
THIS IS HOW I AM ITERATIONG OVER THE COMPONENT
<div class="cards-container">
<GradientCard
v-for="(lovedGradient, index) in lovedGradients"
:expertGradient="lovedGradient"
:index="index"
:id="lovedGradient.gradient_id"
/>
</div>
Is there any way to not mount/render the cards before all data has been retrieved? or is this caused by something else?
Related
I am building a project with Nuxt and I need to know the size of the wrapper to adjust the grid setting
(I want a single line, I could still do this in pure CSS probably by hiding the items)
It's my first time using composition API & script setup
<script setup>
const props = defineProps({
name: String
})
const width = ref(0)
const wrapper = ref(null)
const maxColumns = computed(() => {
if (width.value < 800) return 3
if (width.value < 1000) return 4
return 5
})
onMounted(() => {
width.value = wrapper.value.clientWidth
window.onresize = () => {
width.value = wrapper.value.clientWidth
console.log(width.value);
};
})
</script>
<template>
<div class="category-preview" ref="wrapper">
...
</div>
</template>
The console log is working properly, resizing the window and refreshing the page will return 3, 4 or 5 depending on the size, but resizing won't trigger the computed value to change
What am I missing ?
In my test enviroment I had to rename your ref 'width' into something else. After that it did worked for me with a different approach using an event listener for resize events.
You can do something like this:
<script setup>
import { ref, onMounted, onUnmounted, computed } from 'vue'
const wrapperWidth = ref(0)
const wrapper = ref(null)
// init component
onMounted(() => {
getDimensions()
window.addEventListener('resize', debounce(() => getDimensions(), 250))
})
// remove event listener after destroying the component
onUnmounted(() => {
window.removeEventListener('resize', debounce)
})
// your computed property
const maxColumns = computed(() => {
if (wrapperWidth.value < 800) {
return 3
} else if (wrapperWidth.value < 1000) {
return 4
} else {
return 5
}
})
// get template ref dimensions
function getDimensions () {
const { width } = wrapper.value.getBoundingClientRect()
wrapperWidth.value = width
}
// wait to call getDimensions()
// it's just a function I have found on the web...
// there is no need to call getDimensions() after every pixel have changed
const debounce = (func, wait) => {
let timeout
return function executedFunction (...args) {
const later = () => {
timeout = null
func(...args)
}
clearTimeout(timeout)
timeout = setTimeout(later, wait)
}
}
</script>
<template>
<div ref="wrapper">
{{ maxColumns }} // will change after resize events
</div>
</template>
I'm currently trying out some of the latest vue version and features (3.2).
I've created a useFetch composable to reuse some logic (it's based on the vue documentation)
useFetch.js
import { ref } from 'vue'
import axios from 'axios';
const apiClient = axios.create({
baseURL: process.env.VUE_APP_API_ENDPOINT,
withCredentials: false,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
timeout: 10000,
});
export function useFetch(url) {
const data = ref(null);
const error = ref(null);
const isLoading = ref(true);
apiClient({ method, url, data: body })
.then((response) => (data.value = response.data))
.catch((err) => (error.value = err))
.finally(() => isLoading.value = false)
return { data, error }
}
I'm using the useFetch composable in a component to fetch companies from the backend. The data I'm getting back is rough so I want to reformat it using a computed (That was the way I did it when using vue 2)
CompanyList.vue
<script setup>
import { computed } from 'vue';
import useFetch from '#/composables/useFetch';
import { formatEmail, formatPhone, formatEnterpriseNumber } from '#/utils/formatters';
const { data, isLoading, error } = useFetch('get', '/companies');
const companies = computed(() =>
data.companies?.map((company) => ({
id: `#${company.id}`,
name: `${company.legal_entity_type} ${company.business_name}`,
enterprise_number: formatEnterpriseNumber(company.enterprise_number),
email: formatEmail(company.email),
phone: formatPhone(company.phone),
}))
);
</script>
<template>
<div v-if="isLoading">Loading...</div>
<div v-else-if="error">Oops! Error encountered: {{ error }}</div>
<div v-else-if="companies">
Companies:
<pre>{{ companies }}</pre>
</div>
<div v-else>No data :(</div>
</template>
When using Companies inside the template tags it stays null. I've checked and data has a companies property of the type Array with data in it.
Anyone an idea how to handle this?
I think the issue may be due to use of ref. Try using reactive for data instead of ref
export function useFetch(url) {
const data = reactive(null);
const error = ref(null);
const isLoading = ref(true);
apiClient({ method, url, data: body })
.then((response) => (data = response.data))
.catch((err) => (error.value = err))
.finally(() => isLoading.value = false)
return { data, error }
}
To use ref, you would need to access the value via ref.value. Also, ref is not the best choice for objects and arrays as it was meant for primitive data types. to use ref you can
const companies = computed(() =>
data.value?.companies?.map((company) => ({
id: `#${company.id}`,
name: `${company.legal_entity_type} ${company.business_name}`,
enterprise_number: formatEnterpriseNumber(company.enterprise_number),
email: formatEmail(company.email),
phone: formatPhone(company.phone),
}))
);
note the use of ?. after value, which is required since the ref is null initially.
I saw in one lesson that we can create with composition api hook usePromise but the problem that I have simple crud app with to-do list, where I have create, delete, get API calls and I don't understand how I can use this hook for all api in one component. All call works correct but the loading is not, it works only at first call PostService.getAll() and then loader isn't triggered. Thanks for response.
usePromise.js
import { ref } from 'vue';
export default function usePromise(fn) {
const results = ref(null);
const error = ref(null);
const loading = ref(false);
const createPromise = async (...args) => {
loading.value = true;
error.value = null;
results.value = null;
try {
results.value = await fn(...args);
} catch (err) {
error.value = err;
} finally {
loading.value = false;
}
};
return { results, loading, error, createPromise };
}
apiClient.js
import axios from 'axios';
export default axios.create({
baseURL: 'https://jsonplaceholder.typicode.com/',
withCredentials: false,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
});
PostService.js
import apiClient from './apiClient';
const urlPath = '/posts';
export default {
getAll() {
return apiClient.get(urlPath);
},
add(post) {
return apiClient.post(urlPath, post);
},
delete(id) {
return apiClient.delete(`${urlPath}/${id}`);
},
};
List.vue
<template>
<div>
<VLoader v-if="loading" />
<template v-else>
<table class="table">
<thead>
<tr>
<th>Id</th>
<th>Title</th>
<th></th>
</tr>
</thead>
<tbody>
<tr v-for="post in posts" :key="post.id">
<td>{{ post.id }}</td>
<td>{{ post.title }}</td>
<td>
<button class="btn btn-danger ml-1" #click="deletePost(post.id)">Delete</button>
</td>
</tr>
</tbody>
</table>
</template>
</div>
</template>
<script>
import { ref, computed, watch, unref } from 'vue';
import PostService from '#/services/PostService';
import usePromise from '#/use/usePromise';
export default {
setup() {
const posts = ref([]);
const post = ref({
title: '',
body: '',
});
const {
results: postsResultRef,
loading: postsLoadingRef,
createPromise: getAllPosts,
} = usePromise(() => PostService.getAll());
getAllPosts(); //get all posts by initialize component
const {
results: postDeleteResultRef,
loading: postDeleteLoadingRef,
createPromise: deletePost,
} = usePromise((id) => PostService.delete(id).then((result) => ({ ...result, removedId: id })));
watch(postsResultRef, (postsResult) => {
posts.value = postsResult.data;
});
watch(postDeleteResultRef, (postDeleteResult) => {
if (postDeleteResult.status === 200) {
posts.value = posts.value.filter((item) => item.id != postDeleteResult.removeId);
// unref(posts).splice(/* remove postDeleteResult.removedId */);
}
});
const loading = computed(() => [postsLoadingRef, postDeleteLoadingRef].map(unref).some(Boolean));
return { posts, post, loading };
},
};
</script>
A ref keeps reactive reference to a value that is supposed to exist through the entire component lifecycle. It stays reactive on other places of a component - a template, computed properties, watchers, etc.
Hooks like usePromise are supposed to be set up inside setup function (hence the name):
const { results, loading, createPromise } = usePromise(() => PostService.getAll()
For multiple requests, multiple hook results can be composed:
const posts = ref([]);
const { results: postsResultRef, loading: postsLoadingRef, createPromise: getAllPosts } = usePromise(() =>
PostService.getAll()
);
const { results: postDeleteResultRef, loading: postDeleteLoadingRef, createPromise: deletePost } = usePromise(id =>
PostService.delete(id).then(result => ({...result, removedId: id }))
);
...
watch(postsResultRef, postsResult => {
posts.value = postsResult.data
});
watch(postDeleteResultRef, postDeleteResult => {
if (postDeleteResult.status === 200)
unref(posts).splice(/* remove postDeleteResult.removedId */)
});
...
const loading = computed(() => [postsLoadingRef, postDeleteLoadingRef, ...].map(unref).some(Boolean))
getAllPosts, etc are supposed to be used as a callback, e.g. in a template, a promise it returns doesn't need to be handled explicitly and chained in general, as its current state is already reflected in hook results. This indicates a potential flaw in the hook, as createPromise arguments are unknown at the time when a result is available, this requires to provide a parameter explicitly for delete result.
The problem is only the first loading ref is returned from setup(). The others are hidden and unused inside each method.
One solution is to track the active loading ref in state, returned from setup():
Declare state.loading.
export default {
setup() {
const state = reactive({
//...
loading: null,
})
//...
}
}
Set state.loading to the loading ref within each method.
const fetchPosts = () => {
const { results, loading, createPromise } = usePromise(/*...*/)
state.loading = loading
//...
}
const deletePost = (id) => {
const { results, loading, createPromise } = usePromise(/*...*/)
state.loading = loading;
//...
}
const onSubmit = () => {
const { results, loading, createPromise } = usePromise(/*...*/)
state.loading = loading
//...
}
Remove the loading ref that was originally returned from setup(), since we already have state.loading, and toRefs(state) would expose loading to the template already:
export default {
setup() {
//...
//return { toRefs(state), loading }
// ^^^^^^^
return { toRefs(state) }
}
}
demo
I have a very simple component relying on two fetch calls :
<template>
<div>
<div ng-if="this.foo">
{{ foo.name }}
</div>
<div ng-if="this.bar">
{{ bar.address }}
</div>
</div>
</template>
<script>
export default {
name: 'identity-card',
data() {
return {
foo:undefined,
bar:undefined
}
}
created() {
Promise.all([
fetch('http://ul/to/api/foo'),
fetch('http://ul/to/api/bar')
]).then(async (response) => {
this.foo = await response[0].json();
this.bar = await response[1].json();
})
}
}
</script>
I'm trying to test that component with Jest.
While I found how to mock a Promise with Jest, I couldn't find a way to mock both fetch responses.
How can I do it without adding an external lib and without potentially refactoring my code?
You could set global.fetch to a jest.fn() that uses the mockReturnValueOnce() API for each expected fetch call:
const makeFetchResp = value => ({ json: async() => value }) // mock fetch().json()
const mockFetch = jest.fn()
.mockReturnValueOnce(makeFetchResp(true)) // 1st fetch() returns true
.mockReturnValueOnce(makeFetchResp(false)) // 2nd fetch() returns false
global.fetch = mockFetch
Before asserting any changes from the fetches, the test needs to flush their Promises for their then callbacks to be invoked. This can be done with:
const flushPromises = () => new Promise(r => setTimeout(r))
await flushPromises()
Your test would look similar to this:
it('fetches foo bar', async () => {
const makeFetchResponse = value => ({ json: async() => value })
const mockFetch = jest.fn()
.mockReturnValueOnce(makeFetchResponse(true))
.mockReturnValueOnce(makeFetchResponse(false))
global.fetch = mockFetch
const wrapper = shallowMount(MyComponent)
await flushPromises()
expect(mockFetch).toHaveBeenCalledTimes(2)
expect(wrapper.vm.foo).toBeTruthy()
expect(wrapper.vm.bar).toBeFalsy()
})
I want to make several API calls to get data into a component. I created a PostService.ts that looks like this:
const apiClient = axios.create({
baseURL: '/api/v1',
})
export default {
async getPosts() {
const { data }: { data: Post[] } = await apiClient.get('/posts')
// transform data ...
return data
},
async getTags() {
const { data }: { data: Tag[] } = await apiClient.get('/tags')
return data
},
async getComments() {
const { data }: { data: Comment[] } = await apiClient.get('/comments')
return data
},
}
This is my posts.vue:
<template>
<div>
<div v-if="dataLoaded">
content
</div>
<div v-else>
loading...
</div>
</div>
</template>
<script>
finishedApiCalls = 0
get dataLoaded() {
return this.finishedApiCalls === 3
}
created() {
PostService.getPosts()
.then((posts) => {
this.posts = posts
this.finishedApiCalls++
})
.catch((error) => {
console.log('There was an error:', error)
})
PostService.getTags()
.then((tags) => {
this.tags = tags
this.finishedApiCalls++
})
.catch((error) => {
console.log('There was an error:', error)
})
PostService.getComments()
.then((comments) => {
this.comments = comments
this.finishedApiCalls++
})
.catch((error) => {
console.log('There was an error:', error)
})
}
</script>
The key point is that I want to display a loading spinner as long as the data has not been loaded. Is it recommended to make the API calls from created()? What would be a more elegant way to find out when all calls are finished? It does not feel right to use the finishedApiCalls variable.
I recommend using Nuxt's fetch method along with Promise.all() on all your async PostService fetches:
// MyComponent.vue
export default {
fetch() {
return Promise.all([
PostService.getPosts().then((posts) => ...).catch((error) => ...),
PostService.getTags().then((tags) => ...).catch((error) => ...),
PostService.getComments().then((comments) => ...).catch((error) => ...)
])
}
}
Nuxt provides a $fetchState.pending prop that you could use for conditionally rendering a loader:
<template>
<div>
<Loading v-if="$fetchState.pending" />
<div v-else>My component data<div>
</div>
</template>
You can use Promise.all for this kind of requirements.
this.loading = true
Promise.all([PostService.getPosts(), PostService.getTags(), PostService.getComments()])
.then(values => {
let [posts, tags, comments] = values
this.posts = posts
this.tags = tags
this.comments = comments
//Here you can toggle your fetching flag like below
this.loading = false
})
You can use Promise.all(). This will wait till all resolves or if 1 fails.
With async / await you can make it "synchronous"
data() {
return {
loaded: false
}
},
async created() {
let [posts, tags, comments] = await Promise.all([PostService.getPosts(), PostService.getTags(), PostService.getComments()])
this.posts = posts;
this.tags = tags;
this.comments = comments;
this.loaded = true;
}