Centralized Vuetify Snackbar

Vuetify offers the <v-snackbar> component which is often used as follows in applications:

<template>
  <v-btn
    prepend-icon="mdi-content-save"
    text="Save"
    @click="save()"
    :loading="saving"
    color="primary"
  />

  <v-snackbar :color="snackbar.color" v-model="snackbar.show">
    {{ snackbar.text }}
  </v-snackbar>
</template>

<script lang="ts" setup>
  const saving = ref(false);

  const snackbar = ref({
    color: "success",
    show: false,
    text: "",
  });

  async function updateFood() {
    saving.value = true;

    try {
      await $fetch(`/api/items`, {
        method: "POST",
        body: {
          // ...
        },
      });
      snackbar.value.show = true;
      snackbar.value.text = "Saved successfully";
      snackbar.value.color = "success";
    } catch (error) {
      console.error(error);
      snackbar.value.show = true;
      snackbar.value.text = "Save failed";
      snackbar.value.color = "error";
    } finally {
      saving.value = false;
    }
  }
</script>

Having to repeat this pattern in every component that uses a snackbar creates a lot of code duplication. To solve this problem, one can introduce a centralized snackbar controlled by a composable.

<template>
  <v-app>
    <v-main>
      <!-- ... -->
    </v-main>
    <v-snackbar
      :text="snackbar.text"
      v-model="snackbar.show"
      :color="snackbar.color"
    />
  </v-app>
</template>
<script setup>
  const { snackbar } = useSnackbar();
</script>

The useSnackbar composable can be defined as follows:

// composables/useSnackBar.ts
export function useSnackbar() {
  const snackbar = useState("snackbar", () => ({
    show: false,
    text: "",
    color: "success" as "success" | "error",
  }));

  function notify(text: string, color: "success" | "error" = "success") {
    snackbar.value = { show: true, text, color };
  }

  return { snackbar, notify };
}

The snackbar’s visibility can then be triggered from any component using the notify function, which greatly simplifies the example above:

<template>
  <v-btn
    prepend-icon="mdi-content-save"
    text="Save"
    @click="save()"
    :loading="saving"
    color="primary"
  />
</template>

<script lang="ts" setup>
  const saving = ref(false);

  async function updateFood() {
    saving.value = true;

    try {
      await $fetch(`/api/items`, {
        method: "POST",
        body: {
          // ...
        },
      });
      notify("Saved successfully");
    } catch (error) {
      console.error(error);
      notify("Save failed", "error");
    } finally {
      saving.value = false;
    }
  }
</script>