176 lines
3.8 KiB
Markdown
176 lines
3.8 KiB
Markdown
---
|
|
title: Hooks
|
|
description: Collection hooks, field hooks, and context patterns
|
|
tags: [payload, hooks, lifecycle, context]
|
|
---
|
|
|
|
# Payload CMS Hooks
|
|
|
|
## Collection Hooks
|
|
|
|
```typescript
|
|
export const Posts: CollectionConfig = {
|
|
slug: 'posts',
|
|
hooks: {
|
|
// Before validation - format data
|
|
beforeValidate: [
|
|
async ({ data, operation }) => {
|
|
if (operation === 'create') {
|
|
data.slug = slugify(data.title)
|
|
}
|
|
return data
|
|
},
|
|
],
|
|
|
|
// Before save - business logic
|
|
beforeChange: [
|
|
async ({ data, req, operation, originalDoc }) => {
|
|
if (operation === 'update' && data.status === 'published') {
|
|
data.publishedAt = new Date()
|
|
}
|
|
return data
|
|
},
|
|
],
|
|
|
|
// After save - side effects
|
|
afterChange: [
|
|
async ({ doc, req, operation, previousDoc, context }) => {
|
|
// Check context to prevent loops
|
|
if (context.skipNotification) return
|
|
|
|
if (operation === 'create') {
|
|
await sendNotification(doc)
|
|
}
|
|
return doc
|
|
},
|
|
],
|
|
|
|
// After read - computed fields
|
|
afterRead: [
|
|
async ({ doc, req }) => {
|
|
doc.viewCount = await getViewCount(doc.id)
|
|
return doc
|
|
},
|
|
],
|
|
|
|
// Before delete - cascading deletes
|
|
beforeDelete: [
|
|
async ({ req, id }) => {
|
|
await req.payload.delete({
|
|
collection: 'comments',
|
|
where: { post: { equals: id } },
|
|
req, // Important for transaction
|
|
})
|
|
},
|
|
],
|
|
},
|
|
}
|
|
```
|
|
|
|
## Field Hooks
|
|
|
|
```typescript
|
|
import type { FieldHook } from 'payload'
|
|
|
|
const beforeValidateHook: FieldHook = ({ value }) => {
|
|
return value.trim().toLowerCase()
|
|
}
|
|
|
|
const afterReadHook: FieldHook = ({ value, req }) => {
|
|
// Hide email from non-admins
|
|
if (!req.user?.roles?.includes('admin')) {
|
|
return value.replace(/(.{2})(.*)(@.*)/, '$1***$3')
|
|
}
|
|
return value
|
|
}
|
|
|
|
{
|
|
name: 'email',
|
|
type: 'email',
|
|
hooks: {
|
|
beforeValidate: [beforeValidateHook],
|
|
afterRead: [afterReadHook],
|
|
},
|
|
}
|
|
```
|
|
|
|
## Hook Context
|
|
|
|
Share data between hooks or control hook behavior using request context:
|
|
|
|
```typescript
|
|
export const Posts: CollectionConfig = {
|
|
slug: 'posts',
|
|
hooks: {
|
|
beforeChange: [
|
|
async ({ context }) => {
|
|
context.expensiveData = await fetchExpensiveData()
|
|
},
|
|
],
|
|
afterChange: [
|
|
async ({ context, doc }) => {
|
|
// Reuse from previous hook
|
|
await processData(doc, context.expensiveData)
|
|
},
|
|
],
|
|
},
|
|
}
|
|
```
|
|
|
|
## Next.js Revalidation Pattern
|
|
|
|
```typescript
|
|
import type { CollectionAfterChangeHook } from 'payload'
|
|
import { revalidatePath } from 'next/cache'
|
|
|
|
export const revalidatePage: CollectionAfterChangeHook = ({
|
|
doc,
|
|
previousDoc,
|
|
req: { payload, context },
|
|
}) => {
|
|
if (!context.disableRevalidate) {
|
|
if (doc._status === 'published') {
|
|
const path = doc.slug === 'home' ? '/' : `/${doc.slug}`
|
|
payload.logger.info(`Revalidating page at path: ${path}`)
|
|
revalidatePath(path)
|
|
}
|
|
|
|
// Revalidate old path if unpublished
|
|
if (previousDoc?._status === 'published' && doc._status !== 'published') {
|
|
const oldPath = previousDoc.slug === 'home' ? '/' : `/${previousDoc.slug}`
|
|
revalidatePath(oldPath)
|
|
}
|
|
}
|
|
return doc
|
|
}
|
|
```
|
|
|
|
## Date Field Auto-Set
|
|
|
|
```typescript
|
|
{
|
|
name: 'publishedOn',
|
|
type: 'date',
|
|
hooks: {
|
|
beforeChange: [
|
|
({ siblingData, value }) => {
|
|
if (siblingData._status === 'published' && !value) {
|
|
return new Date()
|
|
}
|
|
return value
|
|
},
|
|
],
|
|
},
|
|
}
|
|
```
|
|
|
|
## Best Practices
|
|
|
|
- Use `beforeValidate` for data formatting
|
|
- Use `beforeChange` for business logic
|
|
- Use `afterChange` for side effects
|
|
- Use `afterRead` for computed fields
|
|
- Store expensive operations in `context`
|
|
- Pass `req` to nested operations for transaction safety
|
|
- Use context flags to prevent infinite loops
|