この記事で何を解説するのか

入力フォーム(Input)コンポーネントのコードを、1行ずつ分解して解説します。

このコードには、ボタンコンポーネントにはなかった新しい概念が登場します。

  • forwardRefとは何か
  • refとは何か
  • なぜこれらが必要なのか
  • 条件付きレンダリング

読み終わる頃には、このコードがなぜこう書かれているのか、すべて理解できます。

前提知識: refとは何か

コードを読む前に、refという概念を理解する必要があります。

refの基本

refは、DOM要素に直接アクセスするための仕組みです。

DOM要素とは:

  • <input><button><div>などのHTML要素
  • 実際にブラウザに表示される要素

なぜrefが必要なのか

通常、Reactは「データが変わったら画面を更新する」という仕組みです。

しかし、時々DOM要素を直接操作したい場面があります。

例1: 入力欄にフォーカスを当てる

// ページを開いた瞬間に、入力欄にカーソルを表示したい
<input />  // ← このinput要素に直接アクセスしたい

例2: 入力欄の値を直接取得する

// フォーム送信時に、入力欄の現在の値を取得したい
const value = inputElement.value  // ← 直接取得

refの使い方(基本)

import { useRef } from 'react'

function MyComponent() {
  // refを作る
  const inputRef = useRef<HTMLInputElement>(null)
  
  const focusInput = () => {
    // refを使ってDOM要素にアクセス
    inputRef.current?.focus()
  }
  
  return (
    <>
      <input ref={inputRef} />
      <button onClick={focusInput}>フォーカス</button>
    </>
  )
}

この知識を前提に、コードを読んでいきます


コード全体

import { InputHTMLAttributes, forwardRef } from 'react'

interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
  label?: string
  error?: string
}

export const Input = forwardRef<HTMLInputElement, InputProps>(
  ({ label, error, className = '', ...props }, ref) => {
    return (
      <div className="w-full">
        {label && (
          <label className="block text-sm font-medium text-gray-700 mb-1">
            {label}
          </label>
        )}
        <input
          ref={ref}
          className={`w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 ${
            error ? 'border-red-500' : 'border-gray-300'
          } ${className}`}
          {...props}
        />
        {error && (
          <p className="mt-1 text-sm text-red-500">{error}</p>
        )}
      </div>
    )
  }
)

Input.displayName = 'Input'

1行目: import文

import { InputHTMLAttributes, forwardRef } from 'react'

InputHTMLAttributes

HTMLの<input>タグが持つすべての属性の型定義です。

具体的には:

  • type: 入力欄のタイプ("text", "email", "password"など)
  • placeholder: プレースホルダー(薄い文字の説明)
  • value: 入力欄の値
  • onChange: 値が変わった時の処理
  • disabled: 無効化フラグ
  • その他100以上の属性

ボタンの時と同じで、通常のHTMLのinputに書けることすべてが含まれています。

forwardRef

これが新しい概念です。

forwardRefは、親コンポーネントからrefを受け取れるようにするための特別な関数です。

問題: 通常のコンポーネントではrefが使えない

// これは動かない
export function Input(props) {
  return <input {...props} />
}

// 親コンポーネントで使おうとすると...
function Parent() {
  const inputRef = useRef(null)
  return <Input ref={inputRef} />  // ❌ エラー!
}

解決: forwardRefを使う

// forwardRefで包むと、refが使えるようになる
export const Input = forwardRef((props, ref) => {
  return <input ref={ref} {...props} />
})

// 親コンポーネントで使える
function Parent() {
  const inputRef = useRef(null)
  return <Input ref={inputRef} />  // ✅ OK!
}

3-6行目: InputPropsの型定義

interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
  label?: string
  error?: string
}

全体の意味

「Inputコンポーネントが受け取れるデータの設計図」です。

3行目: interface InputProps extends InputHTMLAttributes<HTMLInputElement>

ボタンの時と同じパターンです。

  • InputHTMLAttributes<HTMLInputElement>: 通常のinput要素が持つすべての属性
  • extends: これを引き継ぐ
  • さらに独自の属性(label, error)を追加

つまり:

// HTMLのinputが持つ属性 + 独自の属性
<Input
  type="email"           // ← InputHTMLAttributesから
  placeholder="入力"     // ← InputHTMLAttributesから
  label="メールアドレス"  // ← 独自に追加
  error="必須項目です"    // ← 独自に追加
/>

4-5行目: 独自の属性

label?: string
error?: string

label?: string

入力欄の上に表示するラベル(説明文)です。

  • ?: 省略可能
  • string: 文字列のみ
<Input label="名前" />
// 表示:
// 名前
// [入力欄]

<Input />
// ラベルなし
// [入力欄]

error?: string

エラーメッセージです。入力値が間違っている時に表示します。

<Input error="必須項目です" />
// 表示:
// [入力欄] ← 赤い枠
// 必須項目です ← 赤い文字

<Input />
// エラーなし
// [入力欄] ← 通常の枠

8-10行目: forwardRefの使い方

export const Input = forwardRef<HTMLInputElement, InputProps>(
  ({ label, error, className = '', ...props }, ref) => {

8行目: export const Input = forwardRef<HTMLInputElement, InputProps>(

export const Input =

Inputという名前のコンポーネントを作り、外部から使えるようにします。

forwardRef<HTMLInputElement, InputProps>(

forwardRefの型を指定しています。

forwardRef<参照先のDOM要素の型, propsの型>

具体的には:

  • HTMLInputElement: refが参照する要素は<input>タグ
  • InputProps: propsの型はInputProps

図で表すと:

親コンポーネント
  ↓ ref={inputRef} を渡す
Input コンポーネント
  ↓ refを受け取る
<input> 要素 ← refが参照する先

9行目: 引数の受け取り方

({ label, error, className = '', ...props }, ref) => {

通常のコンポーネントとの違い

通常のコンポーネント:

function Button({ variant, children }: ButtonProps) {
  // 引数は1つだけ(props)
}

forwardRefを使ったコンポーネント:

forwardRef(({ label, error, ...props }, ref) => {
  // 引数が2つ
  // 第1引数: props
  // 第2引数: ref
})

第1引数: props

{ label, error, className = '', ...props }
  • label: ラベルの文字列
  • error: エラーメッセージの文字列
  • className = '': カスタムスタイル(デフォルトは空文字)
  • ...props: 残りのprops(type, placeholder, onChangeなど)

第2引数: ref

ref

親コンポーネントから渡されたrefです。

使用例:

// 親コンポーネント
function Parent() {
  const inputRef = useRef<HTMLInputElement>(null)
  
  return (
    <Input 
      ref={inputRef}  // ← これが第2引数のrefに入る
      label="名前"     // ← これが第1引数のpropsに入る
    />
  )
}

11-27行目: JSXの返却

return (
  <div className="w-full">
    {label && (
      <label className="block text-sm font-medium text-gray-700 mb-1">
        {label}
      </label>
    )}
    <input
      ref={ref}
      className={`w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 ${
        error ? 'border-red-500' : 'border-gray-300'
      } ${className}`}
      {...props}
    />
    {error && (
      <p className="mt-1 text-sm text-red-500">{error}</p>
    )}
  </div>
)

全体の構造

<div>
  ラベル(あれば表示)
  入力欄エラーメッセージ(あれば表示)
</div>

13-17行目: 条件付きレンダリング(ラベル)

{label && (
  <label className="block text-sm font-medium text-gray-700 mb-1">
    {label}
  </label>
)}

{label && ( ... )}の意味

条件付きレンダリングと呼ばれる書き方です。

{条件 && (表示する内容)}
  • 条件がtrue → 内容を表示
  • 条件がfalse → 何も表示しない

具体例:

// labelがある場合
label = "名前"
→ label && ( ... ) は true && ( ... ) になる
→ ラベルが表示される

// labelがない場合
label = undefined
→ label && ( ... ) は false && ( ... ) になる
→ 何も表示されない

JavaScriptの真偽値の扱い

// trueとみなされる値
"文字列"  → true
123      → true
true     → true

// falseとみなされる値
undefined → false
null      → false
""        → false (空文字)
0         → false
false     → false

だから、labelに文字列が入っていれば表示され、undefinedなら表示されません。

18-24行目: input要素

<input
  ref={ref}
  className={`w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 ${
    error ? 'border-red-500' : 'border-gray-300'
  } ${className}`}
  {...props}
/>

19行目: ref={ref}

親コンポーネントから受け取ったrefを、実際の<input>要素に渡します。

これが重要です。

// 親コンポーネント
function Parent() {
  const inputRef = useRef<HTMLInputElement>(null)
  
  const focusInput = () => {
    inputRef.current?.focus()  // ← ここで直接input要素にアクセス
  }
  
  return (
    <>
      <Input ref={inputRef} />  // ← refを渡す
      <button onClick={focusInput}>フォーカス</button>
    </>
  )
}

ref={ref}がないと、親コンポーネントからinput要素にアクセスできません。

20-22行目: classNameの動的生成

className={`w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 ${
  error ? 'border-red-500' : 'border-gray-300'
} ${className}`}

3つの部分が結合されています:

1. 基本スタイル
w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500

すべての入力欄に共通のスタイルです。

2. エラー時の条件分岐
${error ? 'border-red-500' : 'border-gray-300'}

三項演算子:

条件 ? 真の時 : 偽の時
  • errorがある → border-red-500(赤い枠)
  • errorがない → border-gray-300(グレーの枠)

具体例:

// エラーあり
<Input error="必須項目です" />
→ className="... border-red-500 ..."

// エラーなし
<Input />
→ className="... border-gray-300 ..."
3. 外部から渡されたカスタムスタイル
${className}

呼び出し側が追加で指定したスタイルです。

<Input className="mt-4" />
→ className="... border-gray-300 mt-4"

23行目: {...props}

残りのpropsをすべてinput要素に渡します。

<Input 
  type="email"
  placeholder="example@email.com"
  value={email}
  onChange={(e) => setEmail(e.target.value)}
/>

// これらはすべて{...props}で渡される

25-27行目: 条件付きレンダリング(エラーメッセージ)

{error && (
  <p className="mt-1 text-sm text-red-500">{error}</p>
)}

ラベルの時と同じパターンです。

  • errorがある → エラーメッセージを赤い文字で表示
  • errorがない → 何も表示しない

具体例:

// エラーあり
<Input error="必須項目です" />
// 表示:
// [入力欄] ← 赤い枠
// 必須項目です ← 赤い文字

// エラーなし
<Input />
// 表示:
// [入力欄] ← 通常の枠

31行目: displayName

Input.displayName = 'Input'

これは何のためか

React Developer Tools(ブラウザの開発ツール)で、コンポーネント名を表示するためです。

なぜ必要なのか

forwardRefを使うと、コンポーネント名が表示されなくなります。

displayNameがない場合:

<ForwardRef>  ← 何のコンポーネントか分からない
  <div>
    <input>

displayNameがある場合:

<Input>  ← 分かりやすい
  <div>
    <input>

必須ではない

技術的には省略できますが、デバッグしやすくするために書くことが推奨されます。


このコンポーネントの使い方

基本的な使い方

<Input />

結果:

  • 入力欄だけが表示される
  • ラベルなし、エラーなし

ラベル付き

<Input label="名前" />

結果:

名前
[入力欄]

エラー表示

<Input 
  label="メールアドレス" 
  error="必須項目です" 
/>

結果:

メールアドレス
[入力欄] ← 赤い枠必須項目です ← 赤い文字

HTML属性を渡す

<Input 
  label="メールアドレス"
  type="email"
  placeholder="example@email.com"
/>

結果:

メールアドレス
[example@email.com] ← プレースホルダー

refを使う(親コンポーネント側)

import { useRef } from 'react'

function MyForm() {
  const nameInputRef = useRef<HTMLInputElement>(null)
  
  const focusName = () => {
    nameInputRef.current?.focus()
  }
  
  const getNameValue = () => {
    const value = nameInputRef.current?.value
    alert(`入力値: ${value}`)
  }
  
  return (
    <>
      <Input 
        ref={nameInputRef}  // ← refを渡す
        label="名前" 
      />
      <button onClick={focusName}>名前欄にフォーカス</button>
      <button onClick={getNameValue}>値を取得</button>
    </>
  )
}

フォームバリデーション例

import { useState } from 'react'

function LoginForm() {
  const [email, setEmail] = useState('')
  const [emailError, setEmailError] = useState('')
  
  const validateEmail = (value: string) => {
    if (value === '') {
      setEmailError('メールアドレスを入力してください')
    } else if (!value.includes('@')) {
      setEmailError('正しいメールアドレスを入力してください')
    } else {
      setEmailError('')
    }
  }
  
  return (
    <Input
      label="メールアドレス"
      type="email"
      value={email}
      onChange={(e) => {
        setEmail(e.target.value)
        validateEmail(e.target.value)
      }}
      error={emailError}  // ← エラーメッセージを表示
    />
  )
}

ボタンとの違い

ボタンコンポーネント

export function Button({ ... }: ButtonProps) {
  // 通常の関数コンポーネント
}
  • forwardRefなし
  • refを受け取る必要がない(ボタンをプログラムで操作することは少ない)

入力コンポーネント

export const Input = forwardRef<HTMLInputElement, InputProps>(
  ({ ... }, ref) => {
    // forwardRefで包む
  }
)
  • forwardRefあり
  • refを受け取る必要がある(入力欄はプログラムで操作することが多い)

なぜ入力欄はrefが必要なのか

よくある操作:

  • フォーカスを当てる
  • 値を取得する
  • 値をクリアする
  • バリデーションのために直接アクセス
// こういう操作をしたい
inputRef.current?.focus()
inputRef.current?.value
inputRef.current?.select()

ボタンではこういう操作は少ない


よくある疑問

疑問1: なぜforwardRefが必要なのか

通常のコンポーネントは、refを自動的に受け取れません。

// これは動かない
export function Input(props) {
  return <input {...props} />
}

<Input ref={inputRef} />  // ❌ エラー

forwardRefを使うことで、refを受け取れるようになります。

疑問2: refはいつ使うのか

DOM要素を直接操作したい時:

  • フォーカスを当てる
  • スクロール位置を変える
  • アニメーションライブラリと連携
  • サイズを測定する

通常のReactの仕組みで十分な時はrefは不要

疑問3: ?(オプショナル)とデフォルト値の違い

interface InputProps {
  label?: string          // ← ?付き(オプショナル)
  error?: string
}

function Input({ 
  className = '',         // ← デフォルト値
  ...props 
}, ref) {

?(オプショナル):

  • 型定義で使う
  • 「渡さなくてもOK」という意味
  • 値がundefinedになる可能性がある

デフォルト値(=):

  • 関数の引数で使う
  • 「渡されなかった時の初期値」
  • undefinedを防ぐ
// labelは?付き → 渡さなくてもOK
<Input />  // label = undefined

// classNameはデフォルト値 → 渡さなければ''になる
className = ''  // undefinedにならない

疑問4: &&と三項演算子の使い分け

&&を使う場合:

  • 「表示するか、しないか」の2択
{label && <label>{label}</label>}
// labelがあれば表示、なければ何も表示しない

三項演算子を使う場合:

  • 「AまたはB」の2択
{error ? 'border-red-500' : 'border-gray-300'}
// errorがあれば赤、なければグレー

まとめ

このInputコンポーネントは、以下の技術を組み合わせています。

  1. forwardRef: 親コンポーネントからrefを受け取る
  2. ref: DOM要素に直接アクセスする仕組み
  3. 条件付きレンダリング: &&で表示/非表示を切り替え
  4. 三項演算子: 条件によってスタイルを切り替え
  5. 型の継承: extends InputHTMLAttributesで既存の型を引き継ぐ

ボタンコンポーネントとの最大の違いは、forwardRefを使っていることです。

これにより、親コンポーネントからinput要素に直接アクセスできるようになります。


参考資料