Vue3 Composition API: Computed value not updating - vue.js

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>

Related

Component not firing after onMounted is called

I have the following component, it is a very simple countdown to 0. When the countdown reaches 0 it fires a redirect. When I put this component on my page itself, the onMounted function fires the interval(counterFunc()). When I isolate it into a component the interval(counterFunc()) does not fire. How can I fix this?
<template>
{{ counter }}
</template>
<script setup lang="ts">
const props = defineProps<{
time: number;
}>();
let counter = toRef(props, 'time')
const counterFunc = () => {
setInterval(() => {
counter.value--;
if (counter.value == 0) {
clearError({redirect: '/'})
}
}, 1000);
};
onMounted(() => {
counterFunc();
});
Using it in a template:
<AppCountdown :time=10></AppCountdown>
Here is a working example:
<script setup lang="ts">
import { defineProps, ref, onMounted, toRef } from "vue";
const props = defineProps<{
time: number;
}>();
const counter = ref(props.time); // set a proper ref that can be mutated
const counterFunc = () => {
setInterval(() => {
counter.value = counter.value - 1; // mutate the ref as expected
if (counter.value == 0) {
console.log("clearError"); // replace with your clear error function
}
}, 1000);
};
onMounted(counterFunc);
</script>
<template>
{{ counter }}
</template>
You can try it here: https://codesandbox.io/s/heuristic-stallman-niiqzl?file=/src/App.vue:0-419

Not all fetched data from backend is rendered onMounted VueJS

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?

Vue 3 Geolocation computed property latitude and longitude always returning 0

i create a composable function to get current coordinates from geolocation and returned correctly, but when i acces in my script setup always returned 0
here is my composable useGeolocation.js
import { onUnmounted, onMounted, ref } from 'vue'
export function useGeolocation(){
const kordinat = ref({ latitude: 0, longitude: 0 })
const isSupported = 'navigator' in window && 'geolocation' in navigator
let watcher = null
onMounted(() => {
if(isSupported)
watcher = navigator.geolocation.watchPosition(
position => (kordinat.value = position.coords)
)
})
onUnmounted(()=> {
if (watcher) navigator.geolocation.clearWatch(watcher)
})
return { kordinat, isSupported }
}
and how I use in my component
<script setup>
import { useGeolocation } from '#/composables/useGeoLocation'
const {kordinat} = useGeolocation()
const posisiUser = computed(()=>({
lat : kordinat.value.latitude,
lng : kordinat.value.longitude
}))
//...
</script>
in template i can display coordinates correctly with <h2>Coords : {{posisiUser.lat}}, {{posisiUser.lng}}</h2>
Coords : -6.22592, 106.8302336
but when i log console.log({p:posisiUser.value}) it returns '{"p":{"lat":0,"lng":0}}'
I need to be able access the correct coordinates for further usage. What is wrong?
here is sandbox link
You're logging the value of the computed prop immediately, which is not yet updated:
<script setup>
import { computed, ref, onMounted } from "vue";
import { useGeolocation } from "./composables/useGeoLocation";
const { kordinat } = useGeolocation();
const posisiUser = computed(() => ({
lat: kordinat.value.latitude,
lng: kordinat.value.longitude,
}));
console.log({ p: posisiUser.value }); // ❌ posisiUser not yet updated in useGeolocation()
</script>
If you want to log every change to the value, you can use a watchEffect():
watchEffect(() => {
console.log({ p: posisiUser.value });
})
To watch it only once, you can do:
const unwatch = watchEffect(() => {
unwatch();
console.log({ p: posisiUser.value });
})
demo

Use props in composables vue3

I am upgrading an app from vue 2 to vue 3 and I am having some issues with composables. I'd like to use props in the composable but it doesn't seem to be working. The code sample is pulled from a working component and works fine when I leave it in the component.
I assume defineProps isn't supported by composables, but then I am unclear how to handle it. When I pass the src in the parameters it loses its reactivity.
// loadImage.js
import { defineProps, onMounted, ref, watch } from 'vue'
// by convention, composable function names start with "use"
export function useLoadImage() {
let loadingImage = ref(true)
let showImage = ref(false)
const props = defineProps({
src: String,
})
const delayShowImage = () => {
setTimeout(() => {
showImage.value = true
}, 100)
}
const loadImage = (src) => {
let img = new Image()
img.onload = (e) => {
loading.value = false
img.onload = undefined
img.src = undefined
img = undefined
delayShowImage()
}
img.src = src
}
onMounted(() => {
if (props.src) {
loadImage(props.src)
}
})
watch(
() => props.src,
(val) => {
if (val) {
loadingImage.value = true
loadImage(val)
}
},
)
// expose managed state as return value
return { loadingImage, showImage }
}
Edit
This method worked for me, but the two methods mentioned in the comments below did not.
I have a new question here.
// loadImage.js
import { onMounted, ref, watch } from 'vue'
// by convention, composable function names start with "use"
export function useLoadImage(props) {
let loadingImage = ref(true)
let showImage = ref(false)
const delayShowImage = () => {
setTimeout(() => {
showImage.value = true
}, 100)
}
const loadImage = (src) => {
let img = new Image()
img.onload = (e) => {
loading.value = false
img.onload = undefined
img.src = undefined
img = undefined
delayShowImage()
}
img.src = src
}
onMounted(() => {
if (props.src) {
loadImage(props.src)
}
})
watch(
() => props.src,
(val) => {
if (val) {
loadingImage.value = true
loadImage(val)
}
},
)
// expose managed state as return value
return { loadingImage, showImage }
}
<script setup>
import { defineProps, toRef } from 'vue'
import { useLoadImage } from '../../composables/loadImage'
const props = defineProps({
src: String
})
const { loading, show } = useLoadImage(props)
</script>
According to official docs :
defineProps and defineEmits are compiler macros only usable inside <script setup>
You should pass the props as parameter without destructing them to not loose the reactivity :
export function useLoadImage(props) {
....
}
you can use useRef to pass specific props without losing reactivity
const imgRef = toRef(props, "img");
const { loding, show } = useLoadImage(imgRef);

Passing a single property to a composable in Vue3

I am using a composable to load images in Vue3. I have been able to pass all the props successfully as one object, see this question, but I am unable to pass the one property I want to be reactive. I am fairly certain that the issue is that the property in undefined
// loadImage.js
import { onMounted, ref, watch } from 'vue'
// by convention, composable function names start with "use"
export function useLoadImage(src) {
let loading = ref(true)
let show = ref(false)
const delayShowImage = () => {
setTimeout(() => {
show.value = true
}, 100)
}
const loadImage = (src) => {
let img = new Image()
img.onload = (e) => {
loading.value = false
img.onload = undefined
img.src = undefined
img = undefined
delayShowImage()
}
img.src = src
}
onMounted(() => {
if (src) {
loadImage(src)
}
})
watch(
() => src,
(val) => {
if (val) {
loading.value = true
loadImage(val)
}
},
)
// expose managed state as return value
/**
* loading is the image is loading
* show is a delayed show for images that transition.
*/
return { loading, show }
}
The below method returns this in the console.log and does not error.
Proxy {src: undefined} undefined
<script setup>
import { defineProps, computed } from 'vue'
import { useLoadImage } from '../../composables/loadImage'
const props = defineProps({
src: String
})
console.log(props, props.src)
const srcRef = computed(() => props.src)
const { loading, show } = useLoadImage(srcRef)
</script>
The below method returns this in the console.log
Proxy {src: undefined} undefined
and gives the following error
TypeError: Cannot read properties of undefined (reading 'undefined')
<script setup>
import { defineProps, toRef } from 'vue'
import { useLoadImage } from '../../composables/loadImage'
const props = defineProps({
src: String
})
console.log(props, props.src)
const srcRef = toRef(props.src)
const { loading, show } = useLoadImage(srcRef)
</script>
As indicated in comments, it seems src is undefined in your component because you're probably not passing the prop correctly to the component.
Even if src were set with a string, there still would be a few other issues:
toRef's first argument should be a reactive object (i.e., props), and the second argument should be the name of a key (i.e., 'src'):
// MyComponent.vue
const srcRef = toRef(props.src) ❌
const srcRef = toRef(props, 'src') ✅
Note: It's also valid to use const srcRef = computed(() => props.src), as you were originally doing.
watch's first argument is a WatchSource. When WatchSource is a function dealing with a ref, it should return the ref's unwrapped value. Alternatively, the WatchSource can be the ref itself:
// loadImage.js
watch(() => srcRef, /* callback */) ❌
watch(() => srcRef.value, /* callback */) ✅
watch(srcRef, /* callback */) ✅
The composable receives the image source in a ref, and your onMounted() hook is passing that ref to loadImage(), which is actually expecting the string in the ref's unwrapped value:
// loadImage.js
onMounted(() => {
if (src) { ❌ /* src is a ref in this composable */
loadImage(src)
}
})
onMounted(() => {
if (src.value) { ✅
loadImage(src.value)
}
})
demo