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()`で選択した画像パスを格納し、それをプレビューとして表示させるイメージです。
```jsx
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()`を渡すと動きました。
```jsx
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では、<input type="file" />値はユーザーのみが設定でき、プログラムでは設定できないため、常に制御されていないコンポーネントとなります。
https://reactjs.org/docs/uncontrolled-components.html#the-file-input-tag

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

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


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

## おまけ
`setValue()`の実装コードは以下のようになっていました。
難しくてよくわかりませんでしたが、きっとこの中で`ref.current.value = value`をしているんだと思う。
```js
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() });
    };
```

## 参考
- [<input type="file"> HTML: HyperText Markup Language | MDN](https://developer.mozilla.org/ja/docs/Web/HTML/Element/input/file)
- [[V7] FileInput in <Controller /> component throws error about setting the value of a HTMLInputElement](https://github.com/react-hook-form/react-hook-form/issues/4765)
- [How to set a value to a file input in HTML?](https://stackoverflow.com/questions/1696877/how-to-set-a-value-to-a-file-input-in-html/1696884#1696884)
- [React Uncontrolled Components](https://reactjs.org/docs/uncontrolled-components.html#the-file-input-tag)