Close Menu
    DevStackTipsDevStackTips
    • Home
    • News & Updates
      1. Tech & Work
      2. View All

      The Ultimate Guide to Node.js Development Pricing for Enterprises

      July 29, 2025

      Stack Overflow: Developers’ trust in AI outputs is worsening year over year

      July 29, 2025

      Web Components: Working With Shadow DOM

      July 28, 2025

      Google’s new Opal tool allows users to create mini AI apps with no coding required

      July 28, 2025

      I replaced my Samsung OLED TV with this Sony Mini LED model for a week – and didn’t regret it

      July 29, 2025

      I tested the most popular robot mower on the market – and it was a $5,000 crash out

      July 29, 2025

      5 gadgets and accessories that leveled up my gaming setup (including a surprise console)

      July 29, 2025

      Why I’m patiently waiting for the Samsung Z Fold 8 next year (even though the foldable is already great)

      July 29, 2025
    • Development
      1. Algorithms & Data Structures
      2. Artificial Intelligence
      3. Back-End Development
      4. Databases
      5. Front-End Development
      6. Libraries & Frameworks
      7. Machine Learning
      8. Security
      9. Software Engineering
      10. Tools & IDEs
      11. Web Design
      12. Web Development
      13. Web Security
      14. Programming Languages
        • PHP
        • JavaScript
      Featured

      Performance Analysis with Laravel’s Measurement Tools

      July 29, 2025
      Recent

      Performance Analysis with Laravel’s Measurement Tools

      July 29, 2025

      Memoization and Function Caching with this PHP Package

      July 29, 2025

      Laracon US 2025 Livestream

      July 29, 2025
    • Operating Systems
      1. Windows
      2. Linux
      3. macOS
      Featured

      Microsoft mysteriously offered a Windows 11 upgrade to this unsupported Windows 10 PC — despite it failing to meet the “non-negotiable” TPM 2.0 requirement

      July 29, 2025
      Recent

      Microsoft mysteriously offered a Windows 11 upgrade to this unsupported Windows 10 PC — despite it failing to meet the “non-negotiable” TPM 2.0 requirement

      July 29, 2025

      With Windows 10’s fast-approaching demise, this Linux migration tool could let you ditch Microsoft’s ecosystem with your data and apps intact — but it’s limited to one distro

      July 29, 2025

      Windows 10 is 10 years old today — let’s look back at 10 controversial and defining moments in its history

      July 29, 2025
    • Learning Resources
      • Books
      • Cheatsheets
      • Tutorials & Guides
    Home»Development»How Vue Composables Work – Explained with Code Examples

    How Vue Composables Work – Explained with Code Examples

    June 13, 2025

    Vue composables are a very helpful tool when developing Vue applications. They give developers an easy way to reuse logic across our applications. In addition to allowing for “stateless” logic (things like formatting or routine calculations), composables also give us the ability to reuse stateful logic throughout the app.

    Before diving into the tutorial below, I want to mention that the documentation for Vue is really good. The page on composables explains the basics really well and will get you 90 percent of the way there. I am writing this article because I think the examples in the docs could go a little deeper in explaining how things can work inside of a composable. I will be reiterating some of the information from the docs, but I will also provide an example of a more complex composable.

    Here’s what we’ll cover:

    • Why Use Composables?

    • Simple Composable Example

    • Complex Composable Example

      • Util function for fetch

      • useAsyncState Composable

      • Usage in a component

    • Conclusion

    Why Use Composables?

    Composables let you reuse stateful logic across your apps. Whenever there is logic that is used in more than two places, we typically want to pull that logic into its own function. Most of the time, that logic is considered “stateless”, meaning that it takes an input and returns an output. The docs mention date formatting, but this could also include something like currency calculations or string validation.

    In modern web applications, there are often pieces of logic that require managing state over time. Inside a typical component, we have the ability to adapt the application depending on the “state” of different variables within the component. Sometimes that logic, or at least pieces of that logic, are reused throughout the app.

    For example, in an e-commerce application, you might have logic to increase and decrease the quantity of a product a person is adding to their cart. This logic could be used both on the product page, and inside the cart itself.

    The look and feel of both those places will be different, so re-using a full component wouldn’t make sense – but we still want to centralize the logic to make the code easier to maintain. That is where Composables come in.

    (It is worth noting that not everything needs to be a composable. Logic that is only used in a single component shouldn’t be refactored into a composable until necessary.)

    Simple Composable Example

    Let’s take a look at a simple counter example. Here is some code for a very simple Counter component.

    <script setup lang="ts">
      import { ref } from 'vue'
      import type { Ref } from 'vue'
    
      const count: Ref<number> = ref(0)
      const increment = () => {
        count.value++
      }
      const decrement = () => {
        count.value--
      }
    </script>
    
    <template>
      <div class="bg-teal-100 border-2 border-gray-800 rounded-xl p-4 w-64">
        <div class="text-center mb-4">
          <span class="text-lg font-medium text-gray-800">Count: {{ count }}</span>
        </div>
    
        <div class="flex gap-2 justify-center">
          <button
            @click="decrement"
            class="bg-red-100 border-2 border-gray-800 rounded px-4 py-0 text-gray-800 font-medium hover:bg-red-500 transition-colors"
          >
            -
          </button>
    
          <button
            @click="count = 0"
            class="bg-gray-100 border-2 border-gray-800 rounded px-4 py-0 text-gray-800 font-medium hover:bg-gray-300 transition-colors"
          >
            Reset
          </button>
    
          <button
            @click="increment"
            class="bg-green-100 border-2 border-gray-800 rounded px-4 py-0 text-gray-800 font-medium hover:bg-green-500 transition-colors"
          >
            +
          </button>
        </div>
      </div>
    </template>
    

    The output of that component would look like this:

    8c39759c-6fd9-4fcf-abdf-f67e672c172f

    This works great, but if we end up needing this same counter logic in another component with a completely different look and feel, then we would end up repeating the logic. We can extract the logic into a composable and access the same stateful logic anywhere we need to.

    // counter.ts
    import { ref } from 'vue'
    import type { Ref } from 'vue'
    
    export default function useCounter(): Readonly<{
      count: Ref<number>
      increment: () => void
      decrement: () => void
    }> {
      const count: Ref<number> = ref(0)
      const increment = () => {
        count.value++
      }
      const decrement = () => {
        count.value--
      }
      return { count, increment, decrement }
    }
    

    Then we update the script tag in the component to use the composable:

    <script setup>
    import { useCounter } from '@/counter.ts'
    
    const { count, increment, decrement } = useCounter()
    </script>
    
    <template>
      ...
    </template>
    

    Now we can use this logic in multiple components throughout the app.

    d90d000e-f309-4b22-9530-8e4614b450ec

    You will notice that only the logic is copied and each component still has its own copy of the count state. Using a composable does not mean the state is shared across components, only the stateful logic.

    Complex Composable Example

    In the Vue docs, they give an example of using a composable to handle async data fetching. There are a couple of issues I have with the example they give. The main one is that the error handling is not robust for real world applications. Given that they just want to showcase a straightforward use of composables, this is understandable. But I wanted to showcase a more realistic implementation.

    Util function for fetch

    Before getting into the composable, we need to set up a util function for the fetch API. This is because we want to make sure every request throws an error if it fails. The fetch API doesn’t throw an error if the request responds with an error status. We have to check the response.ok in order to verify the status, and then throw an error if necessary.

    // utils.ts
    export async function handleFetch(url: string, options: RequestInit = {}): Promise<Response> {
      const res = await fetch(url, options)
      if (!res.ok) {
        const err = await res.text()
        throw new Error(err)
      }
      return res
    }
    

    useAsyncState Composable

    When working with async state, the requests can be in a few different states:

    • Pending

    • Resolved

    • Rejected

    In addition to these states, we want to track the data or the error that comes back from the request.

    // useAsyncState.ts
    import { shallowRef } from 'vue'
    import type { Ref } from 'vue'
    
    // Specify a type for the response
    export type AsyncState<T> = {
      data: Ref<T | null>
      error: Ref<Error | null>
      isPending: Ref<boolean>
      isResolved: Ref<boolean>
      isRejected: Ref<boolean>
    }
    
    export default function useAsyncState<T>(promise: Promise<T>): AsyncState<T> {
      // I used shallowRef instead of ref to avoid deep reactivity
      // I only care about the top-level properties being reactive
      const data = shallowRef<T | null>(null)
      const error = shallowRef<Error | null>(null)
      const isPending = shallowRef(false)
      const isResolved = shallowRef(false)
      const isRejected = shallowRef(false)
    
      data.value = null
      error.value = null
      isPending.value = true
      isRejected.value = false
      isResolved.value = false
    
      promise.then((result) => {
        data.value = result
        isPending.value = false
        isResolved.value = true
      }).catch(err => {
        error.value = err
        isPending.value = false
        isRejected.value = true
      })
    
      return { data, error, isPending, isResolved, isRejected }
    }
    

    This gives a few more explicit properties for the different states, rather than relying on the values in data and error. You’ll also notice that this composable takes in a promise rather than a URL string like the docs show. Different endpoints will have different response types and I wanted to be able to handle those outside of this composable.

    Usage in a component

    I have set up an endpoint that will wait a random number of seconds before responding either successfully or with an error. My component is calling this endpoint using the composable and using the data from the composable to update the template.

    304b8c08-5277-4243-b621-70a7c19edcfd

    With the error state showing like this:

    7d0c6923-85b9-4971-8f69-d127ffa6c1f4

    You can see a working example at https://understanding-composables.pages.dev/.

    To make this a bit easier to explain and understand, I am breaking up the <script> tag and the <template> sections of the component.

    Script

    <script lang="ts" setup>
    import { ref, unref } from 'vue'
    import type { Ref } from 'vue'
    import { useAsyncState } from '@/composables'
    import type { AsyncState } from '@/composables'
    import { handleFetch } from '@/utils'
    
    interface RandomResponse {
      msg: string
    }
    
    async function getRandomResponse(): Promise<RandomResponse> {
      const response = await handleFetch('https://briancbarrow.com/api/random')
      const text = await response.text()
      return { msg: text }
    }
    
    const randomResponseData: Ref<AsyncState<RandomResponse> | null> = ref(null)
    
    const handleMakeRequest = async () => {
        const data = getRandomResponse()
        randomResponseData.value = useAsyncState(data)
    }
    </script>
    

    Here we have a method, getRandomResponse that calls an endpoint and returns a promise. That promise is then passed into the useAsyncState when handleMakeRequest is called. That puts the full return value into the randomResponseData ref which we can then use inside the template.

    Rather than show the full template, I will just show a few portions of it.

    Here you can see two different buttons being used depending on the state. I am using a separate button element to indicate the “loading” state, but in practice you can use the composable properties to set the disabled property of the button and change the text.

            <button
              v-if="
                !randomResponseData?.isPending &&
                !randomResponseData?.error &&
                !randomResponseData?.data
              "
              class="px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
              @click="handleMakeRequest"
            >
              Make Request
            </button>
    
            <!-- Loading State Button -->
            <button
              v-if="randomResponseData?.isPending"
              disabled
              class="px-6 py-3 bg-blue-600 text-white font-medium rounded-lg opacity-75 cursor-not-allowed flex items-center mx-auto"
            >
              <svg
                class="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
                xmlns="http://www.w3.org/2000/svg"
                fill="none"
                viewBox="0 0 24 24"
              >
                <circle
                  class="opacity-25"
                  cx="12"
                  cy="12"
                  r="10"
                  stroke="currentColor"
                  stroke-width="4"
                ></circle>
                <path
                  class="opacity-75"
                  fill="currentColor"
                  d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
                ></path>
              </svg>
              Loading...
            </button>
    

    Here are a couple of the rows from the table:

    <tr class="divide-x divide-gray-200">
      <td class="py-4 pr-4 pl-4 text-sm font-medium whitespace-nowrap text-gray-900 sm:pl-0">
        isPending
      </td>
      <td
        class="p-4 text-sm whitespace-nowrap text-gray-500"
        :class="randomResponseData?.isPending ? 'bg-blue-500' : 'bg-gray-300'"
      ></td>
      <td class="p-4 text-sm whitespace-nowrap text-gray-500">
        {{ randomResponseData?.isPending }}
      </td>
    </tr>
    
    <tr class="divide-x divide-gray-200">
      <td class="py-4 pr-4 pl-4 text-sm font-medium whitespace-nowrap text-gray-900 sm:pl-0">
        data
      </td>
      <td
        class="p-4 text-sm whitespace-nowrap text-gray-500"
        :class="randomResponseData?.data ? 'bg-green-500' : 'bg-gray-300'"
      ></td>
      <td class="p-4 text-sm whitespace-nowrap text-gray-500">
        {{ unref(randomResponseData?.data)?.msg }}
      </td>
    </tr>
    

    In those tr tags, you can see the template rendering different things depending on the state coming from the composable.

    For a more complete look at the code, you can visit the GitHub repo. You can also look at how VueUse, a collection of composables, handles similar functionality: https://vueuse.org/core/useAsyncState/

    In a future article, I’ll dive into their implementation.

    Conclusion

    Composables are an incredibly useful tool in Vue 3. As projects grow in size and scope, knowing how and when to use composables can improve the maintainability of the project over the long term.

    The key is identifying when you have stateful logic that needs to be reused across components, then extracting it into a well-structured composable that handles edge cases properly.

    For more real world examples you can check out the VueUse library and repo.

    Source: freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More 

    Facebook Twitter Reddit Email Copy Link
    Previous ArticleHow to Build a Medical Chatbot with Flutter and Gemini: A Beginner’s Guide
    Next Article How to Improve Your Phone’s Privacy

    Related Posts

    Development

    Performance Analysis with Laravel’s Measurement Tools

    July 29, 2025
    Development

    Memoization and Function Caching with this PHP Package

    July 29, 2025
    Leave A Reply Cancel Reply

    For security, use of Google's reCAPTCHA service is required which is subject to the Google Privacy Policy and Terms of Use.

    Continue Reading

    CVE-2025-47827 – IGEL OS Boot Signature Verification Bypass

    Common Vulnerabilities and Exposures (CVEs)

    Cohere Embed 4 multimodal embeddings model is now available on Amazon SageMaker JumpStart

    Machine Learning

    Breaking Down Apple’s Liquid Glass: The Tech, The Hype, and The Reality

    Web Development

    May 2025 Patch Tuesday: Five Zero-Days and Five Critical Vulnerabilities Among 72 CVEs

    Security

    Highlights

    Gourmand Recipe Manager

    May 6, 2025

    Gourmand is a fork of the Gourmet Recipe Manager: a manager, editor, and organizer for…

    Building a REACT-Style Agent Using Fireworks AI with LangChain that Fetches Data, Generates BigQuery SQL, and Maintains Conversational Memory

    May 2, 2025

    CVE-2025-6981 – GitHub Enterprise Server Unauthorized Read Access Vulnerability

    July 16, 2025

    Elevating API Automation: Exploring Karate as an Alternative to Rest-Assured

    June 25, 2025
    © DevStackTips 2025. All rights reserved.
    • Contact
    • Privacy Policy

    Type above and press Enter to search. Press Esc to cancel.