Reactの入力フォームコンポーネントを1行ずつ解説する
この記事で何を解説するのか
入力フォーム(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コンポーネントは、以下の技術を組み合わせています。
- forwardRef: 親コンポーネントからrefを受け取る
- ref: DOM要素に直接アクセスする仕組み
- 条件付きレンダリング:
&&で表示/非表示を切り替え - 三項演算子: 条件によってスタイルを切り替え
- 型の継承:
extends InputHTMLAttributesで既存の型を引き継ぐ
ボタンコンポーネントとの最大の違いは、forwardRefを使っていることです。
これにより、親コンポーネントからinput要素に直接アクセスできるようになります。