Back to Payload

Field Hooks

docs/hooks/fields.mdx

3.84.113.9 KB
Original Source

Field Hooks are Hooks that run on Documents on a per-field basis. They allow you to execute your own logic during specific events of the Document lifecycle. Field Hooks offer incredible potential for isolating your logic from the rest of your Collection Hooks and Global Hooks.

To add Hooks to a Field, use the hooks property in your Field Config:

ts
import type { Field } from 'payload'

export const FieldWithHooks: Field = {
  // ...
  hooks: {
    // highlight-line
    // ...
  },
}

Config Options

All Field Hooks accept an array of synchronous or asynchronous functions. These functions can optionally modify the return value of the field before the operation continues. All Field Hooks are formatted to accept the same arguments, although some arguments may be undefined based on the specific hook type.

<Banner type="warning"> **Important:** Due to GraphQL's typed nature, changing the type of data that you return from a field will produce errors in the [GraphQL API](../graphql/overview). If you need to change the shape or type of data, consider [Collection Hooks](./collections) or [Global Hooks](./globals) instead. </Banner>

To add hooks to a Field, use the hooks property in your Field Config:

ts
import type { Field } from 'payload';

const FieldWithHooks: Field = {
  name: 'name',
  type: 'text',
  // highlight-start
  hooks: {
    beforeValidate: [(args) => {...}],
    beforeChange: [(args) => {...}],
    beforeDuplicate: [(args) => {...}],
    afterChange: [(args) => {...}],
    afterRead: [(args) => {...}],
  }
  // highlight-end
}

The following arguments are provided to all Field Hooks:

OptionDescription
collectionThe Collection in which this Hook is running against. If the field belongs to a Global, this will be null.
contextCustom context passed between Hooks. More details.
dataIn the afterRead hook this is the full Document. In the create and update operations, this is the incoming data passed through the operation.
fieldThe Field which the Hook is running against.
findManyBoolean to denote if this hook is running against finding one, or finding many within the afterRead hook.
globalThe Global in which this Hook is running against. If the field belongs to a Collection, this will be null.
operationThe name of the operation that this hook is running within. Useful within beforeValidate, beforeChange, and afterChange hooks to differentiate between create and update operations.
originalDocIn the update operation, this is the Document before changes were applied. In the afterChange hook, this is the resulting Document.
overrideAccessA boolean to denote if the current operation is overriding Access Control.
pathThe path to the Field in the schema.
previousDocIn the afterChange Hook, this is the Document before changes were applied.
previousSiblingDocThe sibling data of the Document before changes being applied, only in beforeChange and afterChange hook.
previousValueThe previous value of the field, before changes, only in beforeChange and afterChange hooks.
reqThe Web Request object. This is mocked for Local API operations.
schemaPathThe path of the Field in the schema.
siblingDataThe data of sibling fields adjacent to the field that the Hook is running against.
siblingDocWithLocalesThe sibling data of the Document with all Locales.
siblingFieldsThe sibling fields of the field which the hook is running against.
valueThe value of the Field.
<Banner type="success"> **Tip:** It's a good idea to conditionally scope your logic based on which operation is executing. For example, if you are writing a `beforeChange` hook, you may want to perform different logic based on if the current `operation` is `create` or `update`. </Banner>

beforeValidate

Runs during the create and update operations. This hook allows you to add or format data before the incoming data is validated server-side.

Please do note that this does not run before client-side validation. If you render a custom field component in your front-end and provide it with a validate function, the order that validations will run in is:

  1. validate runs on the client
  2. if successful, beforeValidate runs on the server
  3. validate runs on the server
ts
import type { Field } from 'payload'

const usernameField: Field = {
  name: 'username',
  type: 'text',
  hooks: {
    beforeValidate: [
      ({ value }) => {
        // Trim whitespace and convert to lowercase
        return value.trim().toLowerCase()
      },
    ],
  },
}

In this example, the beforeValidate hook is used to process the username field. The hook takes the incoming value of the field and transforms it by trimming whitespace and converting it to lowercase. This ensures that the username is stored in a consistent format in the database.

beforeChange

Immediately before validation, beforeChange hooks will run during create and update operations. At this stage, the data should be treated as unvalidated user input. There is no guarantee that required fields exist or that fields are in the correct format. As such, using this data for side effects requires manual validation. You can optionally modify the shape of the data to be saved.

ts
import type { Field } from 'payload'

const emailField: Field = {
  name: 'email',
  type: 'email',
  hooks: {
    beforeChange: [
      ({ value, operation }) => {
        if (operation === 'create') {
          // Perform additional validation or transformation for 'create' operation
        }
        return value
      },
    ],
  },
}

In the emailField, the beforeChange hook checks the operation type. If the operation is create, it performs additional validation or transformation on the email field value. This allows for operation-specific logic to be applied to the field.

afterChange

The afterChange hook is executed after a field's value has been changed and saved in the database. This hook is useful for post-processing or triggering side effects based on the new value of the field.

ts
import type { Field } from 'payload'

const membershipStatusField: Field = {
  name: 'membershipStatus',
  type: 'select',
  options: [
    { label: 'Standard', value: 'standard' },
    { label: 'Premium', value: 'premium' },
    { label: 'VIP', value: 'vip' },
  ],
  hooks: {
    afterChange: [
      ({ value, previousValue, req }) => {
        if (value !== previousValue) {
          // Log or perform an action when the membership status changes
          console.log(
            `User ID ${req.user.id} changed their membership status from ${previousValue} to ${value}.`,
          )
          // Here, you can implement actions that could track conversions from one tier to another
        }
      },
    ],
  },
}

In this example, the afterChange hook is used with a membershipStatusField, which allows users to select their membership level (Standard, Premium, VIP). The hook monitors changes in the membership status. When a change occurs, it logs the update and can be used to trigger further actions, such as tracking conversion from one tier to another or notifying them about changes in their membership benefits.

afterRead

The afterRead hook is invoked after a field value is read from the database. This is ideal for formatting or transforming the field data for output.

ts
import type { Field } from 'payload'

const dateField: Field = {
  name: 'createdAt',
  type: 'date',
  hooks: {
    afterRead: [
      ({ value }) => {
        // Format date for display
        return new Date(value).toLocaleDateString()
      },
    ],
  },
}

Here, the afterRead hook for the dateField is used to format the date into a more readable format using toLocaleDateString(). This hook modifies the way the date is presented to the user, making it more user-friendly.

beforeDuplicate

The beforeDuplicate field hook is called on each locale (when using localization), when duplicating a document. It may be used when documents having the exact same properties may cause issue. This gives you a way to avoid duplicate names on unique, required fields or when external systems expect non-repeating values on documents.

This hook gets called before the beforeValidate and beforeChange hooks are called.

By Default, unique and required text fields Payload will append "- Copy" to the original document value. The default is not added if your field has its own, you must return non-unique values from your beforeDuplicate hook to avoid errors or enable the disableDuplicate option on the collection. Here is an example of a number field with a hook that increments the number to avoid unique constraint errors when duplicating a document:

ts
import type { Field } from 'payload'

const numberField: Field = {
  name: 'number',
  type: 'number',
  hooks: {
    // increment existing value by 1
    beforeDuplicate: [
      ({ value }) => {
        return (value ?? 0) + 1
      },
    ],
  },
}

TypeScript

Payload exports a type for field hooks which can be accessed and used as follows:

ts
import type { FieldHook } from 'payload'

// Field hook type is a generic that takes three arguments:
// 1: The document type
// 2: The value type
// 3: The sibling data type

type ExampleFieldHook = FieldHook<ExampleDocumentType, string, SiblingDataType>

const exampleFieldHook: ExampleFieldHook = (args) => {
  const {
    value, // Typed as `string` as shown above
    data, // Typed as a Partial of your ExampleDocumentType
    siblingData, // Typed as a Partial of SiblingDataType
    originalDoc, // Typed as ExampleDocumentType
    operation,
    req,
  } = args

  // Do something here...

  return value // should return a string as typed above, undefined, or null
}

Practical Example with Generated Types

Here's a real-world example using generated Payload types:

ts
import type { FieldHook } from 'payload'
import type { Post } from '@/payload-types'

// Hook for a text field in a Post collection
type PostTitleHook = FieldHook<Post, string, Post>

const slugifyTitle: PostTitleHook = ({
  value,
  data,
  siblingData,
  originalDoc,
}) => {
  // value is typed as string | undefined
  // data is typed as Partial<Post>
  // siblingData is typed as Partial<Post>
  // originalDoc is typed as Post | undefined

  // Generate slug from title if not provided
  if (!siblingData.slug && value) {
    const slug = value
      .toLowerCase()
      .replace(/[^\w\s-]/g, '')
      .replace(/\s+/g, '-')

    return slug
  }

  return value
}

// Hook for a relationship field
type PostAuthorHook = FieldHook<Post, string | number, Post>

const setDefaultAuthor: PostAuthorHook = ({ value, req }) => {
  // value is typed as string | number | undefined
  // Set current user as author if not provided
  if (!value && req.user) {
    return req.user.id
  }

  return value
}
<Banner type="success"> **Tip:** When defining field hooks, use the three generic parameters for full type safety: document type, field value type, and sibling data type. This provides autocomplete and type checking for all hook arguments. </Banner>