Closed7

React-Hooks-Formを使って、hiddenにしたinputからファイル(画像)をアップロードしたい

CaaaaatsCaaaaats

タイトルの通り、RHFを使ってhiddenしたinput要素からfileをアップロードできるようにしたい。

前提

  • 上記の技術スタック
  • useRefを使ってhiddenになったinput要素を紐付けた要素からクリックできるhiddenFileInputをrefとして扱っている
  • RHFを使うためにrefでregisterしている
  • button要素のtype属性をsubmit、もしくは指定なし(記述なし)にするとform要素のonSubmitイベントが発火されて想定と違う挙動が起こるので type='button'にしてる。

エラー/現象

useRefで紐付けたinputをbuttonからクリックしてもファイル選択画面が開かない

うまくいっていること/わかったこと

  • console.log('Clicked!')でhandleFileClickクリックイベント自体は発火できていることを確認
  • console.log(hiddenFileInput.current?);の出力がnullなのでhiddenFileInputが参照できていない
  • おそらく ref={{ register } && { hiddenFileInput }} の指定方法が怪しい

const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm({
    mode: 'onChange',
    criteriaMode: 'all',
    shouldFocusError: false,
  });

  const hiddenFileInput = useRef<HTMLInputElement>(null);

  const handleFileClick = () => {
    hiddenFileInput.current?.click();
    console.log(hiddenFileInput.current);
    console.log('Clicked!');
  };

return(

<form onSubmit={handleSubmit(handleUpdateComplete)}>
    <input className='hidden' type='file' name='files' ref={{ register } && { hiddenFileInput }} accept='image/*' />
  <button onClick={handleFileClick} type='button'>
    変更する
  </button>
</form>

)

変更
<input className='hidden' type='file' name='files' ref={hiddenFileInput} {...register} accept='image/*' />

CaaaaatsCaaaaats

わかったこと

  • registerにはrefもonChangeも含まれている(nameも)
  • 別で指定すると上書きされる
  • formのonSubmitから呼び出す関数を発火できない(input要素がregisterに登録されていないから)

対策 / nextアクション

このトレードオフ的なジレンマを乗り越えるために・・・

  1. hiddenされたinputの操作はuseRefを使わずにlabelのhtmlforidの関係性で操作するよう実装
  2. onChangeを使わずにuseFormから即座にstate更新を反映するやり方を調べる
CaaaaatsCaaaaats

1. hiddenされたinputの操作はuseRefを使わずにlabelのhtmlforとidの関係性で操作するよう実装

<form onSubmit={handleSubmit(handleUpdateComplete)}>
  <input
    className='hidden'
    type='file'
    id='avatar-image'  //追加
    // name='photoURL' //削除
    {...register('photoURL')}
    // onChange={handleFileChange} //削除
    // ref={hiddenFileInput} //削除
    accept='image/*'
  />
  <input type='submit' className='hidden' ref={hiddenTextInput} />
  <label
    className='absolute top-[140px] left-[390px] text-sm mb-4 text-gray-400 hover:text-gray-500 rounded cursor-pointer focus:outline-none focus:ring-penn-green focus:ring-2 focus:ring-opacity-50'
    // onClick={handleFileClick} //削除
    htmlFor='avatar-image' //追加
  >
    変更する
  </label>
</form>

ちなみにref={register}はRHFのv7で上記のようにスプレッド使うようになった

CaaaaatsCaaaaats

例えば以下のようにonChangeを...registerの先に持ってくるとエラーになる

<input
・・
onChange={handleFileChange}
{...register('photoURL')}
・・
/>

エラー

'onChange' が複数回指定されているため、ここでの使用は上書きされます。ts(2783)
UserProfile.tsx(96, 19): このスプレッドは、常にこのプロパティを上書きします
CaaaaatsCaaaaats

2. onChangeを使わずにuseFormから即座にstate更新を反映するやり方を調べる
→自分の作ったonChangeをRHFの書式に合わせて使う

検索「react hook form onchange not working」
> How to can I use onChange on React Hook Form Version 7.0
https://stackoverflow.com/questions/66936135/react-hook-form-how-to-can-i-use-onchange-on-react-hook-form-version-7-0

In register documentation https://react-hook-form.com/api/useform/register, sample exists on Custom onChange, onBlur section :

サンプル

// onChange got overwrite by register method
<input onChange={handleChange} {...register('test')} />

// register's onChange got overwrite by register method
<input {...register('test')} onChange={handleChange}/>

const firstName = register('firstName', { required: true })
<input 
  onChange={(e) => {
    firstName.onChange(e); // method from hook form register
    handleChange(e); // your method
  }}
  onBlur={firstName.onBlur}
  ref={firstName.ref} 
/>

実装してみる

<input
  className='hidden'
  type='file'
  id='avatar-image'
  // name='photoURL'
  // ref={register}
  {...register('photoURL')}
  onChange={(e) => {
    register('photoURL').onChange(e)
    handleFileChange(e);
  }}
  // ref={hiddenFileInput}
  accept='image/*'
/>

できた!
ちゃんとドキュメント読んでおこう。。。。。
https://react-hook-form.com/api/useform/register/

CaaaaatsCaaaaats

これでrefも色々カスタマイズできるのかな

// not working, because ref is not assigned
<TextInput {...register('test')} />

const firstName = register('firstName', { required: true })
<TextInput 
  onChange={firstName.onChange}
  onBlur={firstName.onBlur}
  inputRef={firstName.ref} // you can achieve the same for different ref name such as innerRef
/>

// correct way to forward input's ref
const Select = React.forwardRef(({ onChange, onBlur, name, label }, ref) => (
  <select name={name} ref={ref} onChange={onChange} onBlur={onBlur}>
    <option value="20">20</option>
    <option value="30">30</option>
  </select>
));

このスクラップは2021/11/25にクローズされました