adrianmaj.com

Payload,  Next.js

PayloadCMS od podstaw #4 - praktyczne zastosowanie hooków

Autor

Adrian Maj

Data opublikowania

PayloadCMS od podstaw #4 - praktyczne zastosowanie hooków

Podczas wykonywania operacji takich jak zapis, tworzenie czy usuwanie rekordów w kolekcjach często potrzebujemy wykonać dodatkowe operacje - zapisać informację w logu, zrewalidować cache czy zintegrować się z zewnętrznymi usługami. Do tego typu operacji, zwanych side-effectami, wykorzystujemy hooki, które są nieodłączną częścią każdej większej aplikacji.

Payload umożliwia tworzenie hooków na czterech różnych poziomach - od poziomu całej aplikacji (Root Hook) po pojedyncze pole w kolekcji (Field Hook). Każdy z tych poziomów otrzymuje inne dane i ma nieco inną implementację, dlatego przyjrzyjmy się każdemu z nich szczegółowo.

Root Hooks

Na chwilę pisania tego wpisu dostępny jest tylko jeden hook tego rodzaju - afterError. Jak sama nazwa wskazuje, wywołuje się w momencie wystąpienia błędu w aplikacji. Root Hooks nie są przypisane do konkretnej kolekcji, lecz działają w obrębie całej aplikacji. Hook afterError można wykorzystać na przykład do raportowania błędów do zewnętrznych serwisów monitorujących.

Aby dodać Root Hook do projektu, należy w obiekcie konfiguracyjnym w pliku payload.config.ts dodać właściwość hooks, która jest obiektem zawierającym nazwy dostępnych hooków. Każdy hook jest tablicą funkcji wykonywanych w momencie jego wywołania. Implementacja wygląda następująco:

1import { buildConfig } from 'payload'
2
3export default buildConfig({
4 // Your payload config
5 hooks: {
6 afterError: [
7 async ({ error }) => {},
8 ],
9 },
10})

Hooki mogą być zarówno synchroniczne, jak i asynchroniczne i przyjmują różne dane w zależności od rodzaju. Więcej na temat hooka afterError i Root Hooks przeczytasz w dokumentacji.

Collection Hooks

Przejdziemy teraz do najczęściej wykorzystywanych hooków - dla kolekcji. Zasada działania jest podobna do Root Hooks, z tym że działają w obrębie jednej kolekcji i tam też są definiowane. Hooków działających na poziomie kolekcji jest znacznie więcej, więc nie będę omawiał każdego z osobna - ich pełną dokumentację znajdziesz tutaj.

Hooki dla kolekcji w większości przypadków przyjmują następujące parametry:

  • collection - kolekcja, na której działa hook
  • doc, originalDoc lub previousDoc - dokument, na którym są wywoływane (w przypadku hooków odpowiedzialnych za zmiany dokumentu dostępne są warianty przed i po operacji)
  • req - obiekt Web Request, dzięki któremu można uzyskać m.in dostęp do Local API
  • context - dodatkowy kontekst pozwalający na przekazywanie danych między różnymi hookami (przydatne np. w celu uniknięcia nieskończonej pętli hooków)

Zobaczmy jak wygląda przykładowy hook:

1import type { CollectionConfig } from "payload";
2
3export const Users: CollectionConfig = {
4 slug: "users",
5 hooks: {
6 afterChange: [
7 async ({ req, collection, doc }) => {
8 const payload = req.payload;
9 const tenant = await payload.findByID({
10 collection: "tenants",
11 id: typeof doc.tenant === "string" ? doc.tenant : doc.tenant.id
12 })
13 // bring your own logic here
14 },
15 ],
16 },
17}

W tym przykładzie hook afterChange wykorzystuje obiekt req do uzyskania dostępu do Local API, a następnie pobiera tenanta przypisanego do użytkownika. Hook wykonuje się po każdej zmianie dowolnego pola w kolekcji - logika wewnątrz hooka jest w pełni konfigurowalna i można ją dostosować do konkretnych wymagań projektu.

💡 Tip: Pola relacyjne w zwracanym dokumencie (zarówno w doc, jak i w danych z Local API) są zawsze typowane jako obiekt docelowej kolekcji LUB string. Wynika to z faktu, że typy nie są generowane na podstawie uprawnień - jeśli brak nam dostępu do referencyjnego dokumentu, otrzymamy jedynie jego ID. Dlatego należy zawsze weryfikować typ, podobnie jak w przykładzie z polem id powyżej.

Collection Hooki są w pełni otypowane, wykorzystując typy generyczne - oznacza to, że podczas tworzenia hooka musimy podać slug lub typ kolekcji (w zależności od hooka), dla której go definiujemy. Oto dwa przykłady różnych sposobów typowania:

  • Typowanie za pomocą slug'u kolekcji
1import { type CollectionAfterOperationHook } from "payload";
2
3export const afterOperationHook: CollectionAfterOperationHook<"collection-slug"> = async ({
4 operation,
5 req,
6 result,
7}) => {
8 // some custom logic
9 return result;
10};
11
  • Typowanie za pomocą typu kolekcji
1import { type CollectionAfterChangeHook } from "payload";
2import { type MyCollection } from "@/payload-types";
3
4export const afterChangeHook: CollectionAfterChangeHook<MyCollection> = async ({
5 doc,
6 previousDoc,
7 req,
8}) => {
9 // some custom logic
10 return doc;
11}

Tak przygotowany hook zapewnia pełne type-safety i pozwala bezproblemowo dodawać własną logikę do istniejących operacji.

Global Hooks

Konfiguracja i zasada działania Global Hooks jest praktycznie identyczna jak w Collection Hooks, ale mamy tutaj znacznie mniej hooków - wynika to z mniejszej liczby operacji możliwych do wykonania na danych globalnych. Jedyną różnicą w parametrach jest zastąpienie collection parametrem global, który zwraca kolekcję globalną obsługującą dany hook.

Ponieważ Global Hooks funkcjonują niemal identycznie jak Collection Hooks, nie ma sensu omawiać ich szczegółowo - jeśli potrzebujesz konkretnych informacji na ich temat, sprawdź dokumentację Payload.

Field Hooks

Kolejny ważny i bardzo często wykorzystywany rodzaj hooków, który pozwala reagować na zmiany w konkretnym polu kolekcji lub globalsa. Field Hooks konfigurujemy na poziomie pojedynczego pola i w przeciwieństwie do Collection Hooks, każdy z nich przyjmuje identyczne parametry - ich szczegółowy opis znajdziesz w dokumentacji.

Warto wspomnieć, że typowanie Field Hooks różni się od pozostałych hooków, w tym przypadku mamy do dyspozycji jeden główny typ - FieldHook⁣, który również jest generyczny, ale przyjmuje aż 3 argumenty:

  1. Document Type - czyli typ kolekcji, w której znajduje się to pole.
  2. Field Type - jeżeli jest to pole tekstowe to będzie to string itd.
  3. Sibling Data Type - czyli typ pól występujących na tym samym poziomie co pole, na którym wywoływany jest hook (np. w tej samej grupie).

Oto jak może wyglądać przykładowy Field Hook:

1import type { FieldHook } from 'payload'
2import { type MyCollection } from "@/payload-types";
3
4type SiblingDataType = {
5 price: number;
6 description: string;
7}
8
9const titleFieldHook: FieldHook<MyCollection, string, SiblingDataType> = (args) => {
10 const {
11 value, // Title - Typed as `string` as shown above
12 data, // Typed as a Partial of MyCollection
13 siblingData, // Typed as a Partial of SiblingDataType
14 originalDoc, // Typed as MyCollection
15 operation,
16 req,
17 } = args
18
19 // Do something here...
20
21 return value;
22}

W tym przykładzie hook obsługuje pole title i dzięki generycznym typom otrzymujemy pełne type-safety dla wszystkich parametrów. Hook musi zwrócić wartość zgodną z typem pola - w tym przypadku string, undefined lub null.

Podsumowanie

Hooki stanowią kluczowy mechanizm rozszerzania domyślnych funkcjonalności Payload, umożliwiając dodawanie własnej logiki do standardowych operacji w prosty i przyjemny sposób. W tym artykule poznałeś cztery rodzaje hooków - od Root Hooks działających w obrębie całej aplikacji po Field Hooks reagujące na zmiany w pojedynczych polach. Dzięki pełnemu typowaniu i generycznym typom możesz bezpiecznie implementować side-effects dopasowane do konkretnych wymagań Twojego projektu.

W kolejnym artykule omówimy równie przydatne Custom Fields, które pozwalają tworzyć własne typy pól wykraczające poza standardowe możliwości Payload. Poznasz również praktyczne zastosowania hooków w połączeniu z Custom Fields oraz sposoby optymalizacji wydajności aplikacji.

Jeśli chcesz kontynuować naukę i zgłębiać temat PayloadCMS, zachęcam do śledzenia mojego bloga. Dodatkowo, na moich profilach w mediach społecznościowych będę informować o nowych wpisach, aktualnościach i projektach związanych z Payload.