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

3.9 KiB

title aliases tags sources created updated
Hooks — Context (req.context)
payload-context
hooks-context
req-context
payloadcms
hooks
context
typescript
raw/hooks__context.md
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

BadafterChange 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.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

Sources