Back to Rules
Vue.js93% popularity

Vue 3 Composables Pattern

Extract and reuse stateful logic with Vue 3 Composables using the Composition API.

Evan YouUpdated Mar 18, 2024

Overview

Vue 3 Composables represent the recommended pattern for extracting and reusing stateful logic in Vue applications. They are functions that use Vue Composition API primitives to encapsulate and reuse logic with reactive state. Unlike mixins in Vue 2, Composables provide explicit dependencies, no naming collisions, and full TypeScript support. A composable typically returns a reactive object containing state and functions. By convention, composable function names start with 'use' (useAuth, useFetch, useLocalStorage), making them easily identifiable as composition logic. They can be called from setup() or <script setup> blocks and participate in Vue's reactivity system fully. The lifecycle of state within composables mirrors the component lifecycle when used correctly. Using watchEffect within a composable ensures cleanup when the component unmounts. The onUnmounted hook can register cleanup functions. Composables should be careful about creating memory leaks through event listeners or intervals that aren't properly cleaned up. Composables can composed together to build complex logic from simpler pieces. useAuth might internally use useLocalStorage and useFetch. This composition enables building rich features from focused, testable building blocks. The resulting code is easier to understand than equivalent logic scattered across components.

Code Example

.vuerules
import { ref, computed, watch, onMounted, onUnmounted } from 'vue';
import { useLocalStorage } from './useLocalStorage';
import { useFetch } from './useFetch';

export function useAuth() {
  const { set: setToken, remove: removeToken, get: getToken } = useLocalStorage('auth_token');
  const { data: user, execute: fetchUser, loading, error } = useFetch('/api/auth/me');

  const token = ref(getToken() || null);
  const isAuthenticated = computed(() => !!token.value);
  const isLoading = ref(false);

  const login = async (email: string, password: string) => {
    isLoading.value = true;
    error.value = null;

    try {
      const response = await fetch('/api/auth/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email, password }),
      });

      if (!response.ok) {
        throw new Error('Invalid credentials');
      }

      const { access_token } = await response.json();
      token.value = access_token;
      setToken(access_token);

      await fetchUser();
      return true;
    } catch (e) {
      error.value = e instanceof Error ? e.message : 'Login failed';
      return false;
    } finally {
      isLoading.value = false;
    }
  };

  const logout = () => {
    token.value = null;
    removeToken();
    user.value = null;
  };

  return {
    token,
    user,
    isAuthenticated,
    isLoading,
    error,
    login,
    logout,
    fetchUser,
  };
}

export function useLocalStorage(key: string, defaultValue?: any) {
  const storedValue = localStorage.getItem(key);
  const data = ref(storedValue ? JSON.parse(storedValue) : defaultValue);

  watch(
    data,
    (newValue) => {
      if (newValue === null || newValue === undefined) {
        localStorage.removeItem(key);
      } else {
        localStorage.setItem(key, JSON.stringify(newValue));
      }
    },
    { deep: true }
  );

  const set = (value: any) => {
    data.value = value;
  };

  const get = () => data.value;

  const remove = () => {
    data.value = null;
    localStorage.removeItem(key);
  };

  return { data, set, get, remove };
}

export function useFetch<T>(url: string) {
  const data = ref<T | null>(null);
  const error = ref<Error | null>(null);
  const loading = ref(false);

  let abortController: AbortController | null = null;

  const execute = async (options?: RequestInit) => {
    abortController?.abort();
    abortController = new AbortController();

    loading.value = true;
    error.value = null;

    try {
      const response = await fetch(url, {
        ...options,
        signal: abortController.signal,
      });

      if (!response.ok) {
        throw new Error('HTTP ' + response.status + ': ' + response.statusText);
      }

      data.value = await response.json();
      return data.value;
    } catch (e) {
      if (e instanceof Error && e.name !== 'AbortError') {
        error.value = e;
      }
    } finally {
      loading.value = false;
    }
  };

  onUnmounted(() => {
    abortController?.abort();
  });

  return { data, error, loading, execute };
}

export function useDebounce<T>(value: T, delay: number) {
  const debouncedValue = ref(value) as Ref<T>;
  let timeout: ReturnType<typeof setTimeout>;

  watch(
    () => value,
    (newValue) => {
      clearTimeout(timeout);
      timeout = setTimeout(() => {
        debouncedValue.value = newValue;
      }, delay);
    }
  );

  onUnmounted(() => {
    clearTimeout(timeout);
  });

  return debouncedValue;
}

More Vue.js Rules

VUE
91%

Vue 3 Composition API Patterns

Prefer Composition API with script setup for better TypeScript support and reusable logic patterns.

vue3composition-apitypescript
<script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue';
import { useAuth } from '@/composables/useAuth';
import { useTodos } f...
Feb 14, 2024by Evan You
View Rule
VUE
89%

Vue 3 Pinia State Management

Implement centralized state management with Pinia for Vue 3 applications.

vue3piniastate-management
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import type { User, Project } from '@/types';
import { userApi } from '@/api...
Mar 1, 2024by Eduardo San Martin Morote
View Rule
VUE
85%

Vue 3 Routing with Vue Router 4

Implement navigation guards, lazy loading, and advanced routing patterns in Vue 3.

vue3vue-routerrouting
<script setup lang="ts">
import { useRouter, useRoute } from 'vue-router';
import { computed, onMounted } from 'vue';
import { useAuthStore } from '@/...
Feb 5, 2024by Eduardo San Martin Morote
View Rule