Site icon Ampersand Tutorials

Payload CMS Globals, Hooks and Reused Collections Explained (2026)

Payload CMS globals hooks and reused collections illustration

Quick answer: Payload CMS globals, hooks, and reused collections are the three moves that turn a first prototype into a maintainable content model. Globals hold singleton content that exists exactly once — navigation, footer text, site settings — while hooks (beforeChange, afterChange, afterRead, beforeValidate) let you stamp timestamps, derive slugs, keep denormalized fields consistent, and trigger side effects without duplicating logic in your frontend. Reused collections with a relationship field are cheaper than new collections whenever several content types need to share the same records.

Part 6 of our Payload CMS series. Start with what Payload CMS is and install Payload and build your first collection, then read authentication and role-based access control before wiring hooks that touch protected fields.

1. Payload CMS globals: what qualifies as a global

A global is a document that can exist only once. If you find yourself making a collection with exactly one record, or hard-coding a phone number in three components, that content wants to be a global.

ContentWhy it is a global
Site navigationOne menu tree rendered on every page
Footer contact blockOne address, one phone, one email
Global settingsDark mode default, announcement banner toggle, social links
Homepage layoutOne page that owns the blocks of a single homepage

Globals get the same field system as collections, plus admin-panel editing at their own route and their own access control. One thing does not qualify: anything that could later become plural. A single “featured article” field on a homepage global is fine today; a growing list of featured articles belongs in a collection with an array field.

// globals/SiteSettings.ts
import type { GlobalConfig } from 'payload'

export const SiteSettings: GlobalConfig = {
  slug: 'site-settings',
  admin: { group: 'Site' },
  access: {
    read: () => true,
    update: ({ req: { user } }) => Boolean(user),
  },
  fields: [
    { name: 'announcement', type: 'text' },
    { name: 'showAnnouncement', type: 'checkbox', defaultValue: false },
    { name: 'socialLinks', type: 'array', fields: [
      { name: 'platform', type: 'text', required: true },
      { name: 'url', type: 'text', required: true },
    ]},
  ],
}

Globals register under globals, not collections, and they are read and written through variant methods everywhere — payload.findGlobal({ slug: 'site-settings' }) on the Local API, /api/globals/site-settings over REST. Forgetting that distinction is the most common first-day error: the document does exist, but your code is calling find instead of findGlobal, and the return value is an object rather than a list.

2. Reading globals in React Server Components

Globals belong at the seams of a layout, so they are read where pages are laid out rather than inside individual blocks. A footer component reads the footer global once per request, and a layout that wraps every page composes them:

// components/Footer.tsx — server component
import { getPayload } from 'payload'
import config from '@payload-config'

export async function Footer() {
  const payload = await getPayload({ config })
  const footer = await payload.findGlobal({ slug: 'site-footer', depth: 1 })
  return (
    <footer>
      {footer.contactLine && <p>{footer.contactLine}</p>}
      <nav>{/* map footer.links as a subfield of its own */}</nav>
    </footer>
  )
}

Because this read happens in a server component there is no client fetch, no loading state, and no REST round-trip — the same Local API pattern the earlier articles showed for posts. Keep depth honest: depth: 1 is enough when the global references uploads or related documents you want populated, and more depth only costs you.

3. The hook map: which hook does which job

Hooks run your code at fixed points in a document’s lifecycle. Pick the hook by what you need to guarantee, not by trial and error:

HookRuns whenTypical job
beforeValidateBefore field validationNormalize input, reject malformed submissions early
beforeChangeBefore the DB writeStamp updatedAt, derive slugs, compute totals, enforce cross-field rules
afterChangeAfter the DB writeSend notification, revalidate pages, sync to a search index
beforeRead / afterReadOn every readMask fields, inject computed display values
beforeDelete / afterDeleteAround a deleteClean up files, audit-log the deletion

Hooks receive an operation argument that tells you which operation is running, and a hook that throws blocks the operation. That is your enforcement mechanism.

// collections/Posts.ts — excerpt
hooks: {
  beforeChange: [
    async ({ data, originalDoc, operation }) => {
      if (operation === 'create' || operation === 'update') {
        if (!data.slug && data.title) {
          data.slug = data.title.toLowerCase()
            .replace(/[^a-z0-9]+/g, '-')
            .replace(/^-+|-+$/g, '')
        }
      }
      return data
    },
  ],
  afterChange: [
    async ({ doc, req, operation }) => {
      if (operation === 'update' && doc.status === 'published') {
        req.payload.logger.info(`post ${doc.id} went live`)
      }
    },
  ],
}

Keep hooks small and single-purpose. A hook that both validates and notifies is two hooks sharing a bug.

4. Hooks run on the server — keep them fast

Your hook code runs in the same process as the CMS while a request is waiting on it. A slow afterChange hook becomes slow saves in the admin panel. Two rules keep hooks cheap:

If a hook can fail in a way that must not block the save, it does not belong in a blocking hook at all. Catch its errors, log them, and accept the side effect may retry later — Payload will not roll back the database write because your email provider had a bad second.

5. Reuse via relationships, not copies

When several content types need to share records, the cheap answer is one collection plus relationship fields. The expensive answer is three near-identical collections that drift apart. Authors, categories, and media are the three most common cases:

// collections/Authors.ts — one collection, referenced from anywhere
export const Authors: CollectionConfig = {
  slug: 'authors',
  fields: [
    { name: 'name', type: 'text', required: true },
    { name: 'bio', type: 'textarea' },
    { name: 'avatar', type: 'upload', relationTo: 'media' },
  ],
}

// collections/Posts.ts — field excerpt
{ name: 'author', type: 'relationship', relationTo: 'authors', required: true },

Any collection can now point at authors, and a display change (new field, updated bio template, computed byline) happens once. If you reach a point where two collections genuinely need different fields, split them — but split on schema difference, not on convenience.

6. The migration plan that will not surprise you

Globals, hooks and relationships all touch your schema, so the same migration discipline from the deployment article applies:

The surprise teams report most often: a hook that stamps updatedAt was written against the dev database that already had the field, and the migration to add the field never ran in production because someone assumed hooks handle schema too. Hooks do not manage schema; they assume it continues to exist.

7. Testing hooks and globals without a live server

Because these are plain TypeScript objects and functions, they are testable without booting the admin panel. Two testable shapes stand out:

Keep the admin panel out of the test until you are testing the admin panel. The Local API is the test surface for data behavior.

8. The errors you will actually hit

SymptomCause and fix
Global reads as null / undefinedYou queried the collection route, not the global — use findGlobal or /api/globals/<slug>
Hook never firesIt is registered on the wrong side (field hooks vs collection hooks run at different points) — check the config nesting
Slug is empty after publishThe beforeChange hook reads data.title before variants of the operation have saved it — read from the operation argument the hook gives you
Side effect fires twiceThe hook is registered in a shared config that gets imported twice — move it into a single list in one place
Relationship shows as an IDdepth is too low — raise depth on the query or render the ID as a fallback
Permission error on hook writeThe hook writes with overrideAccess: false implied — pass overrideAccess: true for system-written fields, or make the field update: false
Global edit does not show on siteCaching between config change and render — treat globals like any content that needs revalidation hooks when you put a cache in front

9. Key takeaways and challenge

Challenge: take the Posts collection from the earlier articles and add a readingTime field computed by a beforeChange hook from the rich-text content — pure function first, hook second. Then move the announcement banner into a site-settings global that the footer of your layout reads with findGlobal, and give the global an access rule that only authenticated users can update it. Write down one place where a hook and an access rule could conflict, and say which one wins.

*Designing a content model that has outgrown its first draft? Ampersand Academy runs one-to-one mentoring on TypeScript and Next.js backends. Built and run by Mahadhi — development and digital marketing.

When should something be a global instead of a collection?

When there can only ever be one document of that content such as site settings, the footer, or the homepage layout. If it could become plural later, start it as a collection or an array field inside the global so you do not have to migrate the shape later.

How do I read a global in code?

With the Local API method findGlobal with the slug argument on the server, or the REST route at globals slash slug when a client needs it. Collection-style find will not see it, and that mismatch is the most common first error.

Which hook should compute a derived field like a slug?

beforeChange. It runs before the database write, it can modify the data being saved, and both create and update operations pass through it.

Do hooks run before or after field validation?

beforeValidate runs first, then validation, then beforeChange. Normalize malformed input in beforeValidate. Derive values from already-valid fields in beforeChange.

Can a hook fail the save entirely?

Yes. Throwing inside a blocking hook stops the operation, which is useful for enforcing cross-field rules. Keep that for real rule enforcement, and put side effects that must not block the save in afterChange with their own error handling.

Should I duplicate a collection to reuse its fields?

No. Use a relationship field pointing at one shared collection. Duplicate collections drift apart, while a relationship keeps one source of truth with its own access rules and types.

Do hooks handle database schema changes?

No. Hooks assume the fields they touch exist. Collection field changes still need migrations in production: generate them with payload migrate create and run them during deploy.

Exit mobile version