React入力フォームのフォーカス制御:useRef・forwardRefを使う

スポンサーリンク
// 画面表示時に自動フォーカス
const nameRef = useRef<HTMLInputElement>(null)
useEffect(() => { nameRef.current?.focus() }, [])

// Enter で次フィールドへ移動
const moveFocus = (e: React.KeyboardEvent<HTMLInputElement>, next: React.RefObject<HTMLInputElement | HTMLButtonElement | null>) => {
  if (e.key === 'Enter') { e.preventDefault(); next.current?.focus() }
}

// 子コンポーネントに ref を渡す
const InputField = forwardRef<HTMLInputElement, Props>(({ label, onChange }, ref) => (
  <input ref={ref} onChange={onChange} />
))

Reactでフォームを作るとき、こんな操作を実現したい場面があると思います。

  • 画面表示と同時に最初の入力欄にフォーカスが当たる
  • Enterキーで次の入力欄に移動できる
  • 送信後にフォームがクリアされ、先頭の入力欄にフォーカスが戻る

キーボードだけで操作できるフォームはUXとして優しいですね。useRefuseEffectforwardRef を使って実装する方法を解説します。

完成形はこちらです。

React input フォーカス制御デモ


構成

2つのコンポーネントで構成します。

  • App.tsx:フォームのロジックとrefを管理する親コンポーネント
  • InputField.tsxforwardRef で ref を受け取る子コンポーネント

InputField.tsx:forwardRefで ref を受け取る

まず子コンポーネントから作ります。子コンポーネントの <input> に外から ref を渡したいときは forwardRef を使います。

import { forwardRef } from 'react'

type Props = {
  label: string
  value: string
  placeholder?: string
  onChange: (e: React.ChangeEvent<HTMLInputElement>) => void
  onKeyDown?: (e: React.KeyboardEvent<HTMLInputElement>) => void
}

const InputField = forwardRef<HTMLInputElement, Props>(
  ({ label, value, placeholder, onChange, onKeyDown }, ref) => (
    <div className="field">
      <label>{label}</label>
      <input
        ref={ref}
        value={value}
        placeholder={placeholder}
        onChange={onChange}
        onKeyDown={onKeyDown}
      />
    </div>
  )
)

InputField.displayName = 'InputField'

export default InputField

forwardRef<HTMLInputElement, Props> の第1型引数が ref の型、第2引数が Props の型です。displayName を設定しておくと React DevTools で名前が表示されます。


App.tsx:フォーカス制御のロジック

useRef で DOM への参照を持つ

const nameRef = useRef<HTMLInputElement>(null)
const emailRef = useRef<HTMLInputElement>(null)
const submitRef = useRef<HTMLButtonElement>(null)

useRef<HTMLInputElement>(null) で入力欄への参照を作ります。ref.current から DOM 要素に直接アクセスできます。

useEffect で初期フォーカス

useEffect(() => {
  nameRef.current?.focus()
}, [])

依存配列を [] にすることで初回レンダリング時だけ実行されます。?. はオプショナルチェーニングで current が null のときは何もしません。

Enter で次フィールドへ移動

const moveFocus = (
  e: React.KeyboardEvent<HTMLInputElement>,
  next: React.RefObject<HTMLInputElement | HTMLButtonElement | null>
) => {
  if (e.key === 'Enter') {
    e.preventDefault()
    next.current?.focus()
  }
}

e.preventDefault() でブラウザのデフォルト動作(フォーム送信)を止めてから、次の要素にフォーカスを移します。移動先が input でも button でも同じ関数で動くよう、union 型で受け取っています。

送信後にフォーカスをリセット

const handleSubmit = (e: React.FormEvent) => {
  e.preventDefault()
  setName('')
  setEmail('')
  nameRef.current?.focus()
}

setState でフォームをクリアした後、nameRef.current?.focus() で先頭の入力欄に戻します。

JSX で ref と onKeyDown を渡す

<InputField
  ref={nameRef}
  label="名前"
  value={name}
  placeholder="山田 太郎"
  onChange={(e) => setName(e.target.value)}
  onKeyDown={(e) => moveFocus(e, emailRef)}
/>
<InputField
  ref={emailRef}
  label="メールアドレス"
  value={email}
  placeholder="example@email.com"
  onChange={(e) => setEmail(e.target.value)}
  onKeyDown={(e) => moveFocus(e, submitRef)}
/>
<button ref={submitRef} type="submit">
  送信
</button>

onKeyDown に次の移動先 ref を渡します。名前フィールドは emailRef、メールフィールドは submitRef へ。


App.tsx 全体

import { useEffect, useRef, useState } from 'react'
import InputField from './InputField'

function App() {
  const [name, setName] = useState('')
  const [email, setEmail] = useState('')

  const nameRef = useRef<HTMLInputElement>(null)
  const emailRef = useRef<HTMLInputElement>(null)
  const submitRef = useRef<HTMLButtonElement>(null)

  useEffect(() => {
    nameRef.current?.focus()
  }, [])

  const moveFocus = (
    e: React.KeyboardEvent<HTMLInputElement>,
    next: React.RefObject<HTMLInputElement | HTMLButtonElement | null>
  ) => {
    if (e.key === 'Enter') {
      e.preventDefault()
      next.current?.focus()
    }
  }

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault()
    setName('')
    setEmail('')
    nameRef.current?.focus()
  }

  return (
    <div className="container">
      <h1>お問い合わせ</h1>
      <form onSubmit={handleSubmit}>
        <InputField
          ref={nameRef}
          label="名前"
          value={name}
          placeholder="山田 太郎"
          onChange={(e) => setName(e.target.value)}
          onKeyDown={(e) => moveFocus(e, emailRef)}
        />
        <InputField
          ref={emailRef}
          label="メールアドレス"
          value={email}
          placeholder="example@email.com"
          onChange={(e) => setEmail(e.target.value)}
          onKeyDown={(e) => moveFocus(e, submitRef)}
        />
        <button ref={submitRef} type="submit">
          送信
        </button>
      </form>
    </div>
  )
}

export default App

まとめ

やりたいこと 使うもの ポイント
画面表示時に自動フォーカス useRef + useEffect(fn, []) 依存配列を空にして初回のみ実行
Enterキーで次フィールドへ onKeyDown + e.preventDefault() デフォルトのフォーム送信を止めてから移動
送信後にフォーカスをリセット handleSubmit 内で .focus() setState の後に呼ぶ
子コンポーネントに ref を渡す forwardRef 第1型引数に ref の型、第2引数に Props の型