obsidian/wiki/payloadcms/hooks-context.md
2026-05-15 15:50:49 +01:00

115 lines
3.9 KiB
Markdown

---
title: "Hooks — Context (req.context)"
aliases: [payload-context, hooks-context, req-context]
tags: [payloadcms, hooks, context, typescript]
sources: [raw/hooks__context.md]
created: 2026-05-15
updated: 2026-05-15
---
## Overview
`req.context` is a plain object that persists across the **entire lifecycle of a single request** — available in every hook, middleware, and Local API call for that request. Use it to share data between hooks without extra DB/API calls.
## When To Use
| Problem | Context Solution |
|---------|-----------------|
| Need same 3rd-party data in `beforeChange` and `afterChange` | Fetch once in `beforeChange`, store on `context`, read in `afterChange` |
| `afterChange` calls `payload.update()` on the same collection → infinite loop | Set a flag (`context.triggerAfterChange = false`) and guard with early return |
| Pass extra data to Local API without adding spurious fields | Set on `req.context`, pass `context` option to `payload.create()` / `payload.update()` |
| Share values between hooks and custom middleware/endpoints | Hooks set context; `postMiddleware` reads it |
## How To Use
### Passing Data Between Hooks
```ts
const Customer: CollectionConfig = {
slug: 'customers',
hooks: {
beforeChange: [
async ({ context, data }) => {
// fetch once, store on context
context.customerData = await fetchCustomerData(data.customerID)
return { ...data, name: context.customerData.name }
},
],
afterChange: [
async ({ context }) => {
// reuse — no second fetch
if (context.customerData.contacted === false) {
createTodo('Call Customer', context.customerData)
}
},
],
},
}
```
### Preventing Infinite Loops
**Bad**`afterChange` calling `payload.update()` on the same collection triggers itself:
```ts
afterChange: [
async ({ doc, req }) => {
await req.payload.update({ collection: 'customers', id: doc.id, data: { ... } })
// ☠️ infinite loop
},
],
```
**Fixed** — use a boolean flag passed through `context`:
```ts
afterChange: [
async ({ context, doc, req }) => {
if (context.triggerAfterChange === false) return // guard
await req.payload.update({
collection: 'customers',
id: doc.id,
data: { ... },
context: { triggerAfterChange: false }, // flag for next invocation
})
},
],
```
## TypeScript — Module Augmentation
Default type is `{ [key: string]: unknown }`. For strict typing, augment `RequestContext`:
```ts
// any .ts or .d.ts file
declare module 'payload' {
export interface RequestContext {
customerData?: CustomerData
triggerAfterChange?: boolean
}
}
```
- Uses [declaration merging](https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation)
- Gets autocomplete on `context.*` everywhere in hooks
- Wrong augmentation syntax breaks all types — copy the exact pattern above
## Key Takeaways
- `req.context` persists for the full request lifecycle; set in any hook, read in any later hook
- Primary use cases: **data sharing** (avoid duplicate fetches) and **infinite loop prevention** (flag pattern)
- Pass custom context into Local API calls via the `context` option on `payload.create()` / `payload.update()`
- Augment `RequestContext` interface for type-safe context properties across the project
- The flag pattern for loops: check flag → early return → pass `context: { flag: false }` in the nested update
## Related
- [[wiki/payloadcms/hooks|Hooks Overview]] — hook types and lifecycle
- [[wiki/payloadcms/hooks-collections|Collection Hooks]] — all 18 collection hook signatures
- [[wiki/payloadcms/local-api|Local API]] — `payload.update()` / `payload.create()` with context option
- [[wiki/payloadcms/database-transactions|Database Transactions]] — another request-scoped mechanism (`req`)
## Sources
- `raw/hooks__context.md` — compiled 2026-05-15
- Official docs: https://payloadcms.com/docs/hooks/context