
しかし、こんな基本的なUIでも大変な落とし穴があるものです。
この記事を見て一人でもこの落とし穴にハマる人を減らすことができれば光栄です。
いつものようにブロック開発をしていました。何気なく以前作ったブロックに文字入れをしようと思ってTextControlに日本語を入れようと思うと次の画像のような現象が起きました。

この画像でおわかりでしょうか?
この画像はブロックエディタのサイドバーです。
母音はローマ字変換されているけど子音は母音の入力を待たず入力が確定しているという状態です。これまで見たことがない怪現象です。
IMEの設定がおかしくなったかと思い確認しましたが特に問題はありません。そもそも他のTextControlではこんな現象は起きていません。
よくみると漢字変換もできない状態です。つまり、一文字入力するごとにEnterキーを押したような状態です。
TextControlに何か変なPropsでも渡したかなと思って確認しても特に何もありません。
こんな時はChatGPTかと思って現象を説明しましたしたがReactの非同期処理の問題というような回答でイマイチ的を得ていませんでした。
TextControlをレンダリングしているコードは次のようになっています。
<TextControl label="コピーテキスト" labelPosition="top" value={optionStyle.copy_content} onChange={(newValue) => { setLocalOptionStyle(prev => ({ ...prev, copy_content: newValue })); }} />
上記のコードのonChange={(newValue) => {setLocalOptionStyle(prev => ({ ...prev, copy_content: newValue }));
でこのブロックコンポーネントが持つoptionStyleオブジェクト内のcopy_content オブジェクトの値を書き換えています。
そして、
const [localOptionStyle, setLocalOptionStyle] = useState(optionStyle); // localOptionStyle の変更があるたびに setAttributes を呼び出す useEffect(() => { setAttributes({ optionStyle: localOptionStyle }); }, [localOptionStyle]);
としてあって、setLocalOptionStyleでlocalOptionStyleに変更があれば、optionStyleというブロックの属性が書き換わるという仕組みです。
特別特殊なことは何もしてないし、何か問題が起こるなどとは全く思っていませんでした。
ところが、このコードには重大な欠陥があるのです。おわかりでしょうか?
ヒントはこの表題の中にあります。
答えに至るポイントは
value={optionStyle.copy_content}
にありました。
どうしても
onChange={(newValue) => { setLocalOptionStyle(prev => ({ ...prev, copy_content: newValue })); }}
の方に目が行きがちでChatGPTもonChange
でなくonBlur
で処理すれば、入力の確定を遅らせることができるなんていう説明をしていました。
しかし、そこではないのです。
TextControlに表示されている文字列はvalue
に渡された値です。そして、次のようなコードがより一般的だと思います。
<TextControl value={copyInputValue} onChange={(newValue) => { setCopyInputValue(newValue); }} />
このコードと問題のコードの違いがおわかりでしょうか。
そう、一般的なコードはvalue
にnewValue
が何も加工されずに渡っています。ところが、問題のコードはvalue
に{ optionStyle.copy_content }
というnewValue
を加工したものを表示させています。
そうするとTextControlは入力途中で未返還の状態を維持することができず、確定した文字列を表示してしまうのです。
「TextControlのonChangeは一文字入力ごとに発火する」ということを意識していれば、そのたびにsetLocalOptionStyle(prev => ({ ...prev, copy_content: newValue }));
が働いて一文字一文字確定させていくんだというイメージが湧いたのではないでしょうか。
たとえば次のようなコードならわかりやすいでしょう。
<TextControl value={copyInputValue} onChange={(newValue) => { setCopyInputValue(`${newValue}.`); }} />
こんなコードを書くことはないでしょうが、テンプレートリテラルでnewValue
に’.’をつけるような加工をしています。これがTextControl内にレンダリングされたら、その後は日本語変換はできなくなるだろうなということは想像がつくと思います。
この問題を解消するコードを示します。
//TextControlの表示用変数 const [copyInputValue, setCopyInputValue] = useState(optionStyle.copy_content); <TextControl label="コピーテキスト" labelPosition="top" value={copyInputValue} onChange={(newValue) => { setCopyInputValue(newValue); setLocalOptionStyle(prev => ({ ...prev, copy_content: newValue })); }} />
このコードではTextControlの表示用として状態変数を一つ用意して、それをvalue
に渡しています。そうすることでTextControlの表示として加工していないnewValue
を渡すことができます。setLocalOptionStyleはこれまでどおり行えばよいのです。ブロックにレンダリングされるのはそれによって書き換えられたoptionStyle.copy_content
です。それ自身は正解で、何もonBlur
のイベント発生を待つ必要はありません。それを待っているとブロックへのレンダリングが遅れ、TextControlの入力内容がリアルタイムにブロックの表示に反映されなくなります。
TextControlは非常に一般的で基本的なUIだと思っていましたが、他の入力コントロールと違い入力途中の状態を表示するという特殊性があります。そのため入力コントロールの表示とブロックのレンダリングが必ずしも一致しないということを意識しないといけないのかということに気付きました。もちろん、ほとんど場合は一致するので気が付きにくいです。日本語入力でなく、英数字を入力していればこんな現象は起こらず欠陥には気づきません。
そういう意味でなかなか、良い勉強になったと思いました。
参考にしていただけると光栄です。