PayloadCMS od podstaw #4 - praktyczne zastosowanie hooków
Autor
Adrian Maj
Data opublikowania

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'23export default buildConfig({4 // Your payload config5 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 hookdoc,originalDoclubpreviousDoc- 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 APIcontext- 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";23export 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.id12 })13 // bring your own logic here14 },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";23export const afterOperationHook: CollectionAfterOperationHook<"collection-slug"> = async ({4 operation,5 req,6 result,7}) => {8 // some custom logic9 return result;10};11
- Typowanie za pomocą typu kolekcji
1import { type CollectionAfterChangeHook } from "payload";2import { type MyCollection } from "@/payload-types";34export const afterChangeHook: CollectionAfterChangeHook<MyCollection> = async ({5 doc,6 previousDoc,7 req,8}) => {9 // some custom logic10 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:
- Document Type - czyli typ kolekcji, w której znajduje się to pole.
- Field Type - jeżeli jest to pole tekstowe to będzie to string itd.
- 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";34type SiblingDataType = {5 price: number;6 description: string;7}89const titleFieldHook: FieldHook<MyCollection, string, SiblingDataType> = (args) => {10 const {11 value, // Title - Typed as `string` as shown above12 data, // Typed as a Partial of MyCollection13 siblingData, // Typed as a Partial of SiblingDataType14 originalDoc, // Typed as MyCollection15 operation,16 req,17 } = args1819 // Do something here...2021 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.

Dowiedz się, jak zarządzać użytkownikami oraz określać dostępy do danych w PayloadCMS. Poznaj różne typy użytkowników, i wykorzystaj ich potencjał.

Dowiedz się, jak tworzyć i zarządzać kolekcjami oraz danymi globalnymi w PayloadCMS. Poznaj ich różnice, i skutecznie strukturyzuj treści.