payload-admin-components について
Payload CMSの管理パネルにカスタムReactコンポーネントを追加するためのリファレンススキルです。全体、コレクション、グローバル文書、フィールドのUIをカスタマイズする開発者向けです。
主な機能
- 01管理パネルのルートコンポーネントをカスタマイズ
- 02コレクションとグローバルのコンポーネントを追加
- 03フィールドとセルのUIを作成
- 04コンポーネントのパスとPropsを設定
- 05サーバー/クライアントコンポーネントを利用
- 06Payload APIと翻訳にアクセス
活用シーン
- 01管理パネルのロゴやナビゲーションを置き換え
- 02ダッシュボードやヘッダーにアクションを追加
- 03フィールド編集と一覧表示をカスタマイズ
- 04サーバーコンポーネントでPayloadデータを取得
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ timschoch/ skilly --skill payload-admin-componentsインストール後、Claude Code で/payload-admin-componentsを入力して読み込まれたか確認します。
詳細
Payload CMS のカスタムコンポーネント
カスタムコンポーネントを使用すると、独自の React コンポーネントに差し替えて、管理パネルを完全にカスタマイズできます。インターフェースのほぼすべての部分を置き換えたり、新しい機能を追加したりできます。
コンポーネントの種類
カスタムコンポーネントには、主に次の 4 種類があります。
- ルートコンポーネント - 管理パネル全体に影響します(ロゴ、ナビゲーション、ヘッダー)
- コレクションコンポーネント - コレクションビュー専用です
- グローバルコンポーネント - グローバルドキュメントビュー専用です
- フィールドコンポーネント - カスタムフィールドの UI とセルです
カスタムコンポーネントの定義
コンポーネントのパス
設定を軽量に保ち、Node.js との互換性を確保するため、コンポーネントは直接インポートするのではなく、ファイルパスを使用して定義します。
import { buildConfig } from 'payload'
export default buildConfig({
admin: {
components: {
logout: {
Button: '/src/components/Logout#MyComponent', // Named export
},
Nav: '/src/components/Nav', // Default export
},
},
})
コンポーネントのパスのルール:
- パスはプロジェクトのルート(または
config.admin.importMap.baseDir)からの相対パスです - 名前付きエクスポートの場合:
#ExportNameを追加するか、exportNameプロパティを使用します - デフォルトエクスポートの場合:サフィックスは不要です
- ファイル拡張子は省略できます
コンポーネント設定オブジェクト
文字列のパスの代わりに、設定オブジェクトを渡すこともできます。
{
logout: {
Button: {
path: '/src/components/Logout',
exportName: 'MyComponent',
clientProps: { customProp: 'value' },
serverProps: { asyncData: someData },
},
},
}
設定プロパティ:
| プロパティ | 説明 |
|---|---|
path | コンポーネントのファイルパス(名前付きエクスポートは # を使用) |
exportName | 名前付きエクスポート(パス内の # の代わり) |
clientProps | クライアントコンポーネントに渡す Props(シリアル化可能である必要があります) |
serverProps | サーバーコンポーネントに渡す Props(シリアル化できない値も指定できます) |
ベースディレクトリの設定
import path from 'path'
import { fileURLToPath } from 'node:url'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
export default buildConfig({
admin: {
importMap: {
baseDir: path.resolve(dirname, 'src'), // Set base directory
},
components: {
Nav: '/components/Nav', // Now relative to src/
},
},
})
サーバーコンポーネントとクライアントコンポーネント
すべてのコンポーネントは、デフォルトで React サーバーコンポーネントです。
サーバーコンポーネント(デフォルト)
Local API を直接使用し、非同期処理を実行して、Payload インスタンス全体にアクセスできます。
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>
}
export default MyServerComponent
クライアントコンポーネント
インタラクティブ機能、フック、状態などを使用するには、'use client' ディレクティブを使用します。
'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>
}
重要: クライアントコンポーネントは、シリアル化できない Props(関数、クラスインスタンスなど)を受け取れません。Payload はクライアントコンポーネントに渡す際に、これらを自動的に除外します。
デフォルトの Props
すべてのカスタムコンポーネントは、デフォルトで次の Props を受け取ります。
| Prop | 説明 | 型 |
|---|---|---|
payload | Payload インスタンス(Local API へのアクセス) | Payload |
i18n | 国際化オブジェクト | I18n |
locale | 現在のロケール(ローカライズが有効な場合) | string |
サーバーコンポーネントの例:
async function MyComponent({ payload, i18n, locale }) {
const data = await payload.find({
collection: 'posts',
locale,
})
return <div>{data.docs.length} posts</div>
}
クライアントコンポーネントの例:
'use client'
import { usePayload, useLocale, useTranslation } from '@payloadcms/ui'
export function MyComponent() {
// Access via hooks in client components
const { getLocal, getByID } = usePayload()
const locale = useLocale()
const { t, i18n } = useTranslation()
return <div>{t('myKey')}</div>
}
カスタム Props
clientProps または serverProps を使用して、追加の Props を渡します。
{
logout: {
Button: {
path: '/components/Logout',
clientProps: {
buttonText: 'Sign Out',
onLogout: () => console.log('Logged out'),
},
},
},
}
コンポーネントで受け取ります。
'use client'
export function Logout({ buttonText, onLogout }) {
return <button onClick={onLogout}>{buttonText}</button>
}
ルートコンポーネント
ルートコンポーネントは、管理パネル全体に影響します。
利用可能なルートコンポーネント
| コンポーネント | 説明 | 設定パス |
|---|---|---|
Nav | ナビゲーションサイドバー全体 | admin.components.Nav |
graphics.Icon | 小さなアイコン(ナビゲーションで使用) | admin.components.graphics.Icon |
graphics.Logo | ロゴ全体(ログイン画面で使用) | admin.components.graphics.Logo |
logout.Button | ログアウトボタン | admin.components.logout.Button |
actions | ヘッダーアクション(配列) | admin.components.actions |
header | ヘッダー上部(配列) | admin.components.header |
beforeDashboard | ダッシュボードコンテンツの前(配列) | admin.components.beforeDashboard |
afterDashboard | ダッシュボードコンテンツの後(配列) | admin.components.afterDashboard |
beforeLogin | ログインフォームの前(配列) | admin.components.beforeLogin |
afterLogin | ログインフォームの後(配列) | admin.components.afterLogin |
beforeNavLinks | ナビゲーションリンクの前(配列) | admin.components.beforeNavLinks |
afterNavLinks | ナビゲーションリンクの後(配列) | admin.components.afterNavLinks |
settingsMenu | 設定メニュー項目(配列) | admin.components.settingsMenu |
providers | カスタム React Context プロバイダー | admin.components.providers |
views | カスタムビュー(ダッシュボードなど) | admin.components.views |
例:カスタムロゴ
export default buildConfig({
admin: {
components: {
graphics: {
Logo: '/components/Logo',
Icon: '/components/Icon',
},
},
},
})
// components/Logo.tsx
export default function Logo() {
return <img src="/logo.png" alt="My Brand" width={200} />
}
例:ヘッダーアクション
export default buildConfig({
admin: {
components: {
actions: ['/components/ClearCacheButton', '/components/PreviewButton'],
},
},
})
// components/ClearCacheButton.tsx
'use client'
export default function ClearCacheButton() {
return (
<button
onClick={async () => {
await fetch('/api/clear-cache', { method: 'POST' })
alert('Cache cleared!')
}}
>
Clear Cache
</button>
)
}
コレクションコンポーネント
コレクションコンポーネントは、コレクションのビューに固有のものです。
import type { CollectionConfig } from 'payload'
export const Posts: CollectionConfig = {
slug: 'posts',
admin: {
components: {
// Edit view components
edit: {
PreviewButton: '/components/PostPreview',
SaveButton: '/components/CustomSave',
SaveDraftButton: '/components/CustomSaveDraft',
PublishButton: '/components/CustomPublish',
},
// List view components
list: {
Header: '/components/PostsListHeader',
beforeList: ['/components/ListFilters'],
afterList: ['/components/ListFooter'],
},
},
},
fields: [
// ...
],
}
グローバルコンポーネント
コレクションコンポーネントと似ていますが、グローバルドキュメント向けです。
import type { GlobalConfig } from 'payload'
export const Settings: GlobalConfig = {
slug: 'settings',
admin: {
components: {
edit: {
PreviewButton: '/components/SettingsPreview',
SaveButton: '/components/SettingsSave',
},
},
},
fields: [
// ...
],
}
フィールドコンポーネント
編集ビューとリストビューでのフィールドのレンダリング方法をカスタマイズします。
フィールドコンポーネント(編集ビュー)
{
name: 'status',
type: 'select',
options: ['draft', 'published'],
admin: {
components: {
Field: '/components/StatusField',
},
},
}
// components/StatusField.tsx
'use client'
import { useField } from '@payloadcms/ui'
import type { SelectFieldClientComponent } from 'payload'
export const StatusField: SelectFieldClientComponent = ({ path, field }) => {
const { value, setValue } = useField({ path })
return (
<div>
<label>{field.label}</label>
<select value={value} onChange={(e) => setValue(e.target.value)}>
{field.options.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
)
}
セルコンポーネント(リストビュー)
{
name: 'status',
type: 'select',
options: ['draft', 'published'],
admin: {
components: {
Cell: '/components/StatusCell',
},
},
}
// components/StatusCell.tsx
import type { SelectFieldCellComponent } from 'payload'
export const StatusCell: SelectFieldCellComponent = ({ data, cellData }) => {
const isPublished = cellData === 'published'
return (
<span
style={{
color: isPublished ? 'green' : 'orange',
fontWeight: 'bold',
}}
>
{cellData}
</span>
)
}
UI フィールド(表示専用)
データに影響を与えずにカスタム UI を追加するための特殊なフィールドタイプです。
{
name: 'refundButton',
type: 'ui',
admin: {
components: {
Field: '/components/RefundButton',
},
},
}
// components/RefundButton.tsx
'use client'
import { useDocumentInfo } from '@payloadcms/ui'
export default function RefundButton() {
const { id } = useDocumentInfo()
return (
<button
onClick={async () => {
await fetch(`/api/orders/${id}/refund`, { method: 'POST' })
alert('Refund processed')
}}
>
Process Refund
</button>
)
}
フックの使用
Payload は、クライアントコンポーネント向けに多くの React フックを提供します。
'use client'
import {
useAuth, // Current user
useConfig, // Payload config (client-safe)
useDocumentInfo, // Current document info (id, slug, etc.)
useField, // Field value and setValue
useForm, // Form state and dispatch
useFormFields, // Multiple field values (optimized)
useLocale, // Current locale
useTranslation, // i18n translations
usePayload, // Local API methods
} from '@payloadcms/ui'
export function MyComponent() {
const { user } = useAuth()
const { config } = useConfig()
const { id, collection } = useDocumentInfo()
const locale = useLocale()
const { t } = useTranslation()
return <div>Hello {user?.email}</div>
}
重要: これらのフックは、Admin Panel のコンテキスト内にあるクライアントコンポーネントでのみ動作します。
Payload 設定へのアクセス
サーバーコンポーネントの場合:
async function MyServerComponent({ payload }) {
const { config } = payload
return <div>{config.serverURL}</div>
}
クライアントコンポーネントの場合:
'use client'
import { useConfig } from '@payloadcms/ui'
export function MyClientComponent() {
const { config } = useConfig() // Client-safe config
return <div>{config.serverURL}</div>
}
重要: クライアントコンポーネントには、シリアライズ可能な形式の設定が渡されます(関数やバリデーションなどは取り除かれます)。
フィールド設定へのアクセス
サーバーコンポーネント:
import type { TextFieldServerComponent } from 'payload'
export const MyFieldComponent: TextFieldServerComponent = ({ field }) => {
return <div>Field name: {field.name}</div>
}
クライアントコンポーネント:
'use client'
import type { TextFieldClientComponent } from 'payload'
export const MyFieldComponent: TextFieldClientComponent = ({ clientField }) => {
// clientField has non-serializable props removed
return <div>Field name: {clientField.name}</div>
}
翻訳(i18n)
サーバーコンポーネント:
import { getTranslation } from '@payloadcms/translations'
async function MyServerComponent({ i18n }) {
const translatedTitle = getTranslation(myTranslation, i18n)
return <p>{translatedTitle}</p>
}
クライアントコンポーネント:
'use client'
import { useTranslation } from '@payloadcms/ui'
export function MyClientComponent() {
const { t, i18n } = useTranslation()
return (
<div>
<p>{t('namespace:key', { variable: 'value' })}</p>
<p>Language: {i18n.language}</p>
</div>
)
}
コンポーネントのスタイル設定
CSS 変数の使用
import './styles.scss'
export function MyComponent() {
return <div className="my-component">Custom Component</div>
}
// styles.scss
.my-component {
background-color: var(--theme-elevation-500);
color: var(--theme-text);
padding: var(--base);
border-radius: var(--border-radius-m);
}
Payload SCSS のインポート
@import '~@payloadcms/ui/scss';
.my-component {
@include mid-break {
background-color: var(--theme-elevation-900);
}
}
一般的なパターン
条件付きフィールド表示
'use client'
import { useFormFields } from '@payloadcms/ui'
import type { TextFieldClientComponent } from 'payload'
export const ConditionalField: TextFieldClientComponent = ({ path }) => {
const showField = useFormFields(([fields]) => fields.enableFeature?.value)
if (!showField) return null
return <input type="text" />
}
API からのデータ読み込み
'use client'
import { useState, useEffect } from 'react'
export function DataLoader() {
const [data, setData] = useState(null)
useEffect(() => {
fetch('/api/custom-data')
.then((res) => res.json())
.then(setData)
}, [])
return <div>{JSON.stringify(data)}</div>
}
サーバーコンポーネントでの Local API の使用
import type { Payload } from 'payload'
async function RelatedPosts({ payload, id }: { payload: Payload; id: string }) {
const post = await payload.findByID({
collection: 'posts',
id,
depth: 0,
})
const related = await payload.find({
collection: 'posts',
where: {
category: { equals: post.category },
id: { not_equals: id },
},
limit: 5,
})
return (
<div>
<h3>Related Posts</h3>
<ul>
{related.docs.map((doc) => (
<li key={doc.id}>{doc.title}</li>
))}
</ul>
</div>
)
}
export default RelatedPosts
パフォーマンスのベストプラクティス
1. クライアントバンドルサイズを最小限に抑える
// In Admin Panel UI: import from @payloadcms/ui
'use client'
import { Button } from '@payloadcms/ui'
// In frontend code: use specific paths for tree-shaking
import { Button } from '@payloadcms/ui/elements/Button'
2. 再レンダリングを最適化する
// Bad: Re-renders on every form change
'use client'
import { useForm } from '@payloadcms/ui'
export function MyComponent() {
const { fields } = useForm()
// Re-renders on ANY field change
}
// Good: Only re-renders when specific field changes
;('use client')
import { useFormFields } from '@payloadcms/ui'
export function MyComponent({ path }) {
const value = useFormFields(([fields]) => fields[path])
// Only re-renders when this field changes
}
3. 可能な場合はサーバーコンポーネントを使用する
クライアントコンポーネントは、次が必要な場合にのみ使用してください。
- 状態(useState、useReducer)
- エフェクト(useEffect)
- イベントハンドラー(onClick、onChange)
- ブラウザー API(localStorage、window)
4. React のベストプラクティス
- 負荷の高いコンポーネントには React.memo() を使用する
- リスト内で適切な key props を設定する
- レンダリング内でのインライン関数定義を避ける
- 非同期処理には Suspense 境界を使用する
インポートマップ
Payload は、すべてのコンポーネントパスを解決するインポートマップを app/(payload)/admin/importMap.js に生成します。
手動で再生成する:
payload generate:importmap
場所を上書きする:
export default buildConfig({
admin: {
importMap: {
baseDir: path.resolve(dirname, 'src'),
importMapFile: path.resolve(dirname, 'app', 'custom-import-map.js'),
},
},
})
型安全性
コンポーネントには Payload の TypeScript 型を使用してください。
import type {
TextFieldServerComponent,
TextFieldClientComponent,
TextFieldCellComponent,
} from 'payload'
export const MyFieldComponent: TextFieldServerComponent = (props) => {
// Fully typed props
}
トラブルシューティング
「useConfig is undefined」または類似のフックエラー
原因: Payload パッケージ間の依存バージョンが一致していません。
解決策: すべての @payloadcms/* パッケージを完全に同じバージョンに固定します。
{
"dependencies": {
"payload": "3.0.0",
"@payloadcms/ui": "3.0.0",
"@payloadcms/richtext-lexical": "3.0.0"
}
}
コンポーネントが読み込まれない
- ファイルパスが正しいことを確認します(baseDir からの相対パス)
- 名前付きエクスポートの構文を確認します:
/path/to/file#ExportName payload generate:importmapを実行して再生成します- コンポーネントファイルに TypeScript エラーがないか確認します