Skip to content

Instantly share code, notes, and snippets.

@atomjoy
Last active September 4, 2026 08:19
Show Gist options
  • Select an option

  • Save atomjoy/e3d621f917df2c0d6afcb53068a5ab11 to your computer and use it in GitHub Desktop.

Select an option

Save atomjoy/e3d621f917df2c0d6afcb53068a5ab11 to your computer and use it in GitHub Desktop.
Composable przykłady

Composable

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.

Tworzenie

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,
  }
}

Komponent

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>

Przykład z myszką

useMouse.js

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 }
}

Mouse.vue

<script setup>
import { useMouse } from './useMouse.js'

const { x, y } = useMouse()
</script>

<template>
  <p>Pozycja myszy: X: {{ x }}, Y: {{ y }}</p>
</template>

Przykład fetch

useFetch.js

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 }
}

useAsyncFetch.js

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 }
}

Fetch.vue

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

Rozbudowany przykład

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 }
}

Comp3.vue

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

Wyścig odpowiedzi

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 }
}

Przechowywanie stanu (globalny koszyk zakupowy)

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,
  }
}

Comp1.vue

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

Comp2.vue

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

Singleton

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()

app.vue

<script setup>
import { cartService } from './CartService.js'

// Przypisujemy do zmiennych, aby użyć w szablonie
const { cartItemsCount, addToCart } = cartService
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment