EY-Office ブログ

Autocompleteの初期値が設定されないです?

以前Reactの研修を受けて頂いた企業向けに、現在Reactプロダクトの開発支援を行っています。 そこで、出た問題が実はReactの深い問題に触れていたので、解説記事を書きます。

uncontrolled to controlled DALL·Eで生成したuncontrolled to controlledの画像

現象

問題は、以下のようなMUI(Material UI)のAutocompleteという文字入力要素(TextField, input)にサジェスチョン付きの選択肢を追加できるコンポーネントを使ったコードで起きます。起動1秒後に選択肢が設定されますが、その際に初期値2が表示されそうなのですが、表示されません。

  • ① 選択肢のStateです。型は文字列の配列で初期値は空配列です
  • ② アプリ起動1秒後に選択肢が設定されます(本物のコードではバックエンドと通信して選択肢を取得しています)
  • ③ Autocompleteに選択肢を設定
  • ④ Autocompleteの初期値は、選択が2に等しいものを設定(本物のコードでは前回設定した選択肢を設定しています)
import { useEffect, useState } from 'react';
import { Autocomplete, TextField } from '@mui/material';

const App = () =>  {
  const [list, setList] = useState<string[]>([]);   // ← ①

  useEffect(() => {
    setTimeout(() => setList(["1", "2", "3", "4"]), 1000);   // ← ②
  }, []);

  return (
    <Autocomplete
      options={list}                                       // ← ③
      value = {list.find(e => e === "2")}                  // ← ④
      renderInput={(params) => <TextField {...params} />}
    />
  )
}
export default App

そして、このアプリを起動1秒後にブラウザーのコンソールには以下のようなエラーが表示されます。

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.

最初の行をGoogle翻訳すると以下のようになります。何を言いたいのかわかりますか? 謎のメッセージですよね。😅

MUI: コンポーネントは、オートコンプリートの制御されていない値の状態を制御されるように変更しています。

controlled component vs uncontrolled component

上のメッセージを見て、controlled/uncontrolledは一般的な言葉ではなく、Reactのcontrolled component(制御されたコンポーネント) / uncontrolled component(非制御コンポーネン)の事だと気が付いた人は、ちゃんとReactを勉強した人ですね❗

改めて解説すると、controlled componentは下のコードのようにinputタグで入力された文字列を明示的にState管理する、Reactらしいコードです。

import { useState } from 'react'

const App = () =>  {
  const [text, SetText] = useState("");
  return (
    <form onSubmit={e => {
      console.log(`input = ${text}`);  e.preventDefault();}}>
      <input type="text" value={text} onChange={e => SetText(e.target.value)} />
      <input type="submit" />
    </form>
  )
}
export default App

一方、uncontrolled componentは入力処理はブラウザーのinputタグに任せ、useRefを使いブラウザーのDOMにアクセスし入力値を取得しています。

import { useRef } from 'react'

const App = () =>  {
  const inputRef = useRef<HTMLInputElement>(null);
  return (
    <form onSubmit={e => {
      console.log(`input = ${inputRef.current?.value}`);  e.preventDefault();}}>
      <input type="text" ref={inputRef} />
      <input type="submit" />
    </form>
  )
}

export default App

問題の解説

さて今回の問題ですが、React/JavaScriptの特性・仕様が組み合わさることで発生しています。

・ Autocompleteの特性

コンソールに表示されるエラーメッセージの1行目は、

uncontrolled componentであるAutocompleteコンポーネントのvalue属性を設定することで、controlled componentに変更しようとしています。

のような意味でしょうか、2行目は

uncontrolled componentからcontrolled componentに切り替えるべきではありません(その逆も)。

さらに、3,4行目には

Autocompleteコンポーネントの実行時にはcontrolledかuncontrolledを決めておかないといけません。 初期レンダリングの時にvalue属性がundefinedで無ければcontrolled componentになります

のように説明してくれています。😁

・ find()メソッドの仕様

配列のfind()メソッドは、条件に合う項目が無い場合はundefinedが戻ります!
今回の問題コードでは、初期描画時にはlistは空なので、find()メソッドは確実にundefinedが戻します。

$ node
> ["1", "2", "3", "4"].find(e => e === "2")
'2'
> [].find(e => e === "2")
undefined
>

ということで、今回の問題は以下の2つが組み合わさって起きたのです。

  1. Autocompleteのvalue=属性の初期値がundefinedの場合、uncontrolled componentになり、以後value=属性が設定されても値は設定されない
  2. list.find(e => e === "2") 条件に合う項目が無い場合はundefinedが戻る

解決案

解決案はいくつかあると思いますが、以下のコードでは

④のところで、find()メソッドがundefinedを戻した場合はnullをvalue属性に設定し、Autocompleteをcontrolled componentに設定しています。これでvalueの設定が有効になります。

import { useEffect, useState } from 'react';
import { Autocomplete, TextField } from '@mui/material';

const App = () =>  {
  const [list, setList] = useState<string[]>([]);

  useEffect(() => {
    setTimeout(() => setList(["1", "2", "3", "4"]), 1000);
  }, []);

  return (
    <Autocomplete
      options={list}
      value = {list.find(e => e === "2") ?? null}             // ← ④
      renderInput={(params) => <TextField {...params} />}
    />
  )
}
export default App

まとめ

今回のバグは、なかなか手強いバグでした。しかしコンソールには問題点や解決方法が表示されていました。やはりエラーメッセージはちゃんと読まないといけませんね。英語ではテクニカルタームに一般的な単語を使う事が多いので、今回のようにGoogle翻訳やDeeplでは上手く翻訳できない事もあります。

また、エラーメッセージでGoogle検索してきた日本語の情報も説明不足のものが多く、やはりStack Overflowや英語のブログ記事には良い記事もありました(でも読むのが面倒ですね😅)。

EY-OfficeではReactの入門教育だけではなく、開発支援も行っています。はじめてReactを実プロジェクトに導入するさいには色々な問題が発生します、多くは初心者には解決に時間がかかります。そのような際にはEY-Officeのお問い合わせください。

- about -

EY-Office代表取締役
・プログラマー
吉田裕美の
開発者向けブログ