MUI DataGridで日本語入力がバグる?IME問題の原因と対策まとめ
MUIのDataGrid
コンポーネントで日本語を入力しようとすると、以下のような奇妙な挙動に遭遇したことはありませんか?
- 「て」と入力したのに「tえ」「ttええ」になる
- 文字が確定する前に変な文字列が追加される
- 編集しようとしたら未確定文字が消えてしまう
本記事では、この IME(日本語入力)とDataGridの相性問題の原因と、実用的な4つの解決策について解説します。
問題の再現コード
<DataGrid
columns={[
{
field: 'name',
headerName: '名前',
width: 200,
renderCell: (params) => (
<input
type="text"
value={params.row.name}
onChange={(e) => {
const newName = e.target.value;
setRows((prev) =>
prev.map((row) =>
row.id === params.row.id ? { ...row, name: newName } : row
)
);
}}
/>
),
},
]}
rows={rows}
/>
このように、renderCell
で <input>
を直接配置し、その中で setState
を呼ぶと、IMEでの日本語入力中に以下のような不具合が発生します:
-
文字が勝手に確定される
-
連打入力で重複した文字が入る
-
入力中にセルが再描画され、未確定文字が消える
原因:MUI DataGridとIMEの仕組みの衝突
-
renderCell
は「表示モード用のセル描画」 - 編集モードに移行せずに
<input>
を使うと、DataGridが入力状態を管理できない - IMEの未確定入力(composition中)に再レンダリングが発生すると、不正確に確定される
解決策①:renderEditCell を使う
MUIの推奨方法は、編集モード時にだけ <input>
を描画する方法です。
function CustomEditCellComponent(props: GridRenderEditCellParams) {
const [value, setValue] = useState(props.value);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = e.target.value;
setValue(newValue);
props.api.setEditCellValue({
id: props.id,
field: props.field,
value: newValue,
});
};
return <input value={value} onChange={handleChange} autoFocus />;
}
<DataGrid
columns={[
{
field: 'name',
headerName: '名前',
editable: true,
width: 200,
renderEditCell: CustomEditCellComponent,
},
]}
rows={rows}
/>
特徴
- IME未確定文字の扱いが安定する
-
autoFocus
も効く - 編集状態が明確に制御される
解決策②:processRowUpdate を使って値を保存
編集確定(Enter/Tab/Blur)時に、processRowUpdate
を使って状態を一括で更新します。
<DataGrid
editMode="cell"
processRowUpdate={(newRow) => {
setRows((prev) =>
prev.map((row) => (row.id === newRow.id ? newRow : row))
);
return newRow;
}}
columns={[ /* 省略 */ ]}
rows={rows}
/>
特徴
- 編集ロジックと表示ロジックを分離できる
-
renderEditCell
と併用することでIME安全な入力が可能 - バリデーションやAPI連携もこの中でできる
解決策③:compositionイベントを使った応急処置
どうしてもrenderCell
で常時 <input>
を表示したい場合、compositionstart
/ compositionend
を使ってIME入力中の更新を制御する方法があります。
function ComposingInput({ value }: { value: string }) {
const [localValue, setLocalValue] = useState(value);
const [isComposing, setIsComposing] = useState(false);
// 親のsetRows関数が必要なのでpropsに渡す想定
const updateRows = (newValue: string) => {
setRows((prev) =>
prev.map((row) =>
row.id === someId ? { ...row, name: newValue } : row
)
);
};
return (
<input
value={localValue}
onCompositionStart={() => setIsComposing(true)}
onCompositionEnd={(e) => {
setIsComposing(false);
updateRows(e.target.value); // 確定時にだけ更新
}}
onChange={(e) => {
setLocalValue(e.target.value);
if (!isComposing) {
updateRows(e.target.value);
}
}}
/>
);
}
解決策④:renderCell + カスタムコンポーネント(useStateで内部管理)
renderCell
に <input>
を直接書くとIME不具合が出やすいですが、状態管理をローカルに切り離すことで一部の問題を回避できます。
function EditableCell({
params,
onChange,
}: {
params: GridRenderCellParams;
onChange: (value: string, id: number) => void;
}) {
const [value, setValue] = useState(params.value);
return (
<input
value={value}
onChange={(e) => {
setValue(e.target.value);
onChange(e.target.value, params.row.id);
}}
/>
);
}
<DataGrid
columns={[
{
field: 'name',
headerName: '名前',
width: 200,
editable: true,
renderCell: (params) => (
<EditableCell
params={params}
onChange={(value, id) => {
setRows((prev) =>
prev.map((row) => (row.id === id ? { ...row, name: value } : row))
);
}}
/>
),
},
]}
rows={rows}
/>
特徴
- 常に
<input>
が表示されている -
useState
によって IME 中の文字が破棄されにくい -
composition
対応よりもシンプルに動くことがある
注意点
- 編集モードではないため、TabキーやEnterキーでの保存・フォーカス移動は自前で実装が必要
- 仮想スクロールやソートでコンポーネントが破棄・再生成されると状態が失われる可能性あり
常時編集可能 vs 編集モードの比較
パターン | 常時 <input> 表示 |
IME対応 | 編集モード | フォーカス制御 | スクロール耐性 |
---|---|---|---|---|---|
renderCell + input(直接) | ✅ | ❌ | ❌ | ❌ | ❌ |
renderCell + useState(カスタム) | ✅ | △〜◎ | ❌ | △ | △ |
renderCell + composition | ✅ | △ | ❌ | △ | △ |
renderEditCell + editable | ❌(クリックで表示) | ◎ | ✅ | ◎ | ◎ |
renderEditCell + processRowUpdate | ❌(クリックで表示) | ◎ | ✅ | ◎ | ◎ |
補足:editMode="cell"
と editMode="row"
の違い
モード | 説明 |
---|---|
cell |
セル単位で編集開始/確定できる(細かい制御向け) |
row |
行全体を一括で編集・確定(一括更新・バリデ向け) |
ベストプラクティスまとめ
方法 | IME対応 | 安定性 | 推奨度 |
---|---|---|---|
renderEditCell | ◎ | ◎ | ✅✅✅ |
processRowUpdate | - | ◎ | ✅✅ |
renderCell + useState | △〜◎ | △ | ✅(場面による) |
renderCell + composition | △ | △ | ❌(補足的対応) |
renderCell + input(直接) | ❌ | ❌ | ❌ |
まとめ
-
renderCell
で直接<input>
を書くと、IMEとぶつかって不具合が出やすい - MUIが用意している編集モード(
editable
,renderEditCell
,processRowUpdate
)を活用すれば安定動作が可能 - renderEditCellでもrenderCellでも、カスタムコンポーネント内でuseStateを使いローカル状態を持つことで、IMEの未確定入力を安定的に扱いやすくなる
- どうしても常時テキストボックスを表示したい場合は、
renderCell
+useState
+ カスタムコンポーネントの手法も検討の余地あり - UXや要件に応じて、適切に選択する必要がある