TextControlに日本語入力ができなくなった!?

どんな状態になったか

いつものようにブロック開発をしていました。何気なく以前作ったブロックに文字入れをしようと思ってTextControlに日本語を入れようと思うと次の画像のような現象が起きました。

固定ページを編集 “コンタクトフォームブロック用の固定ページ” ‹ ブロックテストのテーマ — Wo.png

この画像でおわかりでしょうか? この画像はブロックエディタのサイドバーです。 母音はローマ字変換されているけど子音は母音の入力を待たず入力が確定しているという状態です。これまで見たことがない怪現象です。 IMEの設定がおかしくなったかと思い確認しましたが特に問題はありません。そもそも他のTextControlではこんな現象は起きていません。 よくみると漢字変換もできない状態です。つまり、一文字入力するごとにEnterキーを押したような状態です。 TextControlに何か変なPropsでも渡したかなと思って確認しても特に何もありません。 こんな時はChatGPTかと思って現象を説明しましたしたがReactの非同期処理の問題というような回答でイマイチ的を得ていませんでした。

TextControlのレンダリングコード

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というブロックの属性が書き換わるという仕組みです。 特別特殊なことは何もしてないし、何か問題が起こるなどとは全く思っていませんでした。 ところが、このコードには重大な欠陥があるのです。おわかりでしょうか?

TextControlのonChangeは一文字入力ごとに発火する

ヒントはこの表題の中にあります。 答えに至るポイントは

value={optionStyle.copy_content}

にありました。 どうしても

onChange={(newValue) => {
    setLocalOptionStyle(prev => ({ ...prev, copy_content: newValue }));
}}

の方に目が行きがちでChatGPTもonChangeでなくonBlurで処理すれば、入力の確定を遅らせることができるなんていう説明をしていました。 少し説明を加えるとonChangeというイベントは文字を入力する度に起きるのですが、onBlurはインプットボックスからフォーカスが外れないと発生しません。つまり、文字の入力の度にオブジェクトの値を更新してるのが悪いというわけです。 しかし、それはどんなTextControlでも同じでしょう。なぜ特定のTextControlでだけこの現象が起きるのでしょうか。

そこの問題ではないのです。onChangeonBlurに変えても結果は変わりません。

そもそも、TextControlに表示されている文字列はvalueに渡された値です。 次のようなコードがより一般的だと思います。

<TextControl
    value={copyInputValue}
    onChange={(newValue) => {
        setCopyInputValue(newValue);
    }}
/>

このコードと問題のコードの違いがおわかりでしょうか。 そう、一般的なコードはvaluenewValueが何も加工されずに渡っています。ところが、問題のコードは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だと思っていましたが、他の入力コントロールと違い入力途中の状態を表示するという特殊性があります。そのため入力コントロールの表示とブロックのレンダリングが必ずしも一致しないということを意識しないといけないのかということに気付きました。もちろん、ほとんど場合は一致するので気が付きにくいです。日本語入力でなく、英数字を入力していればこんな現象は起こらず欠陥には気づきません。 そういう意味でなかなか、良い勉強になったと思いました。 参考にしていただけると光栄です。