Search code examples
vue.jsvuejs3vue-composition-apivue-script-setup

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>

Solution

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

    1. 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.

    2. 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 */) ✅
      
    3. 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