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