An Overview of PayloadCMS Field and Collection Hooks
Learn how to use Payload field and collection hooks.
What’s up everyone? In this guide, I'm going to go over how to use the field and collection hooks in Payload CMS. Let’s dive in.
I’ll be continuing our discussion of hooks in this guide. We’ve already talked about how to use hooks in your Payload config and in your Globals, but now we’ll dive into the field and collection hooks.
While collections sit at a higher level in the hierarchy of concepts in Payload CMS, the number of hooks Collections contain is quite large. So, we’ll start with field hooks to get those knocked out fairly quickly. Some of these hooks are repeated with information from the Global hooks guide, so be sure to watch that guide before moving on with this one.
Let’s get started with field hooks.
Field Hooks
Field hooks are hooks that run on documents at the field level. You can execute your own logic during each event in the document lifecycle, but instead of influencing an entire global or collection, you’ll only impact the field on which you included the hook. You can add hooks to a field by typing in the hooks property in your field config. It’s important to add that if you’re trying to use hooks to change the shape or type of data at the Field level, you are likely going to get errors from GraphQL. Those kinds of changes are more appropriate for Global or Collection Hooks.
You can organize your hooks for fields in a couple of different ways. If you want to keep everything separate, you could create config files for each field and export and import them into your Collection. This would allow you to reuse the fields and their hooks in other collections, but it can be harder to maintain and understand what’s happening across your project.
What we’ll do is include a new fieldHooks directory in our collection directory. For the sake of this example and the Collection hooks section to come, we’ll create a new Posts directory, change the name of the file to be index.ts, and add our fieldHooks directory here.
Next, we’ll create an index.ts for our fieldHooks.
Unlike global hooks, all field hooks are provided with the same arguments. Let’s explore each of those arguments.
Collection returns the collection where this hook is running. If we were to use this field in our Posts collection, it would return the slug for our posts collection. But if the field is included in our Header Global document, the collection will return null.
If the field is in a global, the global argument will return the global in which this hook is running. If the field is in a collection, this one will return null.
Data returns the full document in the afterRead hook, but it will return the incoming data passed through the operation for any hooks that run at the create or update operations. So, if we use the beforeValidate hook, we will see the data from the current submission be returned.
Operation is used to check which operation is currently running. This is helpful because both create and update operations happen within beforeValidate, beforeChange, and afterChange. That means you can use conditional logic to change and route your data argument depending on which operation the hook is currently executing.
originalDoc returns your document before changes were applied when used within the update operation. When used in afterChange, this will return the resulting document.
overrideAccess will return a boolean denoting if the current operation is overriding your access control.
Field simply returns the field the hook is running on.
findMany returns a boolean to check if you’re running against finding one or finding many like you could in the afterRead hook.
Path simply returns the path to your field.
previousDoc works in the afterChange hook. This returns the document before changes are applied. This allows you to check differences between your new doc and the previous version.
You can use previousValue to see what the previous value of the field was in the beforeChange and afterChange hooks. Using value, you can see the current value of the field. This allows you to do conditional logic to create branches of behavior based on the results of this check.
Req can be used to access the Local API in a hook. This is the web request object and allows you to perform more advanced queries within the field hook.
Context is also available to you where you can pass custom context between hooks, but we won’t cover that here. previousSiblingDoc, schemaPath, and siblingDocWithLocales are also in there, but we won’t cover those.
Now that we have all the arguments, we can go over each of the hooks available at the field level. Field hooks do not need hook types to be imported, as they are included as part of the field type.
beforeValidate runs before the update operation, so it allows you to format field data before it’s validated. This is helpful if you have data in a sibling field that needs to be transformed before it can be accepted by the field with validation. For example, if you have a slug field that is required, but you need to include a title first, you can use beforeValidate to generate a slug from the title field upon saving. This operation won’t fail because the slug field won’t be validated until after it’s properly populated.
beforeChange takes place after validation and runs within the create and update operations. The data being saved is now validated and the document lifecycle will continue. You can use beforeChange to perform additional validation or transformation at the field level.
afterChange executes after the value of the field has been changed and saved in the database. You can use this hook to check if the value of the field matches the previous value, and if it doesn’t execute some kind of custom logic like logging the change or using the req argument to query another collection.
afterRead runs after the field is read from the database. This allows you to format the field for output, like making a date more readable.
beforeDuplicate is a hook we haven't gone over yet. This hook is run when you duplicate your document. This allows you to adjust fields with unique or that are required to prevent errors when duplicating the document. It’s called before your beforeValidate and beforeChange hooks. By default, Payload will add an indication that the required field is a copy, but you can change this behavior by using the beforeDuplicate hook.
Collection Hooks
Now for collection hooks. Collection hooks run on documents within the specified collection the hook is created in. All hooks allow you to execute your own logic during operations and events in the PayloadCMS dashboard, but collections allow you to do this specifically at the collection-level.
There are many collection hooks, and for this video, we’ll cover any hook that isn’t used in the auth-enabled collections.
You create a hook in a collection by adding the hooks option and assigning an empty object to it. Just like our global hooks, we’ll want to create a hooks directory in the same folder as our collection. In this case, we’ll create the hooks directory in our Posts folder, next to the fieldHooks folder.
At this point, we’ve discussed at length what each provided argument means. If you need a refresher about what each argument means, you can scroll back up. I will mention the provided arguments for each hook and explain only new arguments from here on out.
Each collection hook will need its type imported from payload, so starting with beforeOperation, you will import type {CollectionBeforeOperationHook} from 'payload'. Then, export a new constant called beforeOperationHook, assign CollectionBeforeOperationHook to an anonymous function to return your arguments. You’re able to access collection, context, operation, and req with this hook. Operations include create, read, update, delete, login, refresh, and forgotPassword, which you can then use to create branching logic to run different side-effects during that operation. This allows you to have extreme fine-tuned control over what happened before each operation.
For beforeValidate, you’ll import type {CollectionBeforeValidateHook} from 'payload'. Then, export a new constant called beforeValidateHook, assign CollectionBeforeValidateHook to an async anonymous function to return your arguments. This hook runs before the create and update operations, so you can add or format data before validating it on the server-side. This doesn’t work with client-side validation if you used the validate function, so keep that in mind before using this hook. collection, context, data, operation, originalDoc, and req are all available for this hook.
For beforeChange, you’ll import type {CollectionBeforeChangeHook} from 'payload.'. Then, export a new constant called beforeChangeHook, assign CollectionBeforeChangeHook to an async anonymous function to return your arguments. This hook runs within the create and update operations, so you can modify the shape of the data to be saved after it’s been validated. collection, context, data, operation, originalDoc, and req are all available for this hook.
afterChange requires you to import type {CollectionAfterChangeHook} from 'payload'. Then, export a new constant called afterChangeHook, assign CollectionAfterChangeHook to an async anonymous function to return your arguments. This hook runs after the create or update operations and data is saved. You’re then able to use the resulting data. For example, you can use this hook to update a user profile both in Payload CMS and in your CRM. collection, context, doc, operation, previousDoc, and req are all available for this hook.
beforeRead requires you to import type {CollectionBeforeReadHook} from 'payload'. Then, export a new constant called beforeReadHook, assign CollectionBeforeReadHook to an async anonymous function to return your arguments. This hook runs before the find and findByID operations are transformed for output by afterRead. This allows you to access hidden fields before they’re removed. collection, context, doc, query, and req are all available for this hook.
For afterRead, you’ll import type {CollectionAfterReadHook} from 'payload'. Then, export a new constant called afterReadHook, assign CollectionAfterReadHook to an async anonymous function to return your arguments. This hook is the final step before a document is returned. It hides protected fields and removes fields that users don’t have access to. collection, context, doc, query, and req are all available for this hook.
beforeDelete runs before the delete operation is run. You can use this hook to send data somewhere before deleting a document. For example, you could set up a hook to send an email each time a document is deleted from a sensitive collection. You will need to import type {CollectionBeforeDeleteHook} from 'payload', export a new constant, which we’ll call beforeDeleteHook, and assign it to type CollectionBeforeDeleteHook. Open a new async anonymous function to then use whatever logic you’d like to here. Any returned values are discarded, since the document is being deleted. You have the collection, context, id, and req arguments available to you.
afterDelete runs after the delete operation removes the document from your database. Since this is a delete function, all returned values will be discarded. You can use this hook to run any kind of business logic after a document has been deleted. You’ll need to import the type {CollectionAfterDeleteHook} from 'payload', export a new constant called afterDeleteHook and assign it to the CollectionAfterDeleteHook type, open up a new async anonymous function, and then perform any actions you’d like to perform in the function. You’re able to access the collection, context, doc, id, and req arguments in the afterDelete hook.
afterOperation gives you the flexibility to modify the result of any operation or run side-effects based on your own custom logic. Similar to beforeOperation, you’re able to specify the operation you want to modify. The options are create, find, findByID, update, updateByID, delete, deleteByID, login, refresh, and forgotPassword. Operation is an argument in the afterOperation hook, so you can pass that into your async anonymous function and then use if-else statements to drive custom behavior for this hook. You’ll need to import type {CollectionAfterOperationHook} from 'payload', export a constant, which we’ll call afterOperationHook and assign CollectionAfterOperationHook to your async anonymous function with any of the following arguments: args, collection, req, operation, and result.
The last hook we’ll cover in this video is afterError. This is triggered by an error that takes place in your Payload app. This is another great hook that allows you to send an email when there’s an error in the collection or if you have a third-party logging service. You can access this hook by importing type {CollectionAfterErrorHook} from 'payload', exporting a new constant that’s assigned to the CollectionAfterErrorHook, and setting that equal to a new async anonymous function. You’re able to access the following arguments for this hook: error, context, graphqlResult, req, collection, and result. So you could send an email to your development team what error took place in which collection. This makes troubleshooting errors much easier, since you and your dev team will know where to look without much digging.
Final Thoughts
Now that we’ve initialized a bunch of hooks that don’t do anything yet, we can import them into our collection using the hooks option. As you get started with projects, you can reference this guide and my last one on global hooks to create powerful and custom side-effects in your documents.
This was obviously just an overview of what each hook does, and the true power in these hooks is in actually applying them to your project. Some key use cases for hooks are on-demand revalidation with NextJS, programmatically setting fields from sibling fields, sending emails after operations, and even integrating with your third-party CRM. The options really are endless.