React Hook Formで画像アップロード機能を作るとエラーで怒られたので忘備録


この記事について

React Hook Form で画像アップロード機能を実装しました。
実装している際に、エラーが出てその解決に時間を溶かしてしまったのでその過ちを繰り返さないために残しておきます。

InvalidStateError: Failed to set the 'value' property on 'HTMLInputElement': This input element accepts a filename, which may only be programmatically set to the empty string.

環境

  • フレームワーク: Next.js 11.0.1
  • CSS in JS: ChakraUI 1.6.5
  • フォーム周り: react-hook-form 7.10.1

やりたいこと

<input type="file />を使用し、画像を選択するとプレビュー欄に表示される。

NGコード

react-hook-formのrefなどをregister()で渡していて、画像を選択したらsetValue()で選択した画像パスを格納し、それをプレビューとして表示させるイメージです。

const onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
  const { files } = event.target;
  setValue('preview_url', URL.createObjectURL(files[0])); // ここでエラーが出た
};

const fileUploadExample = () => {
  return (
    <>
      <Avatar
        size="m"
        src={watch('preview_url')}
      />
      <Input
        {...ragister('preview_url')}
        type="file"
        accept="image/*"
        onChange={onFileInputChange}
      />
    </>
  );
};

動いたコード

react-hook-formのrefは渡さずに、useRef()を渡すと動きました。

const inputRef = useRef<HTMLInputElement>(null);
const onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
  const { files } = event.target;
  setValue('preview_url', URL.createObjectURL(files[0])); // 動いた!!
};

const { name: previewUrl } = register('preview_url');

const fileUploadExample = () => {
  return(
    <>
      <Avatar
        size="m"
        src={watch('preview_url')}
      />
       <Input
          id={previewUrl}
          ref={inputRef}
          name={previewUrl}
          type="file"
          accept="image/*"
          onChange={onFileInputChange}
        />
    </>
  );
};

原因

<input type="file" />にはvalueが設定できないにもかかわらず、valueをセットしようとしていることが原因でした。

Reactドキュメントにもちゃんと書かれていました。

Reactでは、値はユーザーのみが設定でき、プログラムでは設定できないため、常に制御されていないコンポーネントとなります。
https://reactjs.org/docs/uncontrolled-components.html#the-file-input-tag

react-hook-formのrefregister()を使ってDOMにセットし、画像選択時にsetValue()を行うと、<input type="file">のDOMにvalueがセットされるためエラーが出ていたようです。

useRef()を使用した場合だと、setValue()時にそのDOMにvalueをセットしない為うまくいっているようでした。

まとめ

分かってしまえばそりゃそうなるよなという感じですが、ふんわりとした認識でsetValue()refを使っていたためエラー解決に時間を溶かしてしまい辛い気持ちになりました...。改めてドキュメントを読むことの大切さを実感。
ref周りは難しいので(特にforwardRefとか)もう少し時間をかけて学んでいきたいところ。
基礎は大事!!!!

おまけ

setValue()の実装コードは以下のようになっていました。
難しくてよくわかりませんでしたが、きっとこの中でref.current.value = valueをしているんだと思う。

const setValue = (name, value, options = {}) => {
        const field = get(fieldsRef.current, name);
        const isFieldArray = namesRef.current.array.has(name);
        if (isFieldArray) {
            subjectsRef.current.array.next({
                values: value,
                name,
                isReset: true,
            });
            if ((readFormStateRef.current.isDirty ||
                readFormStateRef.current.dirtyFields) &&
                options.shouldDirty) {
                set(formStateRef.current.dirtyFields, name, setFieldArrayDirtyFields(value, get(defaultValuesRef.current, name, []), get(formStateRef.current.dirtyFields, name, [])));
                subjectsRef.current.state.next({
                    name,
                    dirtyFields: formStateRef.current.dirtyFields,
                    isDirty: getIsDirty(name, value),
                });
            }
            !value.length &&
                set(fieldsRef.current, name, []) &&
                setValue(fieldArrayDefaultValuesRef.current, name, []);
        }
        ((field && !field._f) || isFieldArray) && !isNullOrUndefined(value)
            ? setInternalValues(name, value, isFieldArray ? {} : options)
            : setFieldValue(name, value, options, true, !field);
        isFieldWatched(name) && subjectsRef.current.state.next({});
        subjectsRef.current.watch.next({ name, values: getValues() });
    };

参考

https://developer.mozilla.org/ja/docs/Web/HTML/Element/input/file
https://github.com/react-hook-form/react-hook-form/issues/4765
https://stackoverflow.com/questions/1696877/how-to-set-a-value-to-a-file-input-in-html/1696884#1696884
https://reactjs.org/docs/uncontrolled-components.html#the-file-input-tag