When working with Vue’s Composition API, sometimes you need a single source of truth — like shared config, cached data, or a global flag. In these cases, you might reach for provide/inject… but there’s another elegant option: singleton composables.

This post covers:

  • How to build a reactive singleton composable
  • How to expose a reload method
  • A comparison with provide/inject

🔁 What’s a Singleton Composable?

A singleton composable is just a regular function that returns a shared reactive state, ensuring it’s only initialized once — no matter how many components use it.


🛠 Example: Fetching Data Once and Reloading It

Here’s how you can create a reactive singleton composable with a reload method:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// composables/useSettings.ts
import { ref } from 'vue'

const settings = ref(null)
const loading = ref(false)
let loaded = false

async function fetchSettings() {
  loading.value = true
  // Simulate a fetch or call to API
  await new Promise(resolve => setTimeout(resolve, 1000))
  settings.value = {
    theme: 'dark',
    language: 'en',
  }
  loading.value = false
  loaded = true
}

export function useSettings() {
  if (!loaded) {
    fetchSettings()
  }

  return {
    settings,
    loading,
    reload: fetchSettings,
  }
}

✅ Usage in components

1
2
3
4
5
6
7
8
9
10
11
<script setup>
import { useSettings } from '@/composables/useSettings'

const { settings, loading, reload } = useSettings()
</script>

<template>
  <div v-if="loading">Loading...</div>
  <pre v-else>{ '{ settings }' }</pre>
  <button @click="reload">Reload</button>
</template>

✔️ The same settings and loading state is used everywhere.
✔️ Calling reload() triggers a re-fetch globally.


🆚 Singleton vs provide/inject

Let’s break down when to use each:

Feature Singleton Composable provide/inject
🔄 Reactive updates ✅ Shared across all usages ✅ Reactivity scoped to component tree
🧠 Simple to use ✅ Just import and call ❌ Needs explicit provide() and inject()
📦 Lazy loading ✅ Easy to setup with flags ❌ Requires extra setup to defer logic
🧭 Scoped behavior ❌ Global only ✅ Scoped per provide tree
🧪 Test isolation ✅ Easy ✅ Good via injection mocking

💡 Use Singleton When…

  • You need global, reactive state (e.g., feature toggles, config)
  • No need for context scoping
  • You want simplicity

💡 Use provide/inject When…

  • You need contextual or scoped dependencies
  • You’re writing reusable components or plugins
  • You want different instances for different trees

🧠 Bonus Tips

  • Want both? Provide the singleton:
    1
    
    app.provide('settings', useSettings())
    
  • Need SSR? Guard global variables and use Pinia or Nuxt’s useAsyncData for hydration.

  • Avoid duplicate fetches: If multiple components mount simultaneously, they could all trigger a fetch. Use a shared promise pattern to ensure only one request runs:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    const data = ref(null) // prevent reasignment
    let fetchPromise: Promise<any> | null = null
    
    function fetchData(force = false) {
      if (!force && data.value) return Promise.resolve(data.value)
      if (!force && fetchPromise) return fetchPromise 
      
      fetchPromise = fetchStuff().then(result => {
        data.value = result
        fetchPromise = null // important: allow future calls
        return result
      })
      
      return fetchPromise
    }
    
    export function useSingletonData() {
      fetchData()
      return { data, refetch: (force = true) => fetchData(force) }
    }
    

    🔐 This guards against racing conditions and ensures consistent data loading.


🚀 Conclusion

Singleton composables offer a simple, powerful way to share reactive state across components. They’re ideal for global data and can be extended with reloaders, setters, or caching logic.

Use provide/inject if your logic depends on component context or if you need multiple isolated instances.

Happy coding 🧑‍💻
— Alex


[alex_rocha] by alex

🇧🇷Senior Software Developer