3.9 KiB
3.9 KiB
| title | aliases | tags | sources | created | updated | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Hooks — Context (req.context) |
|
|
|
2026-05-15 | 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
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:
afterChange: [
async ({ doc, req }) => {
await req.payload.update({ collection: 'customers', id: doc.id, data: { ... } })
// ☠️ infinite loop
},
],
Fixed — use a boolean flag passed through context:
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:
// any .ts or .d.ts file
declare module 'payload' {
export interface RequestContext {
customerData?: CustomerData
triggerAfterChange?: boolean
}
}
- Uses declaration merging
- Gets autocomplete on
context.*everywhere in hooks - Wrong augmentation syntax breaks all types — copy the exact pattern above
Key Takeaways
req.contextpersists 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
contextoption onpayload.create()/payload.update() - Augment
RequestContextinterface 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 — hook types and lifecycle
- wiki/payloadcms/hooks-collections — all 18 collection hook signatures
- wiki/payloadcms/local-api —
payload.update()/payload.create()with context option - wiki/payloadcms/database-transactions — another request-scoped mechanism (
req)
Sources
raw/hooks__context.md— compiled 2026-05-15- Official docs: https://payloadcms.com/docs/hooks/context