Skip to content

useNotifications ​

The list behind the navbar bell. A Pinia store, persisted, so the notifications and what has been read survive a reload.

Open in demo

Import ​

ts
import { useNotifications } from '@shared/composables/useNotifications'

Signature ​

It is a Pinia setup store, not a plain composable. Its id is notifications, and it persists itself to localStorage under the key sa-notifications.

Calling it returns the store instance, with the refs already unwrapped, so notifications.items is a Notification[] and notifications.unreadCount a number:

ts
const notifications = useNotifications()

Keep the instance rather than destructuring the state, as the template's own components do. storeToRefs gives you standalone refs when you want them; the actions are safe to destructure on their own.

Members ​

MemberType on the instanceDescription
itemsNotification[]Newest first.
unreadCountnumberWhat the bell's badge shows.
add(n) => voidUnshifts one. The id, timestamp and read flag are filled in.
markRead(id) => voidMarks one read.
markAllRead() => voidMarks every one read.
remove(id) => voidDrops one.
seedDemo() => voidFills the list with five fixed examples, only while it is empty.
$reset() => voidEmpties it.

Notification ​

KeyTypeDescription
idstringA UUID, assigned by add.
type'info' | 'success' | 'warning' | 'error'Drives the icon and its color.
titlestringA translation key or a literal.
descriptionstringThe same.
paramsRecord<string, string>Interpolation values for a keyed title or description.
linkstringWhere clicking it goes.
timestampDateSet by add.
readbooleanStarts false.

Example ​

vue
<script setup lang="ts">
import { useNotifications } from '@shared/composables/useNotifications'

const notifications = useNotifications()

function paymentArrived(invoice: string) {
  notifications.add({
    type: 'success',
    title: 'Payment received',
    description: `Invoice ${invoice} has been paid in full.`,
    link: '/finance/invoices',
  })
}
</script>

<template>
  <div class="space-y-3">
    <button type="button" @click="paymentArrived('INV-2043')">
      Simulate a payment
    </button>

    <p>{{ notifications.unreadCount }} unread</p>

    <ul>
      <li v-for="item in notifications.items" :key="item.id">
        <button type="button" @click="notifications.markRead(item.id)">
          {{ item.title }}
        </button>
      </li>
    </ul>
  </div>
</template>

Notes ​

  • title and description go through uiText. A translation key translates and follows a language switch; any other string, such as one your server sent, renders as it is. That is what lets the same list hold both.
  • timestamp is a Date in memory and an ISO string once persisted. Consumers coerce with new Date(...) before formatting, so a rehydrated string is handled either way. Do the same if you read it yourself.
  • seedDemo only fills an empty list, and its ids are fixed. The store is persisted, so an unconditional seed would stack a fresh copy on every reload, and random ids could not be deduplicated. Its timestamps are offsets from the first seeding, so the relative labels still read sensibly whenever the demo is opened.
  • Persisted under sa-notifications.
  • The whole feature sits behind config.features.notifications.