Кастомні hooks Payload CMS
Hooks в Payload — основний механізм розширення бізнес-логіки. Вони виконуються на певних етапах життєвого циклу документа: до/після читання, створення, зміни, видалення. Hooks — async-функції TypeScript з повною типізацією.
Типи hooks колекцій
// collections/Orders.ts
const Orders: CollectionConfig = {
slug: 'orders',
hooks: {
beforeOperation: [/* валідація до операції */],
beforeValidate: [/* зміна даних до валідації */],
beforeChange: [/* трансформація даних */],
afterChange: [/* side effects після збереження */],
beforeRead: [/* фільтрація даних при читанні */],
afterRead: [/* збагачення даних після читання */],
beforeDelete: [/* перевірки перед видаленням */],
afterDelete: [/* cleanup після видалення */],
},
}
Hooks beforeChange: трансформація даних
import type { CollectionBeforeChangeHook } from 'payload/types'
const generateOrderNumber: CollectionBeforeChangeHook = async ({
data,
req,
operation,
}) => {
if (operation === 'create') {
// Генерація номера замовлення
const count = await req.payload.count({ collection: 'orders' })
data.orderNumber = `ORD-${String(count + 1).padStart(6, '0')}`
// Встановити автора
if (req.user) {
data.createdBy = req.user.id
}
// Timestamp
data.createdAt = new Date().toISOString()
}
return data
}
const validateStock: CollectionBeforeChangeHook = async ({ data, req }) => {
// Перевірити наявність товарів перед створенням замовлення
for (const item of data.items || []) {
const product = await req.payload.findByID({
collection: 'products',
id: item.product,
})
if (product.stock < item.quantity) {
throw new Error(`Товар "${product.name}": недостатньо на складі`)
}
}
return data
}
Hooks afterChange: side effects
import type { CollectionAfterChangeHook } from 'payload/types'
const sendOrderConfirmation: CollectionAfterChangeHook = async ({
doc,
operation,
req,
}) => {
if (operation === 'create') {
// Відправити email через сервіс
await emailService.send({
to: doc.customerEmail,
subject: `Замовлення #${doc.orderNumber} прийнято`,
template: 'order-confirmation',
data: { order: doc },
})
}
if (operation === 'update' && doc.status === 'shipped') {
await emailService.send({
to: doc.customerEmail,
subject: `Замовлення #${doc.orderNumber} відправлено`,
template: 'order-shipped',
data: { order: doc, trackingNumber: doc.trackingNumber },
})
}
}
const revalidateCache: CollectionAfterChangeHook = async ({ doc }) => {
// Інвалідація Next.js ISR кеша
const paths = [`/products/${doc.slug}`, '/products']
await Promise.all(
paths.map(path =>
fetch(`${process.env.NEXT_PUBLIC_SERVER_URL}/api/revalidate`, {
method: 'POST',
headers: { 'x-revalidate-secret': process.env.REVALIDATE_SECRET! },
body: JSON.stringify({ path }),
})
)
)
}
const syncWithCRM: CollectionAfterChangeHook = async ({ doc, operation }) => {
if (operation === 'create') {
// Створити сделку в CRM
await crmClient.deals.create({
title: `Замовлення #${doc.orderNumber}`,
amount: doc.total,
contactEmail: doc.customerEmail,
})
}
}
Hooks afterRead: збагачення даних
import type { CollectionAfterReadHook } from 'payload/types'
const addComputedFields: CollectionAfterReadHook = async ({ doc }) => {
// Вичислити підсумкові поля, які не зберігаються в БД
if (doc.items) {
doc.subtotal = doc.items.reduce(
(sum: number, item: any) => sum + item.price * item.quantity,
0
)
doc.totalItems = doc.items.length
}
return doc
}
Hooks beforeDelete: захист від видалення
import type { CollectionBeforeDeleteHook } from 'payload/types'
const preventDeleteWithOrders: CollectionBeforeDeleteHook = async ({
id,
req,
}) => {
// Запретити видалення клієнта з активними замовленнями
const orders = await req.payload.find({
collection: 'orders',
where: {
and: [
{ customer: { equals: id } },
{ status: { not_in: ['completed', 'cancelled'] } },
],
},
limit: 1,
})
if (orders.totalDocs > 0) {
throw new Error('Неможливо видалити клієнта з активними замовленнями')
}
}
Hooks Global
const Settings: GlobalConfig = {
slug: 'settings',
hooks: {
afterChange: [
async ({ doc }) => {
// При зміні налаштувань — інвалідувати весь сайт
await fetch('/api/revalidate?path=/', { method: 'POST' })
},
],
},
}
Повторне використання hooks
// hooks/shared/timestamps.ts
import type { CollectionBeforeChangeHook } from 'payload/types'
export const setTimestamps: CollectionBeforeChangeHook = ({ data, operation }) => {
if (operation === 'create') {
data.createdAt = new Date().toISOString()
}
data.updatedAt = new Date().toISOString()
return data
}
// У колекціях:
hooks: {
beforeChange: [setTimestamps, ...otherHooks],
}
Обробка помилок у hooks
const validateHook: CollectionBeforeChangeHook = async ({ data }) => {
try {
await externalValidationService.validate(data)
} catch (error) {
if (error instanceof ValidationError) {
// Payload покаже помилку в admin UI
throw new Error(`Validation failed: ${error.message}`)
}
// Логування неочікуваних помилок без блокування збереження
console.error('External validation error:', error)
}
return data
}
Часові рамки
Набір hooks для бізнес-логіки однієї колекції (email-сповіщення, CRM-синхронізація, валідація) — 1–2 дні.







