---
title: "Custom Components — Authoring Guide"
aliases: [custom-components-building, payload-rsc, payload-client-components]
tags: [payloadcms, react, rsc, performance]
sources: [raw/custom-components__overview.md]
created: 2026-05-15
updated: 2026-05-15
---

# Custom Components — Authoring Guide

Complements [[wiki/payloadcms/custom-components|Custom Components (slots & config)]].
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.

```ts
// 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:

```ts
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.js` manually.** `layout.tsx` that imports it is safe to modify.

### Override location

```ts
admin: {
  importMap: {
    baseDir: path.resolve(dirname, 'src'),
    importMapFile: path.resolve(dirname, 'app', '(payload)', 'custom-import-map.js'),
  },
}
```

### Custom imports (plugin authors)

```ts
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\|Local API]] — server components only |
| `i18n` | `I18nClient` | Translation object |

Field components additionally receive `field` (server) or `clientField` (client).

### Server Components (default)

```tsx
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:

```tsx
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.

```tsx
'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):

```tsx
'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):

```tsx
'use client'
import type { TextFieldClientComponent } from 'payload'

export const MyField: TextFieldClientComponent = ({ clientField: { name } }) => (
  <p>Field: {name}</p>
)
```

## i18n in Custom Components

### Server Component

```tsx
import { getTranslation } from '@payloadcms/translations'

export default async function MyServerComponent({ i18n }) {
  return <p>{getTranslation(myTranslation, i18n)}</p>
}
```

### Client Component

```tsx
'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

```tsx
export default async function MyServerComponent({ payload, locale }) {
  const page = await payload.findByID({ collection: 'pages', id: '123', locale })
  return <p>{page.title}</p>
}
```

### Client Component

```tsx
'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:

```tsx
'use client'
import { useDocumentInfo } from '@payloadcms/ui'

export function MyComponent() {
  const { slug } = useDocumentInfo()
  return <p>Entity slug: {slug}</p>
}
```

See [[wiki/payloadcms/react-hooks|React Hooks]] for the full hook reference.

## Adding Styles

```tsx
import './index.scss'

export function MyComponent() {
  return <div className="my-component">Custom Component</div>
}
```

```scss
// 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](https://react.dev/reference/react/Suspense) for progressive loading

### Prevent unnecessary re-renders
Use `useFormFields` (targeted) instead of `useFields` (all fields):

```ts
'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/ui` barrel 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`:

```json
"@payloadcms/ui": "3.x.x",
"payload": "3.x.x",
"@payloadcms/next": "3.x.x"
```

See [[wiki/payloadcms/migration-troubleshooting|Migration & Troubleshooting]] for dedup instructions.

## Key Takeaways

- Components are referenced by **path string**, not imported — keeps config Node.js-safe
- `importMap.js` auto-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
- `payload` prop (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 `field` in server components, `clientField` in client components
- For i18n: `getTranslation()` in server, `useTranslation()` in client
- For locale: `locale` prop in server, `useLocale()` in client
- In admin panel imports use bare `@payloadcms/ui`; in frontend use deep paths for tree-shaking
- `useFormFields` over `useFields` to prevent unnecessary re-renders

## Related

- [[wiki/payloadcms/custom-components|Custom Components — Slots & Config]]
- [[wiki/payloadcms/react-hooks|React Hooks]]
- [[wiki/payloadcms/admin-panel-overview|Admin Panel Overview]]
- [[wiki/payloadcms/local-api|Local API]]
- [[wiki/payloadcms/configuration|Payload Config — Overview]]
