8.3 KiB
| title | aliases | tags | sources | created | updated | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Custom Components — Authoring Guide |
|
|
|
2026-05-15 | 2026-05-15 |
Custom Components — Authoring Guide
Complements wiki/payloadcms/custom-components. Covers: Component Paths, Import Map internals, building RSC/Client components, i18n, locale, hooks, styles, performance, troubleshooting.
Component Paths
Components are registered by file path string, not import — keeps the Node.js config lightweight.
// Named export via hash
Button: '/src/components/Logout#MyComponent'
// Default export
Button: '/src/components/Logout'
// Object form
Button: {
path: '/src/components/Logout',
exportName: 'MyComponent',
clientProps: { label: 'Sign out' }, // serializable → client
serverProps: { someData: true }, // non-serializable allowed → server only
}
Set admin.importMap.baseDir to shorten all paths:
admin: {
importMap: { baseDir: path.resolve(dirname, 'src') },
components: { logout: { Button: '/components/Logout#MyComponent' } },
}
Import Map
Payload auto-generates src/app/(payload)/admin/importMap.js (or app/(payload)/admin/importMap.js).
When it regenerates
- Application startup (dev or build)
- HMR — whenever you save a component file during development
- Manual:
pnpm payload generate:importmap
When it does NOT regenerate
- Normal runtime (only at startup)
- After a production build completes
Never edit
importMap.jsmanually.layout.tsxthat imports it is safe to modify.
Override location
admin: {
importMap: {
baseDir: path.resolve(dirname, 'src'),
importMapFile: path.resolve(dirname, 'app', '(payload)', 'custom-import-map.js'),
},
}
Custom imports (plugin authors)
admin: {
dependencies: {
myTestComponent: {
path: '/components/TestComponent.js#TestComponent',
type: 'component',
clientProps: { test: 'hello' },
},
},
}
Building Custom Components
Default props (auto-injected)
| Prop | Type | Notes |
|---|---|---|
payload |
Payload |
Full wiki/payloadcms/local-api — server components only |
i18n |
I18nClient |
Translation object |
Field components additionally receive field (server) or clientField (client).
Server Components (default)
import React from 'react'
import type { Payload } from 'payload'
async function MyServerComponent({ payload }: { payload: Payload }) {
const page = await payload.findByID({ collection: 'pages', id: '123' })
return <p>{page.title}</p>
}
Access Payload Config directly from the payload prop:
export default async function MyServerComponent({ payload: { config } }) {
return <Link href={config.serverURL}>Go Home</Link>
}
Client Components
Add 'use client' — Payload strips non-serializable props automatically.
'use client'
import React, { useState } from 'react'
export function MyClientComponent() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount(count + 1)}>Clicked {count} times</button>
}
Payload Config on the client — use useConfig() hook (Client Config is a serializable subset):
'use client'
import { useConfig } from '@payloadcms/ui'
export function MyClientComponent() {
const { config: { serverURL } } = useConfig()
return <Link href={serverURL}>Go Home</Link>
}
Field config on the client — prop is named clientField (non-serializable fields removed):
'use client'
import type { TextFieldClientComponent } from 'payload'
export const MyField: TextFieldClientComponent = ({ clientField: { name } }) => (
<p>Field: {name}</p>
)
i18n in Custom Components
Server Component
import { getTranslation } from '@payloadcms/translations'
export default async function MyServerComponent({ i18n }) {
return <p>{getTranslation(myTranslation, i18n)}</p>
}
Client Component
'use client'
import { useTranslation } from '@payloadcms/ui'
export function MyClientComponent() {
const { t, i18n } = useTranslation()
return <span>{t('namespace:key', { variable: 'value' })}</span>
}
Locale in Custom Components
Server Component
export default async function MyServerComponent({ payload, locale }) {
const page = await payload.findByID({ collection: 'pages', id: '123', locale })
return <p>{page.title}</p>
}
Client Component
'use client'
import { useLocale } from '@payloadcms/ui'
function Greeting() {
const locale = useLocale()
const trans = { en: 'Hello', es: 'Hola' }
return <span>{trans[locale.code]}</span>
}
Using Payload Hooks (Client)
Any @payloadcms/ui hook is available in client components. Examples:
'use client'
import { useDocumentInfo } from '@payloadcms/ui'
export function MyComponent() {
const { slug } = useDocumentInfo()
return <p>Entity slug: {slug}</p>
}
See wiki/payloadcms/react-hooks for the full hook reference.
Adding Styles
import './index.scss'
export function MyComponent() {
return <div className="my-component">Custom Component</div>
}
// Use Payload CSS variables for theme compatibility
.my-component {
background-color: var(--theme-elevation-500);
}
// Import Payload's SCSS library for mixins
@import '~@payloadcms/ui/scss';
.my-component {
@include mid-break {
background-color: var(--theme-elevation-900);
}
}
Performance
Reduce server→client HTML size
- Prefer server components; only send necessary props to the client
- All props crossing the server/client boundary are serialized — be explicit
- Use React Suspense for progressive loading
Prevent unnecessary re-renders
Use useFormFields (targeted) instead of useFields (all fields):
'use client'
import { useFormFields } from '@payloadcms/ui'
const MyComponent: TextFieldClientComponent = ({ path }) => {
const value = useFormFields(([fields]) => fields[path])
// ...
}
Import best practices
| Context | Import style | Why |
|---|---|---|
| Admin Panel UI | import { X } from '@payloadcms/ui' |
Avoids package mismatch |
| Frontend app | import { X } from '@payloadcms/ui/elements/X' |
Tree-shaking, avoids bundle bloat |
Importing the entire
@payloadcms/uibarrel from frontend code bundles the whole admin library.
Follow React and Next.js best practices: memoization, streaming, caching, server/client boundary placement, layouts vs pages.
Troubleshooting
useConfig is undefined / value of useConfig cannot be destructured
Caused by dependency mismatches — multiple versions of @payloadcms/ui installed.
Fix: pin all Payload packages to the exact same version in package.json:
"@payloadcms/ui": "3.x.x",
"payload": "3.x.x",
"@payloadcms/next": "3.x.x"
See wiki/payloadcms/migration-troubleshooting for dedup instructions.
Key Takeaways
- Components are referenced by path string, not imported — keeps config Node.js-safe
importMap.jsauto-regenerates at startup and on HMR — never edit manually- All custom components are RSC by default; add
'use client'to opt into client mode - Payload auto-strips non-serializable props before passing them to client components
payloadprop (local API) is available on server components for free DB access- Client Config accessed via
useConfig()— it's a serializable subset of full config - Field config is named
fieldin server components,clientFieldin client components - For i18n:
getTranslation()in server,useTranslation()in client - For locale:
localeprop in server,useLocale()in client - In admin panel imports use bare
@payloadcms/ui; in frontend use deep paths for tree-shaking useFormFieldsoveruseFieldsto prevent unnecessary re-renders