Leverage props in Vue 3 composables

While upgrading an app from vue 2 to vue 3, I encountered some difficulties with composables. My issue revolves around using props in the composable, which doesn't seem to work as expected. The code snippet is extracted from a functioning component and behaves well within the component.

I suspect that defineProps may not be supported by composables, leaving me uncertain on how to handle it. Passing the src in the parameters results in losing reactivity.

// loadImage.js
import { defineProps, onMounted, ref, watch } from 'vue'

// conventionally, composable function names begin 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 approach proved effective for me, unlike the two methods mentioned in the comments below.

I posed a new question here.

// loadImage.js
import { onMounted, ref, watch } from 'vue'

// conventionally, 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>

Answer №1

Your assumption is indeed correct that defineProps cannot be utilized in composables! However, the real question remains:

How can props be passed into composables without sacrificing reactivity?

❓ Pass the entire props object

const props = defineProps({ src: string })

useFeature(props)

If you pass the complete props object, reactivity will stay intact! Nevertheless, it is not recommended due to the following reasons:

  1. The composable may not require all the props
  2. If all the props are needed, consider splitting the composable into smaller ones

In essence, strive to keep your composables as straightforward as possible

❓ Utilize toRef

One commonly used solution is to employ toRef:

const props = defineProps({ foo: Object })

useFeature(toRef(props, 'foo'))

While this solution can work in most scenarios, there are two potential issues:

  1. props.foo might be nonexistent when toRef is called
  2. This approach fails to handle situations where props.foo is swapped with a different object.

❓ Utilize computed

Most developers resort to using computed values:

const props = defineProps({ foo: Object })

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

However, using computed values here is suboptimal as it generates an additional effect to cache the computation. Computed values are excessive for simple getters that merely access properties.

❓✅ Employ toRefs

const props = defineProps({ src: string })

const { src } = toRefs(props)
useFeature(src)

Although this method works effectively, with the release of 3.3, we will have reactive defineProps, making it unnecessary to use toRefs on props.

Consider it as legacy code starting from 3.3.

✅ Employ "thunking"

The most cost-effective way to pass non-ref reactive state into a composable is by wrapping it with a getter (or "thunking” - delaying the access of the actual value until the getter is invoked):

const props = defineProps({ foo: Object })

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

This approach preserves reactivity! Here's an example demonstrating how to use this within composables:

import { computed, watch } from 'vue'

export function useFeature(imageSrc) { 
  const newImageSrc = computed(() => `https:\\${imageSrc()}`) // 👈 access it

  watch(imageSrc, (newVal) => { ... } // 👈 watch it
  
  return { ... }
}

Check out a demo in Vue SFC Playground

Future enhancements

In this PR, which heavily influenced this answer, we will soon have the ability to use toRef with a getter syntax like:

toRef(() => object.key)

Therefore, once 3.3 is launched, the optimal approach will be:

✅✅ Utilize toRef with a getter

const props = defineProps({ foo: Object })

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

Answer №2

Per the official documentation:

defineProps and defineEmits are compiler macros that can only be used within <script setup>

It is recommended to pass the props as a parameter without destructuring them in order to maintain reactivity:

export function useLoadImage(props) {
....

}

Answer №3

Utilize the useRef function to efficiently pass individual props while maintaining reactivity

const elemRef = toRef(props, "element");
const { loading, display } = useElementActions(elemRef);

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Is there something lacking in my project.json while building with nuxtjs?

Issue detected in ./services/emailService.js Unable to locate module: Error: Unable to find '@/apis/adl/instance/EmailsApi' in I encountered this error while executing the nuxt build within a docker container or on certain CI/CD servers such a ...

Vue-Routes is experiencing issues due to a template within one of the routes referencing the same ID

I encountered an issue while developing a Vue application with Vue-routes. One of the routes contains a function designed to modify the background colors of two divs based on the values entered in the respective input fields. However, I am facing two probl ...

Mastering the art of utilizing v-if and v-for with nested objects in Vue.js

Struggling to display nested object content using v-for. I have a prop containing an object, but the div doesn't show when I use v-if="prop". Any help on how to resolve this issue would be greatly appreciated. Below is the syntax I'm currently us ...

Assign a class to the initial element of the Vuetify v-select module

Hey there! Currently, I'm utilizing the v-select component from Vuetify. I'm looking to customize the appearance of the first item in the v-select component by adding a specific class. For example, I want the text of the first entry "Item A" to b ...

Something seems to be amiss with the Vue.js eslint configuration. Can you help

I am just starting out with vueJs. I attempted to use this basic code in my vue project to update the data of a component: <template> <div> <h1> {{ message }} <h2> Hello {{ firstname }} {{ lastna ...

Experiencing difficulties when attempting to send a cookie to the server

Why is the vue+axios frontend not sending cookies to my php server in the request header? I am currently in the process of migrating an old project to a new server. After making some optimizations to the project architecture, everything worked perfectly f ...

Utilizing Props in Data and Methods in Vue.js

As a newcomer to Vue.js, I am encountering an issue when passing data from a parent component to a child component using props. While I can access the data in the child component, I'm facing difficulties when trying to use it within the child componen ...

Is there a way to position the icon in the top left corner within the image using Vue and CSS?

I am currently working on a Vue project and I am attempting to create a row of images with an icon positioned at the top left corner of each image. Here is my code snippet: <div v-for="(image, index) in images" :key="index" class=&q ...

Combining package.json commands for launching both an Express server and a Vue app

I recently developed an application using Vue.js and express.js. As of now, I find myself having to open two separate terminal windows in order to run npm run serve in one and npm start in the other. My ultimate goal is to streamline this process and have ...

Convert price to Indonesian Rupiah currency format with the help of Vue.js

Can someone help me convert the price format from IDR 50,000.00 to IDR 50.000 using JavaScript and Vue? I found a script on this website, but I am having trouble understanding how it works. The script looks like this: replace(/(\d)(?=(\d{3})+(?: ...

TypeError: The firestore function within the firebase_app__WEBPACK_IMPORTED_MODULE_0__ object is not recognized in Vue.js

I encountered a problem with my firebase.js file where I received the error message Uncaught TypeError: firebase_app__WEBPACK_IMPORTED_MODULE_0__.firestore is not a function in my console. Despite trying different methods to import firebase, none of them ...

I am running into issues getting Tailwind CSS to work in my project. Despite following the installation instructions in the documentation and trying to learn this new CSS framework, it doesn't seem to

//I followed the instructions in the tailwind documentation to install and set up everything. However, when I try to use tailwind utility classes in my HTML file, they don't seem to work. Can someone please assist me with this issue? // Here is my sr ...

Rendering a Vue select list before receiving data from a Meteor callback

I am currently facing an issue with populating my events array from a meteor call so that it appears in a select list. The 'get.upcoming' Meteor function returns an array of JSON objects, but it seems like the select list is being rendered before ...

Issue with V-checkbox input-value not triggering correctly on change

Query Example: <query #[`${instanceItemIdKey}`]="{ item }"> <v-checkbox :input="item.content" @modify="$notify('onPermissionUpdate', item)" ></v-checkbox> </query> The information that influ ...

Retrieve a Vue Component by utilizing a personalized rendering method for a Contentful embedded entry

Currently experimenting with Contentful, encountering some issues with the Rich text content field. To customize the rendering of this block and incorporate assets and linked references in Rich text content, I am utilizing the modules '@contentful/ri ...

Navigating with vue-router using guard redirections

I am currently working with vue.js and I'm looking to implement a functionality where I can redirect a user to another URL within the navigation guard called beforeRouteEnter. The main objective is to check if the current city of the user is included ...

A guide to efficiently passing props in Quasar 2 Vue 3 Composition API for tables

I am encountering an issue while attempting to create a custom child component with props as row data. The error message "rows.slice is not a function" keeps appearing, and upon inspecting the parent data, I found that it is an Object. However, the props r ...

I'm currently leveraging Vue.js and Python Flask for my backend development. I'm looking to establish some local variables. What is the best way to accomplish this?

Here is my Vue js file where I am utilizing two URLs from localhost. My goal is to create a configuration file that will allow me to make changes in one place and have those changes reflected throughout. <template> <div> <div class="glob ...

Having trouble creating an axios instance in Vue

I recently came across a helpful tip in an article about using axios for AJAX requests in Vue. The author mentioned that by setting Vue.prototype.$http = axios, we can then use this.$http within the Vue instance, which worked perfectly for me. However, wh ...

The Vue component table is not showing any retrieved data

Seeking help as a newcomer to the world of Laravel and Vue.js. Trying to populate data on a Vue component, but facing an issue with the tableData variable in the axios.get response. It is not rendering the array data onto the table as expected. Could there ...