Skip to content

Store'y Pinia ​

Wybór między store'em a Vue Query ​

Pinia czy Vue Query — decyzja, od której zależy reszta implementacji:

Rodzaj danychMiejsce przechowywania
Rekordy CRUD z serwera: listy, szczegóły, zapisVue Query
Słowniki i konfiguracja pobierane raz, używane w wielu miejscachPinia
Stan sesji użytkownika, czyli token i uprawnieniaPinia
Stan interfejsu dzielony między komponentami, na przykład otwarte menu czy bieżący katalogPinia
Stan pojedynczego komponentuzwykły ref w komponencie

Danych CRUD nie należy duplikować w store

Przechowywanie listy artykułów w Pinii oznacza konieczność samodzielnej obsługi odświeżania, unieważniania oraz stanów ładowania, czyli ręcznego odtworzenia tego, co zapewnia Vue Query.

Rejestracja ​

Plik src/stores/index.js tworzy wyłącznie instancję Pinii, bez żadnych pluginów:

js
// src/stores/index.js
export default defineStore((/* { ssrContext } */) => {
  const pinia = createPinia();
  return pinia;
});

Store'y modułów nie podlegają auto-rejestracji i importowane są jawnie:

js
import { useAuthStore } from "modules/auth/stores/auth";

const authStore = useAuthStore();

Store'y występujące w projekcie ​

Store (id)PlikOdpowiedzialność
authmodules/auth/stores/auth.jsSesja, token, currentUser, uprawnienia; opisany w Autentykacji
core/localemodules/core/stores/core.jsJęzyk interfejsu, język treści oraz lista dostępnych języków
core/navigation-drawer-side-menumodules/core/stores/core.jsInformacja o rozwiniętym podmenu w nawigacji
blog/categorymodules/blog/stores/category.jsDrzewo kategorii wraz ze zmianą kolejności
media/diskmodules/media/stores/disk.jsDyski, defaultDisk, currentDiskName oraz metoda init()
media/directorymodules/media/stores/directory.jsDrzewo katalogów i bieżący katalog
media/filemodules/media/stores/file.jsKontekst listy plików (additionalParamsWithDirectory)
menu/menumodules/menu/stores/menu.jsTypy menu oraz elementów możliwych do podlinkowania
content-buildermodules/contentBuilder/stores/content-builder.jsTypy sekcji i sectionables

Konwencja identyfikatorów

Identyfikator store'a przyjmuje postać <moduł>/<nazwa>, na przykład media/disk czy blog/category. Wyjątkiem są auth i content-builder, czyli moduły posiadające pojedynczy store.

Dwa style zapisu ​

W kodzie występują oba style oferowane przez Pinię. Wybierając jeden, kieruj się tym, co przeważa w danym module.

Options store, dominujący w projekcie ​

js
// src/modules/media/stores/disk.js
export const useMediaDiskStore = defineStore("media/disk", {
  state: () => ({
    disks: [],
    currentDisk: null,
  }),
  getters: {
    defaultDisk: (state) => state.disks.find((d) => d.is_default),
    currentDiskName: (state) => state.currentDisk?.name,
  },
  actions: {
    async fetchDisks() {
      const { data } = await api.get("media/disks");
      this.disks = data.results;
      return data.results;
    },
    async init() {
      if (!this.disks.length) {
        const results = await this.fetchDisks();
        this.currentDisk = results.find((d) => d.is_active && d.is_default);
        return results;
      }
    },
  },
});

Wzorzec metody init()

Metoda init() pobiera dane wyłącznie wtedy, gdy store pozostaje pusty. Pozwala to wywoływać ją z wielu miejsc bez ryzyka zdublowanych żądań. Plik MasterLayout.vue korzysta z niej w funkcji preFetch.

Setup store ​

js
// src/modules/core/stores/core.js
export const useNavigationDrawerSideMenuStore = defineStore(
  "core/navigation-drawer-side-menu",
  () => {
    const item = useLocalStorage("core/navigation-drawer-side-menu/item", {});

    function setItem(newItem) {
      if (item.value?.key !== newItem?.key) {
        item.value = newItem;
        return item.value;
      }
      item.value = null;   // ponowne kliknięcie zamyka
    }

    return { item, setItem };
  },
);

Persystencja ​

W projekcie nie zastosowano pluginu persystencji — każdy store zapisuje stan samodzielnie:

MechanizmStorePrzechowywane dane
LocalStorage (Quasar)authtoken, lastEmail, wasLoggedIn
Cookies (Quasar)core/localelocale, content_locale, ważność 5 lat
useLocalStorage (VueUse)core/navigation-drawer-side-menurozwinięta pozycja menu

Store języka stanowi przykład zapisu w trzy miejsca jednocześnie:

js
// src/modules/core/stores/core.js
setLocale(locale) {
  const lang = this.getLocaleByCode(locale);

  if (lang) {
    Cookies.set("locale", locale, { expires: 365 * 5 });  // 1. ciasteczko
    api.setLocale(locale);                                 // 2. parametr w axiosie
    setQuasarLang(lang.iso);                               // 3. pakiet językowy Quasara
    this.locale = locale;
  }
}

Lista dostępnych języków jest zaszyta w kodzie

Tablice locales i contentLocales w pliku modules/core/stores/core.js mają charakter statyczny; obecnie zawierają wyłącznie pl, a wpis en pozostaje zakomentowany. Dodanie języka wymaga edycji tego pliku oraz utworzenia katalogu tłumaczeń, co opisują Tłumaczenia.

Store korzystający z endpointów modułu ​

Store nie musi wywoływać api bezpośrednio — może sięgać po warstwę endpointów:

js
// src/modules/blog/stores/category.js
import { getCategoriesTree, reorderCategories } from "modules/blog/api/endpoints/category";

export const useBlogCategoryStore = defineStore("blog/category", {
  state: () => ({ categoryTree: [] }),
  actions: {
    async fetchCategoryTree(params) {
      const { data } = await getCategoriesTree({
        ...params,
        order_by: params?.order_by || "order",
        order_direction: params?.order_direction || "ASC",
      });

      this.categoryTree = data.results;
      return data;
    },
    async reorder(sortedCategories) {
      return reorderCategories({ tree: sortedCategories });
    },
  },
});

To zarazem przykład uzasadnionego użycia Pinii do danych z serwera: drzewo kategorii przestawia się metodą przeciągnij i upuść, więc komponent potrzebuje lokalnie mutowalnej kopii, a nie cache przeznaczonego tylko do odczytu.

Store zależny od innego store'a ​

Wywołanie useXStore() wewnątrz gettera albo akcji jest w pełni poprawne:

js
// src/modules/media/stores/file.js
getters: {
  additionalParamsWithDirectory(state) {
    const directoryStore = useMediaDirectoryStore();

    return {
      directory: directoryStore.currentDirectory.path,
      ...state.additionalParams,
    };
  },
},

Wywołania useXStore() na poziomie modułu są niedozwolone

Wywołanie umieszczone poza getterem lub akcją wykona się w momencie importu pliku, czyli zanim Pinia zostanie zainstalowana, co przerywa działanie aplikacji. Musi więc znaleźć się wewnątrz funkcji.

Made by Moonbite