Możesz zaimportować useCounter w kilku różnych komponentach, a każdy z nich będzie miał swój własny, niezależny licznik. Cała logika licznika jest poza komponentem wizualnym.
composable.ts
import { ref } from 'vue'
export function useCounter() {
// Stan (reaktywna zmienna)
const count = ref(0)
// Logika (funkcja zmieniająca stan)
function increment() {
count.value++
}
// Zwracasz to, co będzie potrzebne w komponencie
return {
count,
increment,
}
}App.vue
<script setup>
import { useCounter } from './useCounter.js'
// Wyciągsz ze środka stan i funkcję
const { count, increment } = useCounter()
</script>
<template>
<div>
<p>Licznik: {{ count }}</p>
<button @click="increment">Zwiększ o 1</button>
</div>
</template>import { ref, onMounted, onUnmounted } from 'vue'
export function useMouse() {
const x = ref(0)
const y = ref(0)
function update(event) {
x.value = event.pageX
y.value = event.pageY
}
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
return { x, y }
}<script setup>
import { useMouse } from './useMouse.js'
const { x, y } = useMouse()
</script>
<template>
<p>Pozycja myszy: X: {{ x }}, Y: {{ y }}</p>
</template>import { ref } from 'vue'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
const loading = ref(true)
fetch(url)
.then((res) => res.json())
.then((json) => (data.value = json))
.catch((err) => (error.value = err))
.finally(() => (loading.value = false))
return { data, error, loading }
}import { ref } from 'vue'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
const loading = ref(true)
async function fetchData() {
loading.value = true
error.value = null
try {
const res = await fetch(url)
if (!res.ok) {
throw new Error(`Błąd sieci: ${res.status}`)
}
data.value = await res.json()
} catch (err) {
error.value = err
} finally {
loading.value = false
}
}
fetchData() // Request
return { data, error, loading, refresh: fetchData }
}<script setup>
import { useFetch } from './useFetch.js'
const { data: users, loading, error } = useFetch('https://dummyjson.com/todos')
</script>
<template>
<div v-if="loading">Ładowanie użytkowników...</div>
<div v-else-if="error">Wystąpił błąd: {{ error.message }}</div>
<ul v-else>
<li v-for="user in users" :key="user.id">{{ user.name }}</li>
</ul>
</template>useFetch.js
import { ref, watchEffect, toValue } from 'vue'
export function useFetch(urlSource) {
const data = ref(null)
const error = ref(null)
const loading = ref(true)
async function fetchData() {
loading.value = true
error.value = null
try {
// toValue(urlSource) bezpiecznie wyciąga string
// niezależnie od tego, czy przekazano ref, funkcję, czy zwykły tekst
const url = toValue(urlSource)
const response = await fetch(url)
if (!response.ok) {
throw new Error(`Błąd: ${response.status}`)
}
data.value = await response.json()
} catch (err) {
error.value = err
} finally {
loading.value = false
}
}
// watchEffect automatycznie uruchomi fetchData na starcie
// oraz za każdym razem, gdy urlSource (jeśli to ref/funkcja) się zmieni
watchEffect(() => {
fetchData()
})
return { data, error, loading, refresh: fetchData }
}<script setup>
import { ref, computed } from 'vue'
import { useFetch } from './useFetch.js'
const userId = ref(1)
const url = computed(() => `https://dummyjson.com/todos/${userId.value}`)
// Przekazujemy funkcję computed (reaktywne źródło) do Composable
const { data: user, loading, error, refresh } = useFetch(url)
</script>
<template>
<div>
<button @click="userId++">
Następny użytkownik (Aktualne ID: {{ userId }})
</button>
<button @click="refresh" :disabled="loading">Odśwież</button>
<hr />
<div v-if="loading">Ładowanie danych użytkownika...</div>
<div v-else-if="error">Błąd: {{ error.message }}</div>
<div v-else-if="user">
<h3>{{ user.name }}</h3>
<p>Email: {{ user.email }}</p>
</div>
</div>
</template>Sytuacja race condition występuje wtedy, gdy użytkownik kliknie przycisk kilka razy bardzo szybko.
// useFetch.js
import { ref, watchEffect, toValue, onScopeDispose } from 'vue'
export function useFetch(urlSource) {
const data = ref(null)
const error = ref(null)
const loading = ref(true)
// Zmienna, w której przechowamy aktualny kontroler anulowania
let controller = null
async function fetchData() {
// 1. Jeśli trwa jakieś poprzednie zapytanie, anulujemy je!
if (controller) {
controller.abort()
}
// 2. Tworzymy nowy kontroler dla bieżącego zapytania
controller = new AbortController()
loading.value = true
error.value = null
try {
const url = toValue(urlSource)
// 3. Przekazujemy sygnał (signal) do funkcji fetch
const response = await fetch(url, { signal: controller.signal })
if (!response.ok) {
throw new Error(`Błąd: ${response.status}`)
}
data.value = await response.json()
} catch (err) {
// 4. Ignorujemy błąd, jeśli wynika on z celowego anulowania zapytania
if (err.name === 'AbortError') {
return
}
error.value = err
} finally {
// 5. Czyścimy flagę ładowania tylko, jeśli to zapytanie nie zostało anulowane
if (!controller.signal.aborted) {
loading.value = false
}
}
}
watchEffect(() => {
fetchData()
})
// 6. Jeśli komponent zostanie usunięty z ekranu (unmounted), automatycznie przerywamy trwające w tle pobieranie.
onScopeDispose(() => {
if (controller) controller.abort()
})
return { data, error, loading, refresh: fetchData }
}useCart.js
import { ref, computed } from 'vue'
// 1. STAN GLOBALNY (tworzony tylko raz przy imporcie pliku)
const cart = ref([])
const isCartOpen = ref(false)
// 2. FUNKCJA COMPOSABLE
export function useCart() {
// Gettery (computed) bazujące na stanie globalnym
const cartItemsCount = computed(() => {
return cart.value.reduce((total, item) => total + item.quantity, 0)
})
const totalPrice = computed(() => {
return cart.value.reduce(
(total, item) => total + item.price * item.quantity,
0,
)
})
// Metody zmieniające stan globalny
function addToCart(product) {
const existingItem = cart.value.find((item) => item.id === product.id)
if (existingItem) {
existingItem.quantity++
} else {
cart.value.push({ ...product, quantity: 1 })
}
}
function removeFromCart(productId) {
cart.value = cart.value.filter((item) => item.id !== productId)
}
function toggleCart() {
isCartOpen.value = !isCartOpen.value
}
// Zwracamy stan i metody do komponentów
return {
cart: readonly(cart),
isCartOpen,
cartItemsCount,
totalPrice,
addToCart,
removeFromCart,
toggleCart,
}
}<script setup>
import { useCart } from './useCart.js'
// Wyciągamy tylko to, co potrzebne w nagłówku
const { cartItemsCount, toggleCart } = useCart()
</script>
<template>
<header>
<h1>Mój Sklep</h1>
<button @click="toggleCart">🛒 Koszyk ({{ cartItemsCount }})</button>
</header>
</template><script setup>
import { useCart } from './useCart.js'
const { addToCart } = useCart()
const products = [
{ id: 101, name: 'Buty sportowe', price: 299 },
{ id: 102, name: 'Koszulka bawełniana', price: 79 },
]
</script>
<template>
<div class="products">
<div v-for="product in products" :key="product.id">
<h3>{{ product.name }} - {{ product.price }} zł</h3>
<button @click="addToCart(product)">Dodaj do koszyka</button>
</div>
</div>
</template>CartService.js
import { ref, computed } from 'vue'
class CartService {
// 1. Prywatna statyczna instancja (rdzeń Singletona)
static #instance = null
constructor() {
if (CartService.#instance) {
return CartService.#instance
}
// Inicjalizacja stanu (reaktywnego)
this.cart = ref([])
// Gettery jako computed
this.cartItemsCount = computed(() => {
return this.cart.value.reduce((total, item) => total + item.quantity, 0)
})
CartService.#instance = this
}
// Metody
addToCart(product) {
const existingItem = this.cart.value.find((item) => item.id === product.id)
if (existingItem) {
existingItem.quantity++
} else {
this.cart.value.push({ ...product, quantity: 1 })
}
}
}
// Eksportujemy zawsze jedną, tę samą instancję
export const cartService = new CartService()<script setup>
import { cartService } from './CartService.js'
// Przypisujemy do zmiennych, aby użyć w szablonie
const { cartItemsCount, addToCart } = cartService
</script>