obsidian/wiki/payloadcms/hooks-fields.md
2026-05-15 15:51:51 +01:00

164 lines
5.7 KiB
Markdown

---
title: "Field Hooks"
aliases: [field-hooks, payload-field-hooks]
tags: [payloadcms, hooks, fields, lifecycle]
sources: [raw/hooks__fields.md]
created: 2026-05-15
updated: 2026-05-15
---
Field Hooks run on individual fields during Document lifecycle events — allowing isolated, per-field logic separate from [[wiki/payloadcms/hooks-collections|Collection Hooks]].
## Config
Add to any field's `hooks` property:
```ts
const field: Field = {
name: 'username',
type: 'text',
hooks: {
beforeValidate: [(args) => { ... }],
beforeChange: [(args) => { ... }],
beforeDuplicate:[(args) => { ... }],
afterChange: [(args) => { ... }],
afterRead: [(args) => { ... }],
},
}
```
Each hook type accepts an **array** of sync or async functions. The return value replaces the field value for that operation.
> **GraphQL warning:** Changing the *type* of data returned from a field breaks GraphQL. Use [[wiki/payloadcms/hooks-collections|Collection Hooks]] for type changes.
## Hook Types
| Hook | Runs | Use for |
|------|------|---------|
| `beforeValidate` | create + update, after client validation | Normalise/format input before server validation |
| `beforeChange` | create + update, before validation | Transform or gate data; data is unvalidated here |
| `afterChange` | create + update, after save | Side effects, logging, downstream triggers |
| `afterRead` | read operations | Format for output (dates, masks, derived values) |
| `beforeDuplicate` | document duplication | Avoid unique constraint violations; runs before `beforeValidate` |
### Hook Execution Order (create/update)
1. `beforeValidate` (client → server)
2. `validate` (server)
3. `beforeChange`
4. DB write
5. `afterChange`
On read: `afterRead`.
On duplicate: `beforeDuplicate``beforeValidate``beforeChange`.
## Arguments Reference
All field hooks receive the same args object:
| Arg | Type | Notes |
|-----|------|-------|
| `value` | any | Current field value |
| `data` | Partial\<Doc\> | Incoming data (create/update) or full doc (afterRead) |
| `originalDoc` | Doc | Doc before changes (update); resulting doc (afterChange) |
| `previousDoc` | Doc | Doc before changes in `afterChange` |
| `previousValue` | any | Previous field value — `beforeChange` and `afterChange` only |
| `siblingData` | object | Adjacent field values |
| `siblingFields` | Field[] | Adjacent field definitions |
| `previousSiblingDoc` | object | Sibling data before changes — `beforeChange`/`afterChange` only |
| `siblingDocWithLocales` | object | Sibling data with all locales |
| `operation` | string | `'create'` \| `'update'` — useful for branching logic |
| `field` | Field | Field definition |
| `path` | string[] | Runtime path |
| `schemaPath` | string[] | Schema-level path |
| `collection` | Collection \| null | null if field belongs to a Global |
| `global` | Global \| null | null if field belongs to a Collection |
| `findMany` | boolean | `afterRead` only — differentiates find-one vs find-many |
| `overrideAccess` | boolean | Whether access control is bypassed |
| `context` | object | Shared context across hooks — see [[wiki/payloadcms/hooks-context|Hooks Context]] |
| `req` | Request | Web request (mocked for Local API) |
## Common Patterns
### Normalise input (beforeValidate)
```ts
beforeValidate: [
({ value }) => value?.trim().toLowerCase(),
],
```
### Branch on operation (beforeChange)
```ts
beforeChange: [
({ value, operation }) => {
if (operation === 'create') { /* extra logic */ }
return value
},
],
```
### Log on change (afterChange)
```ts
afterChange: [
({ value, previousValue, req }) => {
if (value !== previousValue) {
console.log(`Changed: ${previousValue}${value}`)
}
},
],
```
### Format for display (afterRead)
```ts
afterRead: [
({ value }) => new Date(value).toLocaleDateString(),
],
```
### Deduplicate on copy (beforeDuplicate)
```ts
beforeDuplicate: [
({ value }) => (value ?? 0) + 1,
],
```
Default behaviour for text fields: Payload appends `" - Copy"` unless you define your own `beforeDuplicate` hook.
## TypeScript
```ts
import type { FieldHook } from 'payload'
import type { Post } from '@/payload-types'
// Three generics: <DocumentType, ValueType, SiblingDataType>
type PostTitleHook = FieldHook<Post, string, Post>
const slugifyTitle: PostTitleHook = ({ value, siblingData }) => {
if (!siblingData.slug && value) {
return value.toLowerCase().replace(/[^\w\s-]/g, '').replace(/\s+/g, '-')
}
return value
}
```
Use all three generics for full autocomplete on `value`, `data`, `siblingData`, and `originalDoc`.
## Key Takeaways
- Field hooks are the preferred place to **isolate per-field logic** — keeps Collection hooks clean.
- `operation` arg lets you branch `beforeChange`/`afterChange` between create and update.
- `beforeChange` receives **unvalidated** data — validate manually if using for side effects.
- Changing the return type breaks GraphQL — stick to same type or use Collection hooks.
- `beforeDuplicate` runs **before** `beforeValidate`; default appends `" - Copy"` to unique text fields.
- Use three-generic `FieldHook<Doc, Val, Sibling>` for full TypeScript safety.
- `context` (from `req.context`) lets hooks share data without extra DB fetches — see [[wiki/payloadcms/hooks-context|Hooks Context]].
## Related
- [[wiki/payloadcms/hooks-collections|Collection Hooks]] — document-level lifecycle hooks
- [[wiki/payloadcms/hooks-context|Hooks Context (req.context)]] — share state across hooks
- [[wiki/payloadcms/hooks|Hooks Overview]] — hook categories and execution model
- [[wiki/payloadcms/fields-overview|Fields Overview]] — field config, validation, custom components
## Sources
- `raw/hooks__fields.md` — https://payloadcms.com/docs/hooks/fields