Reputation: 7447
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 }
}
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>
Upvotes: 23
Views: 19019
Reputation: 1337
you can use toRef
to pass specific props without losing reactivity
const imgRef = toRef(props, "img");
const { loding, show } = useLoadImage(imgRef);
Upvotes: 0
Reputation: 27809
Your assumption is correct that defineProps
cannot be used in composables!
But the question is:
props
objectconst props = defineProps({ src: string })
useFeature(props)
If you pass the whole props object, reactivity will be retained! However, I don't recommend doing that because:
In general keep your composables as simple as they can be
toRef
One solution people use, is toRef
:
const props = defineProps({ foo: Object })
useFeature(toRef(props, 'foo'))
This might work in most cases, however there are two problems:
props.foo
may not exist when toRef
is calledprops.foo
is swapped to a different object.computed
This is the most common solution devs use:
const props = defineProps({ foo: Object })
useFeature(computed(() => props.foo?.bar))
However, using computed is sub-optimal here. Internally, computed creates a separate effect to cache the computation. computed is an overkill for simple getters that just access properties.
toRefs
const props = defineProps({ src: string })
const { src } = toRefs(props)
useFeature(src)
This works very well but starting from 3.3 we would have reactive defineProps
so it would be unnecessary to use toRefs
on props.
I would think of it as a legacy code starting from 3.3.
The least expensive way to pass non-ref reactive state into a composable is by wrapping it with a getter (or "thunking" - i.e. delaying the access of the actual value until the getter is called):
const props = defineProps({ foo: Object })
useFeature(() => props.foo?.bar)
In this way, reactivity will be retained! Here is an example on how to use this inside composables:
import { computed, watch } from 'vue'
export function useFeature(imageSrc) {
const newImageSrc = computed(() => `https:\\${imageSrc()}`) // 👈 access it
watch(imageSrc, (newVal) => { ... } // 👈 watch it
return { ... }
}
Checkout a demo in Vue SFC Playground
In this PR, from which this answer is heavily inspired, we will have the ability to use toRef
with a getter syntax like:
toRef(() => object.key)
So when 3.3 is released the best way to do it will be:
toRef
with a getterconst props = defineProps({ foo: Object })
useFeature(toRef(() => props.foo?.bar))
Upvotes: 53
Reputation: 1
According to official docs :
defineProps
anddefineEmits
are compiler macros only usable inside<script setup>
You should pass the props
as parameter without destructing them to not lose the reactivity :
export function useLoadImage(props) {
....
}
Upvotes: 20