Files
convex-monorepo/.claude/skills/payload/reference/HOOKS.md
T

195 lines
4.3 KiB
Markdown

# Payload CMS Hooks Reference
Complete reference for collection hooks, field hooks, and hook context patterns.
## Collection Hooks
```ts
export const Posts: CollectionConfig = {
slug: 'posts',
hooks: {
// Before validation
beforeValidate: [
async ({ data, operation }) => {
if (operation === 'create') {
data.slug = slugify(data.title);
}
return data;
},
],
// Before save
beforeChange: [
async ({ data, req, operation, originalDoc }) => {
if (operation === 'update' && data.status === 'published') {
data.publishedAt = new Date();
}
return data;
},
],
// After save
afterChange: [
async ({ doc, req, operation, previousDoc }) => {
if (operation === 'create') {
await sendNotification(doc);
}
return doc;
},
],
// After read
afterRead: [
async ({ doc, req }) => {
doc.viewCount = await getViewCount(doc.id);
return doc;
},
],
// Before delete
beforeDelete: [
async ({ req, id }) => {
await cleanupRelatedData(id);
},
],
},
};
```
## Field Hooks
```ts
import type { EmailField, 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;
};
const emailField: EmailField = {
name: 'email',
type: 'email',
hooks: {
beforeValidate: [beforeValidateHook],
afterRead: [afterReadHook],
},
};
```
## Hook Context
Share data between hooks or control hook behavior using request context:
```ts
import type { CollectionConfig } from 'payload';
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);
},
],
},
fields: [{ name: 'title', type: 'text' }],
};
```
## Next.js Revalidation with Context Control
```ts
import type {
CollectionAfterChangeHook,
CollectionAfterDeleteHook,
} from 'payload';
import { revalidatePath } from 'next/cache';
import type { Page } from '../payload-types';
export const revalidatePage: CollectionAfterChangeHook<Page> = ({
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}`;
payload.logger.info(`Revalidating old page at path: ${oldPath}`);
revalidatePath(oldPath);
}
}
return doc;
};
export const revalidateDelete: CollectionAfterDeleteHook<Page> = ({
doc,
req: { context },
}) => {
if (!context.disableRevalidate) {
const path = doc?.slug === 'home' ? '/' : `/${doc?.slug}`;
revalidatePath(path);
}
return doc;
};
```
## Date Field Auto-Set
Automatically set date when document is published:
```ts
import type { DateField } from 'payload';
const publishedOnField: DateField = {
name: 'publishedOn',
type: 'date',
admin: {
date: {
pickerAppearance: 'dayAndTime',
},
position: 'sidebar',
},
hooks: {
beforeChange: [
({ siblingData, value }) => {
if (siblingData._status === 'published' && !value) {
return new Date();
}
return value;
},
],
},
};
```
## Hook Patterns 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 (see [ADAPTERS.md#threading-req-through-operations](ADAPTERS.md#threading-req-through-operations))