whoacowboy
whoacowboy

Reputation: 7447

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>

Upvotes: 23

Views: 19019

Answers (3)

Sunghyun Cho
Sunghyun Cho

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

Roland
Roland

Reputation: 27809

Your assumption is correct that defineProps cannot be used in composables! But the question is:

How to pass props into composables without losing reactivity:

❓ Pass the whole props object

const 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:

  1. The composable doesn't need all the props
  2. If it needs ALL the props then probably you should split that composable into smaller ones

In general keep your composables as simple as they can be

❓ Use 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:

  1. props.foo may not exist when toRef is called
  2. This cannot handle the case when props.foo is swapped to a different object.

❓ Use 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.

❓✅ Use 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.

✅ Use "thunking"

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

Future improvements

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:

✅✅ Use toRef with a getter

const props = defineProps({ foo: Object })

useFeature(toRef(() => props.foo?.bar))

Upvotes: 53

Boussadjra Brahim
Boussadjra Brahim

Reputation: 1

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 lose the reactivity :

export function useLoadImage(props) {
....

}

Upvotes: 20

Related Questions