MUI(Material UI)のAutocompleteは、入力に応じて候補を絞り込むReactのコンポーネントです。文字列の配列なら数行で動きますが、APIから受け取ったオブジェクトの配列を渡した途端に「選んだはずの項目が選択済みにならない」「初期値が入力欄に出ない」といった詰まり方をします。原因になりやすい不一致の警告はv6でコンソールから消え、2026年10月の9.5.0ではIDのまま値を持てるgetOptionValueが加わりました。この記事では最新の@mui/material 9.5.0とReact 19.3.1でコンポーネントを実際に描画し、DOMとコンソール出力を確かめたうえで、症状ごとの原因と対処をコード付きで整理します。HTMLのinput要素が持つautocomplete属性(ブラウザのオートフィル)は同名の別機能なので、後半で切り分けます。
まとめ
- optionsとは別に取得したオブジェクトをvalueに渡すなら、
isOptionEqualToValueで比較方法を指定します。既定の比較は厳密等価(===)なので、APIから取り直した同じ内容のオブジェクトは別物と判定されます。 - その不一致を知らせる警告はv6で削除されました。入力欄の表示は正しいまま選択状態だけが欠落するため、コンソールを見ても気づけません。
- valueをIDで持つ設計に変えるなら、9.5.0以降は
getOptionValue={(o) => o.id}でvalueとonChangeをIDのまま扱えます。ただしoptionsに無いIDを渡すと、警告なしで入力欄が空になります。 - 未選択の初期値は、単一選択なら
null、複数選択なら空配列で与えます。undefinedで初期化すると非制御コンポーネントとして確定し、後から渡した値が反映されません。 - v9では
renderInputに渡るparamsが変わり、params.InputPropsはparams.slotProps.inputになりました。v5世代の非同期サンプルをそのまま貼ると型エラーになります。
基本構成|optionsとrenderInputの最小実装
文字列とlabelキーで動く既定の挙動
Autocompleteが最低限必要とするのはoptionsとrenderInputの2つです。renderInputは入力欄そのものを描く関数で、受け取ったparamsをTextFieldへ展開します。optionsの要素が文字列、またはlabelキーを持つオブジェクトなら、ラベルの取り出しは追加設定なしで動きます。
import Autocomplete from '@mui/material/Autocomplete';
import TextField from '@mui/material/TextField';
const frameworks = [{ label: 'React' }, { label: 'Vue' }, { label: 'Svelte' }];
export function FrameworkSelect() {
return (
<Autocomplete
options={frameworks}
sx={{ width: 320 }}
renderInput={(params) => <TextField {...params} label="フレームワーク" />}
/>
);
}
既定のgetOptionLabelが「文字列ならそのまま、オブジェクトならlabelプロパティ」を読むため、この形で動きます。データ側のキー名をlabelに寄せられるなら、最小構成のまま進めて構いません。
オブジェクト配列でのgetOptionLabel指定
APIレスポンスがlabelではなくnameやtitleで表示名を返す場合、キーが合わないまま渡すと入力欄に[object Object]が表示され、開発ビルドでは次のエラーが出ます。
MUI: The `getOptionLabel` method of Autocomplete returned object ([object Object])
instead of a string for {"id":1,"name":"React"}.
getOptionLabelに「オブジェクトから表示用の文字列を返す関数」を渡せば解消します。返り値は必ず文字列にしてください。数値のIDをそのまま返しても同じエラーになります。
選択済みの項目が反映されない原因|isOptionEqualToValueの実測
値をセットしているのに、候補リスト上で選択済みとして扱われない症状があります。既定のisOptionEqualToValueは(option, value) => option === valueという厳密等価で、オブジェクトは参照が同一でなければ一致しません。サーバーから取得したvalueと、別に取得したoptionsは、中身が同じでも異なるオブジェクトです。
v6で削除された警告メッセージ
v5系では、この不一致をコンソールが教えてくれていました。
MUI: The value provided to Autocomplete is invalid.
None of the options match with `{"id":1,"name":"React"}`.
You can use the `isOptionEqualToValue` prop to customize the equality test.
ところがこの文字列は、v6.0.0以降のパッケージにありません。npmで配布されている各版のtarballを取得し、該当文字列を検索した結果が次の表です。
| バージョン | 不一致の警告 |
|---|---|
| 5.18.0 | あり |
| 6.0.0 | なし |
| 6.5.0 | なし |
| 7.3.11 | なし |
| 9.3.0 | なし |
| 9.5.0 | なし |
「isOptionEqualToValue」で検索してたどり着く警告文は、v5世代のものです。v6以降は同じ不具合が起きてもコンソールは沈黙するので、症状から原因を引く必要があります。
aria-selectedで確認した実測結果
9.5.0のAutocompleteを候補リストを開いた状態で描画し、li[role="option"]のaria-selectedを採取しました。valueにはoptionsと同じ内容で参照だけが異なるオブジェクトを渡しています。
| 条件 | 入力欄の表示 | 該当optionのaria-selected | コンソール出力 |
|---|---|---|---|
| 比較関数なし・別参照 | Vue | false | なし |
| isOptionEqualToValueあり・別参照 | Vue | true | なし |
| options内の同一参照 | Vue | true | なし |
どの条件でも入力欄には正しく「Vue」と表示されます。画面上は正常に見えるため、不具合は「リストを開いても選択中の項目に印が付かない」「複数選択でチップは出るのに候補側の状態が合わない」という形で見つかります。multipleでも同じで、別参照のオブジェクトを値に持つ限り、比較関数を渡さないと選択済みの選択肢には一つも印が付きませんでした。スクリーンリーダーはaria-selectedを読み上げるので、見た目だけの問題ではなくアクセシビリティの欠陥です。
idを比較するisOptionEqualToValueの実装
対処は、業務上の同一性を表すキーで比較する関数を渡すことです。多くの場合はIDの一致で足ります。値の状態もvalueとonChangeで持っておくと、後述の初期値の問題も同時に避けられます。
import * as React from 'react';
import Autocomplete from '@mui/material/Autocomplete';
import TextField from '@mui/material/TextField';
export type Framework = { id: number; name: string };
export const list: Framework[] = [
{ id: 1, name: 'React' },
{ id: 2, name: 'Vue' },
{ id: 3, name: 'Svelte' },
];
export function FrameworkSelectById() {
const [value, setValue] = React.useState<Framework | null>(null);
return (
<Autocomplete
options={list}
value={value}
onChange={(_event, newValue) => setValue(newValue)}
getOptionLabel={(option) => option.name}
isOptionEqualToValue={(option, selected) => option.id === selected.id}
renderInput={(params) => <TextField {...params} label="フレームワーク" />}
/>
);
}
比較関数は、必ず1件だけに一致する条件にしてください。multipleで1つの選択肢が複数の値に一致すると、開発ビルドでdoes not handle the arguments correctlyを含むエラーが出ます。名前など重複しうる項目での比較は避けます。表示名が重複する選択肢がある場合は、Reactのkeyが衝突しないようgetOptionKey={(option) => option.id}も指定します。以降の例では、このコードをframeworks.tsxとして保存し、同じディレクトリからFramework型とlistをimportして再利用します。
ID値による選択管理|9.5.0のgetOptionValue
フォームやAPIが保存するのは選択肢のオブジェクトではなくIDです。9.4.0までは、IDからoptions.find()でオブジェクトを引き直してvalueに渡し、onChangeで再びIDへ戻す変換を自前で書く必要がありました。9.5.0のリリースノートはこれを「Support values other than raw options」としてgetOptionValueで解決しています。
getOptionValueの書き方と型
getOptionValueに選択肢からIDを取り出す関数を渡すと、value・defaultValue・onChangeの値がIDになります。比較にもこの戻り値が使われるため、isOptionEqualToValueは不要です。getOptionLabelやrenderOptionには従来どおり元のオブジェクトが渡されます。
import * as React from 'react';
import Autocomplete from '@mui/material/Autocomplete';
import TextField from '@mui/material/TextField';
import { type Framework, list } from './frameworks';
const getId = (option: Framework) => option.id;
const getName = (option: Framework) => option.name;
export function FrameworkIdSelect() {
const [frameworkId, setFrameworkId] = React.useState<number | null>(2);
return (
<Autocomplete
options={list}
value={frameworkId}
onChange={(_event, newId) => setFrameworkId(newId)}
getOptionValue={getId}
getOptionLabel={getName}
renderInput={(params) => <TextField {...params} label="フレームワーク" />}
/>
);
}
TypeScriptではonChangeのnewIdがnumber | nullと推論されます。getIdとgetNameをコンポーネントの外に置いているのは、公式ドキュメントが「選択肢の照合用キャッシュを再利用するため、getOptionValueは描画間で同じ関数に保つ」よう求めているためです。コンポーネント内で書くならReact.useCallbackで包みます。取引先や品目のマスタをIDで保存する業務画面では、この変換コードがそのまま消えます。基幹システム開発のようにマスタ選択欄が多い画面ほど、IDで値を持つ書き方に揃える効果が大きくなります。
getOptionValueの実測|optionsに無いIDとfreeSoloの制約
9.5.0を開発ビルドで描画し、条件ごとに入力欄・aria-selected・コンソールを記録しました。
| 条件 | 入力欄 | コンソール |
|---|---|---|
| value=2 | Vue(選択済みの印あり) | なし |
| Svelteをクリック | Svelte(onChangeの値は3) | なし |
| options=[]で描画後に候補が到着 | 空→Vue | なし |
| optionsに無いvalue=99 | 空 | なし |
| IDが重複(value=null) | 空(未選択) | エラー |
| freeSoloで文字列IDを返す(value=null) | 空(未選択) | エラー |
注意すべきは4行目です。optionsに存在しないIDを渡しても警告は出ず、入力欄が空になるだけです。削除済みのマスタを参照しているデータや、検索結果の候補だけをoptionsに入れている検索窓で起こります。保存済みの値が画面に出ないときは、そのIDがoptionsに含まれているかを最初に確かめてください。3行目のとおり、候補を非同期で後から読み込む場合は、到着した時点でラベルが入ります。
freeSoloと組み合わせるときは、getOptionValueが数値か真偽値を返す必要があります。文字列は自由入力の値と区別できないためで、文字列のIDを返すとreturned the string value "1" while `freeSolo` is enabled.というエラーになります。IDが文字列のAPIでfreeSoloを使う画面は、従来どおりisOptionEqualToValueで比較してください。
初期値の設定|nullとundefinedで変わる制御の扱い
undefinedで初期化したときの挙動
フォームの初期値をAPIから取得する画面では、取得完了までvalueがundefinedになる書き方をしがちです。MUIは初回レンダリング時点でvalueがundefinedかどうかを見て、制御コンポーネントか非制御かを決めます。後から値を渡すと次のエラーになります。
MUI: A component is changing the uncontrolled value state of Autocomplete to be controlled.
Elements should not switch from uncontrolled to controlled (or vice versa).
Decide between using a controlled or uncontrolled Autocomplete element for the lifetime of the component.
The nature of the state is determined during the first render. It's considered controlled if the value is not `undefined`.
警告だけでは終わりません。undefinedから値ありへ切り替える手順を再現すると、エラーが出たうえで入力欄の値は空文字のままでした。渡した値は画面に反映されません。
nullで初期化する書き方
単一選択ではnull、multipleでは空配列を初期値にします。これらは「未選択」を明示する値なので、初回から制御コンポーネントとして確定します。同じ手順をvalue={null}始まりで再現すると、警告は出ず、後から渡した値が入力欄に反映されました。データ取得中に一時的にundefinedを経由させないよう、状態の初期値そのものをnullにしておくのが確実です。React Hook FormのControllerで包む場合も、defaultValuesにnullを入れる点は同じです。refの扱いなど連携の詳細はMUIとReact Hook Formの連携|インストールとref設定・v9とv8 betaの変更点で扱っています。
複数選択と自由入力|multipleとfreeSoloの設定
multipleでのrenderOptionとkeyの分離
multipleを付けると値は配列になり、選択済みの項目はチップで表示されます。候補の行を自前で描くrenderOptionでは、第1引数のpropsにkeyが含まれます(v5.18.0も同じです)。propsをそのまま展開するとkeyをスプレッド経由で渡すことになり、React 19はA props object containing a "key" prop is being spread into JSXを出します。分割代入で取り出して個別に指定してください。
import * as React from 'react';
import Autocomplete from '@mui/material/Autocomplete';
import TextField from '@mui/material/TextField';
import { type Framework, list } from './frameworks';
export function TechSelect() {
const [values, setValues] = React.useState<Framework[]>([]);
return (
<Autocomplete
multiple
options={list}
value={values}
onChange={(_event, newValues) => setValues(newValues)}
getOptionLabel={(option) => option.name}
isOptionEqualToValue={(option, selected) => option.id === selected.id}
renderOption={(props, option) => {
const { key, ...optionProps } = props;
return (
<li key={key} {...optionProps}>
{option.name}
</li>
);
}}
renderInput={(params) => <TextField {...params} label="使用技術" />}
/>
);
}
propsにはkeyのほかrole・aria-selected・data-option-indexなどが含まれます。候補のハイライトとaria-activedescendantの解決に使われる属性なので、liへ渡さずに独自の要素だけを描くとキーボード操作の前提が崩れます。propsは必ず展開してください。IDの配列で持ちたいなら、ここでもgetOptionValueが使え、value={[1, 3]}でReactとSvelteのチップが出ることを確認しています。
freeSoloでの入力値の受け取り
freeSoloは候補に無い文字列の入力を許可する設定で、検索窓のように使う場面に向きます。選択された値と入力中の文字列は別の状態で、前者はvalueとonChange、後者はinputValueとonInputChangeで扱います。検索キーワードを取りたいだけなら、入力値の側だけを持てば足ります。
import * as React from 'react';
import Autocomplete from '@mui/material/Autocomplete';
import TextField from '@mui/material/TextField';
import { list } from './frameworks';
export function KeywordInput() {
const [input, setInput] = React.useState('');
return (
<Autocomplete
freeSolo
options={list.map((item) => item.name)}
inputValue={input}
onInputChange={(_event, newInput) => setInput(newInput)}
renderInput={(params) => <TextField {...params} label="キーワード" />}
/>
);
}
freeSoloではonChangeの値が選択肢の型ではなく文字列で来ることがあり、TypeScriptではFramework | stringのような合併型になります。値を使う側で型を絞り込むか、上の例のように入力値だけを扱う設計にすると破綻しません。
freeSoloを使わずに入力中の文字を残すclearOnBlur
freeSoloを付けていないと、候補を選ばずにフォーカスを外した時点で入力中の文字が消えます。clearOnBlurの既定値が!props.freeSoloだからです。「文字は残したいが、値の型に文字列を混ぜたくない」ときは、freeSoloを付けずにclearOnBlur={false}だけを指定します。
import Autocomplete from '@mui/material/Autocomplete';
import TextField from '@mui/material/TextField';
import { list } from './frameworks';
export function KeepTypedText() {
return (
<Autocomplete
options={list}
clearOnBlur={false}
getOptionLabel={(option) => option.name}
isOptionEqualToValue={(option, selected) => option.id === selected.id}
renderInput={(params) => <TextField {...params} label="フレームワーク" />}
/>
);
}
9.5.0で「Rea」と入力してフォーカスを外すと、既定では入力欄が空に戻り、clearOnBlur={false}では「Rea」が残りました。どちらの場合もvalueはnullのままで、onChangeの型はFramework | nullから変わりません。入力中の文字を独自のstateで持ち直す実装は不要です。
候補の絞り込み|createFilterOptionsと大量データ対策
一致条件とstringifyの指定
既定の絞り込みは、ラベルに対する部分一致です。前方一致に変えたい、ラベル以外の項目も検索対象にしたいといった要件はcreateFilterOptionsで組み立てます。設定項目はignoreAccents・ignoreCase・limit・matchFrom・stringify・trimです。
import Autocomplete, { createFilterOptions } from '@mui/material/Autocomplete';
import TextField from '@mui/material/TextField';
import { type Framework, list } from './frameworks';
const filterOptions = createFilterOptions<Framework>({
matchFrom: 'start',
ignoreCase: true,
limit: 20,
stringify: (option) => option.name,
});
export function FilteredSelect() {
return (
<Autocomplete
options={list}
filterOptions={filterOptions}
getOptionLabel={(option) => option.name}
isOptionEqualToValue={(option, selected) => option.id === selected.id}
renderInput={(params) => <TextField {...params} label="フレームワーク" />}
/>
);
}
stringifyは検索対象の文字列を組み立てる関数です。型番と製品名の両方でヒットさせたいときは、複数の項目を連結して返します。生成した関数はモジュールスコープに置いて使い回してください。絞り込みは候補リストを開いている間つねに実行されるので、関数まで毎レンダー作り直す必要はありません。
表示件数の制限と9.5.0の打ち切り
Autocompleteはリストを閉じている間、候補のDOMを描きません。負荷がかかるのは開いた瞬間で、既定では絞り込み後の候補が全件そのままli要素になります。9.3.0をjsdom上で描画し、件数を変えて計測すると、リストを開く描画は100件で約62ミリ秒、10,000件で約1,114ミリ秒と件数の増加に伴って伸び、li要素の数は常に候補数と一致しました。既定では仮想化されません。なおgetOptionValueをisOptionEqualToValueなしで使うと、照合用に全候補からMapを作る処理がリストの開閉に関係なく走ります(options・getOptionValueが変わったときに作り直されます)。
軽い対処がlimitです。9.5.0では「Stop filtering once the result limit is reached」の修正が入り、上限に達した時点で走査を止めるようになりました。item-0からitem-99999までの10万件に対して入力文字列item(全件が一致する条件)で、createFilterOptions({ limit: 20 })が返した関数を5回空打ちしたあと15回呼び、中央値を比べました。Node.js 26.5.0・Intel Core i9-9880H搭載のMacで、絞り込み関数だけを測った参考値であり、実ブラウザでの描画時間ではありません。
| バージョン | limitなし | limit: 20 |
|---|---|---|
| 9.4.0 | 約20ms | 約20ms |
| 9.5.0 | 約21ms | 約0.01ms |
9.4.0までのlimitは全件を絞り込んでから先頭を切り出していたため、表示件数は減っても絞り込みの計算量は減りませんでした。一致する候補が後方にしか無い場合は9.5.0でも走査が長くなります。全件をスクロールさせる必要がある画面では、公式ドキュメントが10,000件の例で示しているreact-windowによる仮想化に進みます。仮想化はリスト要素の差し替えを伴うので、先にlimitで足りるかを確かめる順序が無駄になりません。
非同期取得とv9のslotProps移行
renderInputに渡るparamsの構造変更
候補をAPIから取得する実装では、読み込み中にスピナーを出すためにrenderInputのparamsへ手を入れます。ここがv9で変わりました。各版の型定義を比較すると次のとおりです。
| バージョン | 入力要素 | ラベル | input属性 |
|---|---|---|---|
| 5.18.0 | InputProps | InputLabelProps | inputProps |
| 7.3.11 | InputProps | InputLabelProps | inputProps |
| 9.3.0/9.5.0 | slotProps.input | slotProps.inputLabel | slotProps.htmlInput |
v9ではInputPropsが型から消え、slotPropsの下にまとめられました。検索で見つかる非同期サンプルの多くはv5からv7の世代なので、9系にそのまま貼るとTypeScriptがProperty 'InputProps' does not existを返します。移行は参照先をparams.slotProps.inputに置き換えるだけです。
loading表示を組み込んだ実装
次が9系での書き方です。slotPropsを丸ごと上書きするとラベルやinput属性の設定が消えるので、既存のslotPropsを展開したうえでinputだけを差し替えます。
import * as React from 'react';
import Autocomplete from '@mui/material/Autocomplete';
import TextField from '@mui/material/TextField';
import CircularProgress from '@mui/material/CircularProgress';
import { type Framework } from './frameworks';
export function AsyncSelect() {
const [open, setOpen] = React.useState(false);
const [options, setOptions] = React.useState<Framework[]>([]);
const [loading, setLoading] = React.useState(false);
React.useEffect(() => {
if (!open) {
return undefined;
}
const controller = new AbortController();
setLoading(true);
fetch('/api/frameworks', { signal: controller.signal })
.then((res) => {
if (!res.ok) {
throw new Error(`HTTP ${res.status}`);
}
return res.json();
})
.then((json: Framework[]) => setOptions(json))
.catch((error: unknown) => {
if (!(error instanceof DOMException && error.name === 'AbortError')) {
console.error(error);
}
})
.finally(() => {
if (!controller.signal.aborted) {
setLoading(false);
}
});
return () => {
controller.abort();
setLoading(false);
};
}, [open]);
return (
<Autocomplete
open={open}
onOpen={() => setOpen(true)}
onClose={() => setOpen(false)}
options={options}
loading={loading}
loadingText="読み込み中…"
noOptionsText="該当する候補がありません"
getOptionLabel={(option) => option.name}
isOptionEqualToValue={(option, selected) => option.id === selected.id}
renderInput={(params) => (
<TextField
{...params}
label="フレームワーク"
slotProps={{
...params.slotProps,
input: {
...params.slotProps.input,
endAdornment: (
<React.Fragment>
{loading ? <CircularProgress color="inherit" size={20} /> : null}
{params.slotProps.input.endAdornment}
</React.Fragment>
),
},
}}
/>
)}
/>
);
}
loadingをoptions.length === 0から導かず独立したstateにしているのは、空配列の正常応答や通信失敗のあとに「読み込み中…」とスピナーが残り続けるのを防ぐためです。v5時代のサンプルに多いopen && options.length === 0の書き方では、候補が0件の応答でこの状態になります。AbortControllerは、候補リストを開閉するたびにリクエストが積み上がるのを防ぐためのもので、中断時の例外は通常のエラーと区別して握りつぶします。
loadingTextとnoOptionsTextを指定しているのは、既定が英語の「Loading…」「No options」だからです。9.3.0からこの2つの文言はrole="status"・aria-live="polite"を持つstatusスロットに入り、スクリーンリーダーが読み上げるようになりました。9.5.0で描画すると、読み込み中はdiv.MuiAutocomplete-statusに「Loading…」が入ることを確認しています。英語のまま読み上げられないよう、アプリ全体ならcreateTheme({}, jaJP)で日本語ロケール(「読み込み中…」「データがありません」)を当てるのが手早い方法です。
入力ごとの再取得を抑えるdebounce
入力語をサーバーへ送って候補を引く検索窓では、1文字ごとにリクエストが飛びます。一定時間入力が止まってから投げるようタイマーを挟み、クリーンアップでタイマーと通信の両方を取り消します。サーバー側で絞り込むなら、クライアント側の絞り込みは無効化しておかないと二重に候補が削られます。
import * as React from 'react';
import Autocomplete from '@mui/material/Autocomplete';
import TextField from '@mui/material/TextField';
import { type Framework } from './frameworks';
export function DebouncedSearch() {
const [input, setInput] = React.useState('');
const [options, setOptions] = React.useState<Framework[]>([]);
React.useEffect(() => {
if (input === '') {
setOptions([]);
return undefined;
}
const controller = new AbortController();
const timer = setTimeout(() => {
fetch(`/api/frameworks?q=${encodeURIComponent(input)}`, { signal: controller.signal })
.then((res) => {
if (!res.ok) {
throw new Error(`HTTP ${res.status}`);
}
return res.json();
})
.then((json: Framework[]) => setOptions(json))
.catch((error: unknown) => {
if (error instanceof DOMException && error.name === 'AbortError') {
return;
}
console.error(error);
setOptions([]);
});
}, 300);
return () => {
clearTimeout(timer);
controller.abort();
};
}, [input]);
return (
<Autocomplete
options={options}
filterOptions={(option) => option}
inputValue={input}
onInputChange={(_event, newInput) => setInput(newInput)}
getOptionLabel={(option) => option.name}
isOptionEqualToValue={(option, selected) => option.id === selected.id}
renderInput={(params) => <TextField {...params} label="キーワード" />}
/>
);
}
filterOptions={(option) => option}が、受け取った候補をそのまま返してクライアント側の絞り込みを止める指定です。忘れると、サーバーが返した候補にさらに入力文字列での部分一致が掛かり、表記ゆれや読みの違いで候補が消えます。この形でgetOptionValueを併用する場合、検索結果に含まれない選択済みのIDは入力欄が空になります。選択済みの項目を常にoptionsへ含めておいてください。
ブラウザのオートフィルとautocomplete属性の切り分け
MUIのAutocompleteとautocomplete属性の役割分担
MUIのAutocompleteと、HTMLのinput要素が持つautocomplete属性は別物です。前者は候補リストをアプリ側で描くReactコンポーネント、後者はブラウザに保存された氏名や住所を補完させるための指定で、ワンタイムコード欄のautocomplete="one-time-code"などが該当します。
フォームの入力支援として入力を速くしたいだけなら、MUIのAutocompleteを持ち出す必要はありません。ブラウザ標準の補完に任せたほうが実装も保守も軽く済みます。候補データを自分で持つ必要があるかを先に確認してください。MUIのデザインごと持ち込みたくない場合は、Base UI(React)とは?Radix UI・MUIとの違いと使い方【v1.9対応】で扱うヘッドレスの実装も選択肢になります。
補完候補が二重に出るときの抑制
MUIのAutocompleteを住所やメールアドレスの入力に使うと、ブラウザの補完候補がMUIの候補リストに重なることがあります。MUIは既定でautoComplete="off"を付けていますが、公式ドキュメントはChromeがこの指定に従わないと注記しています。公式が試す価値のある回避策として挙げるのが、input要素のautocomplete属性をnew-passwordに変える方法です。抑制が保証されるわけではなく、ブラウザによっては強力なパスワードの提案が出る点も公式に注記されています。v9ではhtmlInputスロット経由で指定します。
import Autocomplete from '@mui/material/Autocomplete';
import TextField from '@mui/material/TextField';
const prefectures = ['北海道', '東京都', '大阪府'];
export function PrefectureSelect() {
return (
<Autocomplete
options={prefectures}
renderInput={(params) => (
<TextField
{...params}
label="お住まいの都道府県"
slotProps={{
...params.slotProps,
htmlInput: {
...params.slotProps.htmlInput,
autoComplete: 'new-password',
},
}}
/>
)}
/>
);
}
v5からv7では同じ指定をinputProps経由で行います。input要素のidにcountryのような意味のある名前を付けないことも、公式が挙げる対処です。ブラウザ側の挙動はMDNのフォーム自動補完を無効にする方法にまとまっています。入力値の妥当性そのものを担保する話は別レイヤーなので、バリデーションとは?入力検証の目的・種類・実装パターンと設計を実装者視点で解説とあわせて設計してください。
よくある質問
valueがnullのとき、isOptionEqualToValueは呼ばれますか?
呼ばれません。比較は値が存在する場合にだけ行われるため、未選択状態でnullを渡していて比較関数が実行されないのは正常です。比較関数の中でselected.idのようにnullチェックなしでアクセスしても、未選択が原因で落ちることはありません。未選択時にエラーが出るなら、スタックトレースからonChangeやrenderOptionなど独自に書いた処理を確認してください。
isOptionEqualToValueとgetOptionValueはどちらを使うべきですか?
9.5.0以降でvalueをIDのまま持ちたいならgetOptionValueです。オブジェクトのまま持つ既存コードや、freeSoloで文字列のIDを扱う画面はisOptionEqualToValueのままで構いません。両方を同時に渡すこともでき、その場合isOptionEqualToValueの第2引数にはgetOptionValueの戻り値が渡されます。
コンソールに出るMUIのエラーは、どれから直すべきですか?
getOptionLabelが文字列を返していない旨のエラーが最優先です。表示が壊れているためです。次が非制御から制御への切り替えで、値が反映されない直接の原因になります。選択状態が合わない不具合と、optionsに無いIDをgetOptionValueで渡した場合は、どちらもエラーを出しません。コンソールが静かでも症状が出ているなら、この2つを疑ってください。
複数選択で、選んだ項目にチェックボックスを出せますか?
出せます。renderOption={(props, option, { selected }) => ...}のように第3引数から選択状態を取り出し、Checkboxのcheckedに渡します。keyをpropsから分離する処理は本文の複数選択の例と同じです。選択のたびにリストが閉じないようdisableCloseOnSelectも添えます。別参照のオブジェクトをvalueに持っているのに比較関数もgetOptionValueも渡していないと、selectedが常にfalseになり、チェックが一切入りません。
Material UIにv8が無いのはなぜですか?v10は公開されていますか?
v8は、MUI Xとメジャーバージョンを揃えるために欠番にしたとv9の公式アナウンスが説明しています(「there is no Material UI v8, like there is no v2」)。npmの正式版は5系・6系・7系・9系で、8系は1つもありません。次の10.0.0は、2026年10月12日にプレリリースのalpha.0がnextタグで公開されました。Autocomplete関連では、候補リストのPaperの既定elevationを1から8へ変える破壊的変更が入っています。安定版はlatestタグの9.5.0です。