カスタムブロックを作っていると、「編集中の投稿のタイトルを使いたい」「登録済みのカテゴリー一覧をセレクトボックスに出したい」「ボタンを押したら別のブロックを挿入したい」といった、ブロック自身の属性だけでは足りない場面が出てきます。ブロックエディターはこうしたデータを @wordpress/data というパッケージのストアで一元管理しており、useSelect で読み取り、useDispatch で更新します。この記事では2つのフックの基本形から、投稿情報やカテゴリー一覧の取得、通知の表示やブロックの挿入といった実践例、値が取れないときや再レンダリングが止まらないときの原因までを解説します。WordPress 6 系、apiVersion 3 のブロックを前提としています。
目次
エディターの状態は共通のストアに集まっている
ブロックエディターの画面には、編集中の投稿の情報、配置されているブロックの一覧、どのブロックが選択されているか、公開済みかどうか、右上に出る通知など、多くの状態があります。これらは各コンポーネントがばらばらに持っているのではなく、用途ごとに分かれた「ストア」に集約されています。
ストアには名前が付いており、よく使うものは次の4つです。
| ストア名 | インポート元 | 扱うデータ |
|---|---|---|
core/block-editor | @wordpress/block-editor | 配置されているブロック、選択状態、ブロックの属性 |
core/editor | @wordpress/editor | 編集中の投稿のタイトル・抜粋・公開状態など |
core | @wordpress/core-data | 投稿・固定ページ・カテゴリー・ユーザーなど WordPress のデータ |
core/notices | @wordpress/notices | エディター上部に表示する通知 |
ストアから値を読み出す関数をセレクター、値を変更する関数をアクションと呼びます。React コンポーネントからは、セレクターを useSelect、アクションを useDispatch 経由で使います。どちらも @wordpress/data からインポートします。
useSelect でストアから値を読み取る
useSelect は、第1引数に「何を取り出すか」を書いた関数、第2引数に依存配列を渡します。関数には select が渡され、select( ストア ) でそのストアのセレクターを取得できます。編集中の投稿タイトルを取り出す例を見てみます。
import { useBlockProps } from '@wordpress/block-editor';
import { useSelect } from '@wordpress/data';
import { store as editorStore } from '@wordpress/editor';
export default function Edit() {
// 編集中の投稿タイトルを取り出す
const postTitle = useSelect(
( select ) =>
select( editorStore ).getEditedPostAttribute( 'title' ),
[]
);
return (
<div { ...useBlockProps() }>
<p>いまのタイトル:{ postTitle }</p>
</div>
);
}
タイトル欄を書き換えると、ブロックの表示も追従して変わります。useSelect は取り出した値が変わったときにコンポーネントを再レンダリングするため、自分で変更を監視する必要はありません。
ストアは文字列ではなくストアオブジェクトで指定する
select( 'core/editor' ) のように文字列でも動きますが、現在は各パッケージが公開している store をインポートして渡す書き方が推奨されています。
import { store as blockEditorStore } from '@wordpress/block-editor';
import { store as coreStore } from '@wordpress/core-data';
import { store as noticesStore } from '@wordpress/notices';
select( blockEditorStore ); // 推奨
select( 'core/block-editor' ); // 動くが文字列の綴りミスに気づけない
文字列を1文字間違えると select() が undefined を返し、その先のセレクター呼び出しでエラーになります。インポートしておけばビルド時点で気づけるので、こちらのほうが安全です。
複数の値はオブジェクトにまとめて返す
useSelect を値の数だけ並べることもできますが、関連する値はひとつの useSelect でオブジェクトとして返すほうがすっきりします。
const { postTitle, isPublished } = useSelect( ( select ) => {
const { getEditedPostAttribute, isCurrentPostPublished } =
select( editorStore );
return {
postTitle: getEditedPostAttribute( 'title' ),
isPublished: isCurrentPostPublished(),
};
}, [] );
戻り値のオブジェクトは毎回新しく作られますが、useSelect は中身を浅く比較して判断するため、これで無駄な再レンダリングが起きることはありません。プロパティの値がすべて同じなら「変わっていない」と扱われます。
第2引数の依存配列は何を指定するのか
依存配列は useMemo や useCallback と同じ考え方で、第1引数の関数を作り直す条件を書きます。ストアの値だけを見ているなら空配列で構いません。関数の中で props や属性を使っている場合は、それを配列に入れます。
export default function Edit( { attributes, clientId } ) {
const { categoryId } = attributes;
// 関数内で使っている categoryId を依存配列に入れる
const category = useSelect(
( select ) =>
select( coreStore ).getEntityRecord(
'taxonomy',
'category',
categoryId
),
[ categoryId ]
);
// …
}
依存配列に入れ忘れると、categoryId を変更しても古い値のまま取得し続けてしまいます。「関数の外から持ち込んだ値はすべて入れる」と覚えておけば間違いありません。
実践:カテゴリー一覧をセレクトボックスに出す
core ストア(@wordpress/core-data)を使うと、REST API 経由で投稿やタクソノミーのデータを取得できます。getEntityRecords にデータの種類を渡すだけで、通信も結果の保持もエディター側が面倒を見てくれます。
import { InspectorControls, useBlockProps } from '@wordpress/block-editor';
import { PanelBody, SelectControl } from '@wordpress/components';
import { useSelect } from '@wordpress/data';
import { store as coreStore } from '@wordpress/core-data';
export default function Edit( { attributes, setAttributes } ) {
const categories = useSelect(
( select ) =>
select( coreStore ).getEntityRecords( 'taxonomy', 'category', {
per_page: -1,
} ),
[]
);
// 取得中は undefined、取得後に配列が入る
const options = ( categories ?? [] ).map( ( category ) => ( {
label: category.name,
value: String( category.id ),
} ) );
return (
<>
<InspectorControls>
<PanelBody title="表示設定">
<SelectControl
label="カテゴリー"
value={ attributes.categoryId }
options={ options }
onChange={ ( value ) =>
setAttributes( { categoryId: value } )
}
__nextHasNoMarginBottom
/>
</PanelBody>
</InspectorControls>
<div { ...useBlockProps() }>{ /* 表示 */ }</div>
</>
);
}
getEntityRecords は初回の呼び出しでは undefined を返し、裏側で通信を行ったうえで、結果が揃った時点でもう一度セレクターが評価されます。そのため categories.map() と直接書くと最初のレンダリングでエラーになります。?? [] のように、データが無い状態を前提にしたコードにしておく必要があります。
useDispatch でストアを更新する
読み取りが useSelect なら、書き込みは useDispatch です。ストアを渡すと、そのストアのアクションがまとまったオブジェクトが返ってくるので、必要なものを分割代入で受け取ります。
import { Button } from '@wordpress/components';
import { useDispatch } from '@wordpress/data';
import { store as noticesStore } from '@wordpress/notices';
export default function Edit() {
const { createSuccessNotice } = useDispatch( noticesStore );
return (
<div { ...useBlockProps() }>
<Button
variant="primary"
onClick={ () =>
createSuccessNotice( '設定を反映しました。', {
type: 'snackbar',
} )
}
>
通知を出す
</Button>
</div>
);
}
type: 'snackbar' を付けると画面左下に小さく出て自動で消えます。付けない場合はエディター上部に警告と同じ形で表示され、閉じるまで残ります。処理の完了を伝えるだけなら snackbar のほうが邪魔になりません。
ブロックを操作するアクション
core/block-editor のアクションを使うと、ブロックの挿入や属性の更新をコードから行えます。次は、自分のブロックの内側に段落を追加するボタンの例です。
import { store as blockEditorStore } from '@wordpress/block-editor';
import { createBlock } from '@wordpress/blocks';
import { useDispatch, useSelect } from '@wordpress/data';
export default function Edit( { clientId } ) {
const { insertBlocks } = useDispatch( blockEditorStore );
// 自分の内側にあるブロックの数
const innerCount = useSelect(
( select ) => select( blockEditorStore ).getBlockCount( clientId ),
[ clientId ]
);
const addParagraph = () => {
insertBlocks(
createBlock( 'core/paragraph', { content: '追加された段落' } ),
innerCount, // 挿入位置(末尾)
clientId // 挿入先の親ブロック
);
};
// …
}
ブロックの識別子である clientId は edit の props として渡ってきます。insertBlocks は挿入するブロック・位置・親ブロックの clientId を受け取るので、自分自身を親に指定すれば内側に追加できます。
なお、自分のブロックの属性を変えたいだけなら useDispatch は不要です。edit の props にある setAttributes を使ってください。ストア経由の updateBlockAttributes は、他のブロックの属性を書き換えたいときの手段です。
値が取れない・再レンダリングが止まらないとき
セレクターの中で配列を作り直している
戻り値は浅く比較されるため、プロパティが同じ値なら再レンダリングは起きません。ところが、セレクターの中で map() や filter() を使って新しい配列を作って返すと、中身が同じでも毎回別の配列になり、比較で「変わった」と判定されてしまいます。
// 避けたい書き方:毎回新しい配列を返す
const titles = useSelect(
( select ) =>
select( blockEditorStore )
.getBlocks()
.map( ( block ) => block.name ),
[]
);
// ストアからは素の値を取り、加工はコンポーネント側で行う
const blocks = useSelect(
( select ) => select( blockEditorStore ).getBlocks(),
[]
);
const names = blocks.map( ( block ) => block.name );
取得中の undefined を考慮していない
core ストアのセレクターは通信を伴うため、値が揃うまで undefined を返します。エラーになるのはたいていこの間です。読み込み中の表示を出したい場合は、Spinner コンポーネントと組み合わせて分岐させると親切です。
import { Spinner } from '@wordpress/components';
if ( ! categories ) {
return (
<div { ...useBlockProps() }>
<Spinner />
</div>
);
}
ただし、フックはコンポーネントの先頭で常に同じ順序で呼ぶ必要があるため、早期 return は useSelect や useDispatch をすべて呼んだあとに書きます。条件分岐の中でフックを呼ぶとエラーになります。
save や表示側で使おうとしている
useSelect と useDispatch はエディターの中で動く仕組みです。save はマークアップを組み立てるだけの関数なので、ここでストアを読むことはできません。フロント側で投稿やカテゴリーの情報を使いたいときは、動的ブロックにして render.php の中で PHP の関数を呼ぶのが正しい方法です。
また、core/editor は投稿編集画面のストアです。サイトエディターのように文脈が異なる画面ではセレクターが期待どおりに動かないことがあるため、ブロックの配置場所を選ばないようにしたい場合は、core/block-editor や core ストアで済む設計にしておくと安全です。
まとめ
ブロックエディターの状態は用途ごとのストアに集まっており、useSelect で読み取り、useDispatch で更新します。useSelect は取り出した値が変わったときだけ再レンダリングしてくれるので、変更の監視を自分で書く必要はありません。ストアは文字列ではなく各パッケージの store をインポートして渡し、関数の外から持ち込んだ値は依存配列に入れるのが基本の形です。
投稿の情報は core/editor、ブロックの構造は core/block-editor、カテゴリーやユーザーなどのデータは core、通知は core/notices と、目的から使うストアを選びます。core ストアは通信を伴うため最初は undefined が返ってくることを前提にコードを書いておくと、原因の分かりにくいエラーを避けられます。
自分の属性を変えるだけなら setAttributes、他のブロックや画面全体に働きかけたいときに useDispatch、と役割を分けて考えると迷いません。この2つのフックが使えるようになると、エディター上での使い勝手を作り込めるようになります。
参考ページ
- @wordpress/data | Block Editor Handbook | WordPress Developer Resources
- WordPress Core Block Editor Data | Block Editor Handbook | WordPress Developer Resources
- WordPress Core Data | Block Editor Handbook | WordPress Developer Resources
- Notices Data | Block Editor Handbook | WordPress Developer Resources