GutenbergでuseSelect()の罠にハマる
Gutenberg Dataにアクセスする時、useSelect()
使ってますか?
ついついこの書き方面倒だったので、以下のようなコードを書いてしまいました。
const s = useSelect(s => s(blockEditorStore), []);
const { attributes, name } = s.getBlock(clientId);
ブロックからブロック名と属性を取得するコードです。
でもこんな書き方せずとも
const { attributes, name } = useRegistry().select(blockEditorStore).getBlock(clientId);
とも書けるんですよね。
さて本題ですが、どちらも大問題が起きます。
ここにあるBlockEdit
はこテキストコントロールが配置してあり、それでブロックの属性を編集し変更(updateBlockAttributes())します。
ブロックに変更があればattributes
にも変化があり、再レンダリングされるはずでした・・・。
でも、反映されないのです・・・。
ブロックの属性の変更は確かに起きてます。
ただ再レンダリングされないのです。
もうすこしコードを広げます。
const InputForm = ({clientId}) =>
{
const { updateBlockAttributes } = useDispatch(blockEditorStore);
/*
const { attributes, name } = useSelect(s => {
// @ts-ignore
return s(blockEditorStore).getBlock(clientId);
}, [])
*/
console.log("Render:::::::")
const s = useSelect(s => {
console.log("UseSelect()");
return s(blockEditorStore);
}, []) as any;
const { attributes, name } = s.getBlock(clientId);
const setAttributes = useMemo(() => newAttributes => {
updateBlockAttributes(clientId, newAttributes)
}, [clientId]);
return (
<BlockEdit
name={name}
clientId={clientId}
attributes={attributes}
setAttributes={setAttributes} />
)
}
何か入力すればuseSelect()の第一引数のコールバックは確かに呼び出されます。
でもレンダリングはされない。
理由は・・・。
useSelect()にありました。
ブロックエディタに変更があるたびに第一引数のセレクタが実行されます。
ただ、このフックは内部で直前のセレクタの「実行結果」を保持しており、それが最新の「実行結果」と比較され一致しなければ再レンダリングされないようになってます。
const s = useSelect(s => s(blockEditorStore), []);
const { attributes, name } = s.getBlock(clientId);
試してはないのでまちがってるかもしれませんが、たしかに常に同じ「結果」を返しそうです。
よって「変化がない」と判断されてしまい再レンダリングされないのでしょう。
よって以下のようにブロックを取得すれば、
const { attributes, name } = useSelect(s => {
// @ts-ignore
return s(blockEditorStore).getBlock(clientId);
}, [])
属性の変化を察知するのか再レンダリングがなされます。
注意しなければいけないのは、「実行結果」の比較は参照だけによる比較ではないようです。
つまりリテラルオブジェクトを毎回作り直しても中身が同じなら「実行結果」が変わったとはみなされないようです。
何故ならこの比較に@wordpress/is-shallow-equal
のisShallowEqual
が使われているようです。
import isShallowEqual from '@wordpress/is-shallow-equal';
ちょっと正攻法ではありませんが、
const ret = {
name: 'query-search/text-query-form',
attributes: {
field: "www",
pid: "626503",
query: "hellof",
uid: "480676"
}
}
const { attributes, name } = useSelect(s => {
// @ts-ignore
const b = s(blockEditorStore).getBlock(clientId);
return ({...ret, rand: Math.random()});
}, [])
こんな感じで毎回結果の異なる乱数を生やすと再レンダリング可能なようです。
もう一つの注意点は、
const b = s(blockEditorStore).getBlock(clientId);
s()を呼び出さないとうんともすんともいいません。
useSelect()は内部でストアの変更をReactに通知してます。
どうやってかというと、useSyncExternalStore()
を使ってです。
このフックはReactのもので、独自のフックなどReactに再レンダリングをお願いしたい場合にしようするようです。
このフックに渡すgetValue()
は、useSelect()のセレクタの実行結果が変わらなければ直前の値をそのまま返します。
useSelect()
のソースコードは結構複雑で分かりにくいコードなので完全に理解してません。
間違ってる可能性もあるので注意してください。