コンポーネント定義
Reactアプリケーションの基本的な構成要素です。
関数コンポーネント (Functional Component)
シンプルで、フック (Hooks) を使用してステートやライフサイクル機能を利用できます。
import React, { useState, useEffect } from 'react';
// 最もシンプルな形式
function Welcome(props) { return <h1>Hello, {props.name}</h1>;
}
// アロー関数形式 (よく使われる)
const MyComponent = ({ title, children }) => { const [count, setCount] = useState(0); useEffect(() => { document.title = `Count: ${count}`; // クリーンアップ関数 return () => { document.title = 'React App'; }; }, [count]); // count が変化した時のみ実行 const handleClick = () => { setCount(prevCount => prevCount + 1); }; return ( <div> <h2>{title}</h2> <p>現在のカウント: {count}</p> <button className="button is-primary" onClick={handleClick}> Increment </button> <div className="content">{children}</div> </div> );
};
export default MyComponent;
JSX (JavaScript XML)
JavaScript の中に XML ライクな構文を記述する拡張機能です。
基本構文
HTMLに似ていますが、JavaScriptの式を {}
で埋め込むことができます。
const name = 'Taro';
const element = <h1>こんにちは、{name}さん!</h1>; // Hello, Taroさん!
function formatName(user) { return user.firstName + ' ' + user.lastName;
}
const user = { firstName: 'Hanako', lastName: 'Yamada' };
const element2 = <h2>ようこそ、{formatName(user)}!</h2>; // ようこそ、Hanako Yamada!
属性
HTML属性に似ていますが、キャメルケース (camelCase
) を使用します (例: className
, onClick
)。
// class は className に
const element = <div className="container">コンテンツ</div>;
// for は htmlFor に
const label = <label htmlFor="inputId">ラベル</label>;
// style はオブジェクトで指定
const styles = { color: 'blue', fontSize: '16px' };
const styledElement = <p style={styles}>スタイル適用</p>;
// data-* 属性や aria-* 属性はそのまま使用可能
const dataElement = <div data-testid="my-div" aria-label="説明">Data属性</div>;
子要素
タグで囲まれたコンテンツが子要素になります。文字列、JSX要素、式の評価結果などを配置できます。
const element = ( <div> <h1>タイトル</h1> <p>これは段落です。</p> { /* コメントはこう書く */ } { 1 + 2 + 3 } {/* 式の結果 (6) が表示される */} <Welcome name="Jiro" /> {/* 他のコンポーネント */} </div>
);
条件付きレンダリング
特定の条件に基づいて要素を表示・非表示にします。
function Greeting({ isLoggedIn }) { // if文 if (isLoggedIn) { return <h2>おかえりなさい!</h2>; } return <h2>ログインしてください。</h2>;
}
function LoginStatus({ isLoggedIn }) { return ( <div> {/* 三項演算子 */} <p>ユーザーは現在 {isLoggedIn ? 'ログイン中' : 'ログアウト中'} です。</p> {/* 論理積演算子 (trueの場合のみ要素を表示) */} {isLoggedIn && <button className="button is-danger">ログアウト</button>} {!isLoggedIn && <button className="button is-success">ログイン</button>} </div> );
}
function Mailbox({ unreadMessages }) { const count = unreadMessages.length; return ( <div> <h3>受信トレイ</h3> {/* count > 0 が true の場合のみメッセージを表示 */} {count > 0 && <p> {count} 件の未読メッセージがあります。 </p> } {/* count === 0 の場合は何も表示しない (nullを返すのと同じ) */} {count === 0 && <p>未読メッセージはありません。</p>} </div> );
}
リストレンダリング
配列データを元に要素のリストを生成します。map()
メソッドをよく使用します。
const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map((number, index) => // key は必須!兄弟要素間で一意にする <li key={number.toString()}> {number * 2} </li>
);
function NumberList({ data }) { return ( <ul> {data.map((item) => ( <ListItem key={item.id} value={item.text} /> ))} </ul> );
}
function ListItem({ value }) { // key は親の map 内で指定するため、ここでは不要 return <li>{value}</li>;
}
const todos = [ { id: 'a', text: 'Learn React' }, { id: 'b', text: 'Build an App' }, { id: 'c', text: 'Deploy it' }
];
const TodoListComponent = () => ( <div> <h3>Todo List</h3> <ul> {todos.map(todo => ( <li key={todo.id}> {todo.text}</li> ))} </ul> </div>
);
フラグメント (`<React.Fragment>` or `<>`)
複数の要素をグループ化するためのラッパー要素ですが、実際のDOMには追加されません。
function Columns() { return ( // <React.Fragment> または短縮構文 <> を使用 // key が必要な場合は <React.Fragment key={item.id}> のように書く <> <td>データ1</td> <td>データ2</td> </> );
}
function Table() { return ( <table className="table is-bordered"> <tr> <Columns /> </tr> </tbody> </table> );
}
Props (プロパティ)
親コンポーネントから子コンポーネントへデータを渡すための仕組みです。Props は読み取り専用です。
渡し方
HTMLの属性のように記述します。
// 文字列リテラル
<Welcome name="Saburo" />
// JavaScript式 (変数、数値、真偽値、オブジェクト、配列、関数など)
const userName = "Shiro";
const userAge = 25;
const isAdmin = true;
const userProfile = { avatar: '/path/to/img.jpg' };
const skills = ['React', 'Node.js'];
const handleClick = () => console.log('Clicked!');
<UserProfile name={userName} age={userAge} isAdmin={isAdmin} profile={userProfile} skills={skills} onClickHandler={handleClick}
/>
// 真偽値 true は属性名のみでも渡せる
<MyButton disabled /> // disabled={true} と同じ
// スプレッド構文でオブジェクトのプロパティをまとめて渡す
const props = { firstName: 'Goro', lastName: 'Sato' };
<UserInfo {...props} />
// 上記は以下と同じ
// <UserInfo firstName="Goro" lastName="Sato" />
受け取り方
関数コンポーネント
引数で props
オブジェクトを受け取るか、分割代入 (destructuring) を使用します。
// props オブジェクト全体を受け取る
function Welcome(props) { return <h1>Hello, {props.name}</h1>;
}
// 分割代入で必要なプロパティを受け取る (推奨)
function UserCard({ name, age, isAdmin }) { return ( <div className="box"> <p>Name: {name}</p> <p>Age: {age}</p> {isAdmin && <span className="tag is-danger">Admin</span>} </div> );
}
propTypes
コンポーネントが受け取る props の型を検証するための仕組みです。開発モードでのみ機能します。prop-types
ライブラリが必要です。
import PropTypes from 'prop-types';
function MyComponent({ name, age, email, onUpdate, config, children, status }) { // ... component logic return ( <div> {/* ... */} </div> );
}
MyComponent.propTypes = { // 必須の文字列 name: PropTypes.string.isRequired, // 数値 age: PropTypes.number, // 特定の形式を持つ文字列 (正規表現) email: PropTypes.string.isRequired, // 必須の関数 onUpdate: PropTypes.func.isRequired, // 特定の形状を持つオブジェクト config: PropTypes.shape({ id: PropTypes.string.isRequired, enabled: PropTypes.bool }), // レンダリング可能なもの (数値、文字列、要素、配列、フラグメント) children: PropTypes.node, // 特定の値のいずれか (列挙型) status: PropTypes.oneOf(['pending', 'processing', 'completed']), // 特定の型の配列 items: PropTypes.arrayOf(PropTypes.number), // 特定の型のオブジェクトの配列 users: PropTypes.arrayOf(PropTypes.shape({ id: PropTypes.number.isRequired, name: PropTypes.string })), // 任意のデータ型 customProp: PropTypes.any
};
defaultProps
props が指定されなかった場合のデフォルト値を設定します。
function Button({ label, theme, size }) { return ( <button className={`button is-${theme} is-${size}`}> {label} </button> );
}
// デフォルト値を設定
Button.defaultProps = { label: 'Click Me', theme: 'primary', // is-primary size: 'normal' // is-normal (bulmaには通常サイズ用のクラスはないが例として)
};
// 使用例
// <Button /> -> label="Click Me", theme="primary", size="normal"
// <Button label="Submit" theme="link" /> -> label="Submit", theme="link", size="normal"
子要素 (`props.children`)
コンポーネントの開始タグと終了タグの間に記述された内容は、props.children
として渡されます。
// Card コンポーネント
function Card({ title, children }) { return ( <div className="card"> <header className="card-header"> <p className="card-header-title">{title}</p> </header> <div className="card-content"> <div className="content"> {children} {/* ここに子要素が入る */} </div> </div> </div> );
}
// Card コンポーネントの使用
function App() { return ( <Card title="User Info"> <p>ここにユーザー情報を表示します。</p> <ul> <li>Name: Taro</li> <li>Email: taro@example.com</li> </ul> <button className="button is-small">詳細</button> {/* 上記の p, ul, button が children として Card に渡される */} </Card> );
}
State (ステート)
コンポーネント内部で保持・管理されるデータです。State が更新されるとコンポーネントは再レンダリングされます。
useState
フック
関数コンポーネントでステートを管理するためのフックです。
import React, { useState } from 'react';
function Counter() { // useState は [現在のstate値, stateを更新する関数] の配列を返す // 引数は state の初期値 const [count, setCount] = useState(0); const [name, setName] = useState(''); const [isActive, setIsActive] = useState(false); const [items, setItems] = useState([]); const [user, setUser] = useState({ id: null, name: '' }); const increment = () => { // 新しい値を直接渡す // setCount(count + 1); // これは連続で呼ばれると問題が起きる可能性がある // 関数を渡す (現在のstateに基づいて更新する場合に安全) setCount(prevCount => prevCount + 1); }; const handleNameChange = (event) => { setName(event.target.value); }; const toggleActive = () => { setIsActive(prevIsActive => !prevIsActive); }; const addItem = () => { const newItem = `Item ${items.length + 1}`; // 配列やオブジェクトの更新は新しいインスタンスを作成する setItems(prevItems => [...prevItems, newItem]); }; const updateUser = (newName) => { setUser(prevUser => ({ ...prevUser, name: newName })); }; return ( <div> <p>Count: {count}</p> <button className="button is-success" onClick={increment}>+1</button> <div className="field"> <label className="label">Name:</label> <div className="control"> <input className="input" type="text" value={name} onChange={handleNameChange} /> </div> <p>Entered Name: {name}</p> </div> <p>Status: {isActive ? 'Active' : 'Inactive'}</p> <button className="button" onClick={toggleActive}>Toggle Status</button> <h4>Items:</h4> <ul> {items.map((item, index) => <li key={index}>{item}</li>)} </ul> <button className="button is-info" onClick={addItem}>Add Item</button> <p>User: {user.name || 'N/A'}</p> <button className="button is-warning" onClick={() => updateUser('Updated User')}>Update User Name</button> </div> );
}
ライフサイクル / フック
コンポーネントの生成、更新、破棄といった過程で特定の処理を実行するための仕組みです。
useEffect
フック
副作用 (データ取得、購読、DOM操作など) を実行するためのフックです。クラスコンポーネントの componentDidMount
, componentDidUpdate
, componentWillUnmount
の機能を兼ね備えています。
import React, { useState, useEffect } from 'react';
function ExampleComponent({ userId }) { const [data, setData] = useState(null); const [count, setCount] = useState(0); const [width, setWidth] = useState(window.innerWidth); // 1. マウント時のみ実行 (componentDidMount相当) // 第2引数の依存配列が空 [] useEffect(() => { console.log('Component Mounted'); // APIから初期データを取得など fetch(`https://api.example.com/data`) .then(res => res.json()) .then(initialData => setData(initialData)); // クリーンアップ関数 (アンマウント時に実行 - componentWillUnmount相当) return () => { console.log('Component Will Unmount'); // イベントリスナーの削除など }; }, []); // 空の依存配列 // 2. 特定の state や props が変更された時のみ実行 (componentDidUpdate相当) // 第2引数の依存配列に変更を監視したい値を入れる useEffect(() => { if (userId) { console.log(`Fetching data for user: ${userId}`); fetch(`https://api.example.com/users/${userId}`) .then(res => res.json()) .then(userData => setData(userData)); } // userId が変更されたときだけ実行される }, [userId]); // 3. レンダリングごとに実行 (依存配列を指定しない) - 注意して使用 // useEffect(() => { // console.log('Component Rendered or Updated'); // // 依存配列がない場合、毎回のレンダリング後に実行される // }); // 4. 複数の副作用を扱う例 (イベントリスナー) useEffect(() => { const handleResize = () => setWidth(window.innerWidth); console.log('Adding resize listener'); window.addEventListener('resize', handleResize); // クリーンアップ関数: コンポーネントのアンマウント時 or 再実行前に実行される return () => { console.log('Removing resize listener'); window.removeEventListener('resize', handleResize); }; }, []); // マウント・アンマウント時のみ実行 return ( <div> <p>User Data: {data ? JSON.stringify(data) : 'Loading...'}</p> <p>Count: {count}</p> <button className="button" onClick={() => setCount(c => c + 1)}>Increment</button> <p>Window Width: {width}px</p> </div> );
}
その他の主要なフック
フック | 説明 | 使用例 |
---|---|---|
useContext | React コンテキスト (Context) の値を読み取るためのフック。Context.Consumer の代わりに使用できる。 | const theme = useContext(ThemeContext); |
useReducer | useState の代替。より複雑なステートロジックや、次のステートが前のステートに依存する場合に適している。Redux のような状態管理パターンをコンポーネント内で実現できる。 | const [state, dispatch] = useReducer(reducer, initialState); |
useCallback | 関数をメモ化 (キャッシュ) するためのフック。依存配列の値が変わらない限り、同じ関数インスタンスを返す。不要な再レンダリングを防ぐために、子コンポーネントに関数を props として渡す場合などに有効。 | const memoizedCallback = useCallback(() => { doSomething(a, b); }, [a, b]); |
useMemo | 計算結果の値をメモ化するためのフック。依存配列の値が変わらない限り、前回の計算結果を再利用する。重い計算処理の結果をキャッシュするのに役立つ。 | const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]); |
useRef | .current プロパティを持つ変更可能なオブジェクトを返すフック。DOM要素への参照や、レンダリング間で保持したいが再レンダリングを引き起こさない値を格納するために使う。 | const inputEl = useRef(null); useEffect(() => { inputEl.current.focus(); }, []); |
useImperativeHandle | ref を使って親コンポーネントに公開するインスタンス値をカスタマイズするためのフック。forwardRef と一緒に使う。 | useImperativeHandle(ref, () => ({ focus: () => { /* ... */ } })); |
useLayoutEffect | useEffect とほぼ同じだが、こちらはDOMの変更が画面に描画される前に同期的に実行される。DOMの測定や同期的な再レンダリングが必要な場合に使う。通常は useEffect を使うことが推奨される。 | useLayoutEffect(() => { /* DOM measurement */ }, [dependency]); |
useDebugValue | React DevTools でカスタムフックのラベルを表示するために使用できるフック。カスタムフック内部でのみ使用可能。 | useDebugValue(isOnline ? 'Online' : 'Offline'); |
イベント処理
ユーザー操作 (クリック、入力など) に応答する仕組みです。
イベントハンドラの渡し方
JSX の属性としてキャメルケース (onClick
, onChange
など) で関数を渡します。
// 関数コンポーネント
function MyButton() { const handleClick = (event) => { console.log('Button clicked!', event); // event.preventDefault(); // 必要に応じてデフォルト動作を抑制 }; const handleHover = () => { console.log('Mouse over!'); }; return ( <button className="button is-link" onClick={handleClick} onMouseOver={handleHover}> Click or Hover </button> );
}
// クラスコンポーネント
class Toggle extends React.Component { constructor(props) { super(props); this.state = { isToggleOn: true }; // this を束縛 (constructor で bind する方法) // this.handleClick = this.handleClick.bind(this); } // アロー関数で定義する方法 (bind 不要) handleClick = (event) => { console.log('Event object:', event); console.log('Synthetic Event Type:', event.type); // イベントの種類 (例: "click") console.log('Target Element:', event.target); // イベントが発生した要素 // event.stopPropagation(); // イベントの伝播を停止 this.setState(prevState => ({ isToggleOn: !prevState.isToggleOn })); }; render() { return ( <button className="button is-warning" onClick={this.handleClick}> {this.state.isToggleOn ? 'ON' : 'OFF'} </button> ); }
}
フォーム
ユーザー入力を扱うための要素 (<input>
, <textarea>
, <select>
) の制御方法です。
制御コンポーネント (Controlled Components)
フォーム要素の値を React の State で管理する方法です。入力値が変更されるたびに State を更新し、フォーム要素の value
プロパティに State を反映させます。一般的に推奨される方法です。
import React, { useState } from 'react';
function NameForm() { const [name, setName] = useState(''); const [essay, setEssay] = useState('ここにエッセイを書いてください。'); const [flavor, setFlavor] = useState('coconut'); // select の初期値 const handleNameChange = (event) => { setName(event.target.value.toUpperCase()); // 入力を大文字にする例 }; const handleEssayChange = (event) => { setEssay(event.target.value); }; const handleFlavorChange = (event) => { setFlavor(event.target.value); }; const handleSubmit = (event) => { alert(`提出された名前: ${name}, エッセイ: ${essay}, 味: ${flavor}`); event.preventDefault(); // フォーム送信時のページリロードを防ぐ }; return ( <form onSubmit={handleSubmit} className="box"> <div className="field"> <label className="label"> 名前: <div className="control"> <input type="text" className="input" value={name} // state を value にバインド onChange={handleNameChange} // 変更時に state を更新 /> </div> </label> </div> <div className="field"> <label className="label"> エッセイ: <div className="control"> <textarea className="textarea" value={essay} onChange={handleEssayChange} /> </div> </label> </div> <div className="field"> <label className="label"> 好きな味を選択: <div className="control"> <div className="select"> <select value={flavor} onChange={handleFlavorChange}> <option value="grapefruit">グレープフルーツ</option> <option value="lime">ライム</option> <option value="coconut">ココナッツ</option> <option value="mango">マンゴー</option> </select> </div> </div> </label> </div> <div className="field"> <div className="control"> <button type="submit" className="button is-primary"> 送信 </button> </div> </div> </form> );
}
非制御コンポーネント (Uncontrolled Components)
フォーム要素の値を DOM 自身が管理する方法です。React は直接関与せず、値を取得したい場合に Ref を使って DOM にアクセスします。ファイル入力 (<input type="file">
) は常に非制御コンポーネントです。
import React, { useRef } from 'react';
function FileInput() { // Ref を作成 const fileInputRef = useRef(null); const nameInputRef = useRef(null); const handleSubmit = (event) => { event.preventDefault(); // Ref を使って DOM 要素にアクセスし、値を取得 const selectedFile = fileInputRef.current.files[0]; const enteredName = nameInputRef.current.value; if (selectedFile) { alert(`ファイル名: ${selectedFile.name}, サイズ: ${selectedFile.size} bytes, 入力名: ${enteredName}`); } else { alert(`ファイルが選択されていません。入力名: ${enteredName}`); } }; return ( <form onSubmit={handleSubmit} className="box"> <div className="field"> <label className="label"> 名前 (非制御): <div className="control"> {/* defaultValue で初期値を設定可能 */} <input type="text" className="input" ref={nameInputRef} defaultValue="初期値" /> </div> </label> </div> <div className="field"> <label className="label"> ファイルアップロード: <div className="control"> <div className="file has-name"> <label className="file-label"> <input className="file-input" type="file" ref={fileInputRef} // Ref を紐付け /> <span className="file-cta"> <span className="file-icon"> <i className="fas fa-upload"></i> {/* Font Awesomeが必要 */} </span> <span className="file-label"> ファイルを選択… </span> </span> {/* ファイル名表示は別途 state で管理するか、JSでDOM操作が必要 */} </label> </div> </div> </label> </div> <div className="field"> <div className="control"> <button type="submit" className="button is-success">送信</button> </div> </div> </form> );
}
一般的には制御コンポーネントが推奨されますが、既存のコードとの統合や、単純なケースでは非制御コンポーネントが便利な場合もあります。
コンテキスト (Context)
Props を介さずに、コンポーネントツリーの深い階層にあるコンポーネントへデータを渡すための仕組みです。「グローバル」なデータを共有するのに適しています (例: テーマ、言語設定、認証情報)。
基本的な使い方
- コンテキストの作成 (`React.createContext`): デフォルト値を持つコンテキストオブジェクトを作成します。
- プロバイダー (`Context.Provider`): コンテキストの値を共有したいコンポーネントツリーのルート近くで、このコンポーネントを使い、
value
prop で共有する値を渡します。 - コンシューマー (`Context.Consumer` または `useContext`): 値を受け取りたいコンポーネントで、コンテキストの値を読み取ります。
import React, { useState, useContext, createContext } from 'react';
// 1. コンテキストを作成 (デフォルト値として 'light' を設定)
const ThemeContext = createContext('light');
// アプリケーションのルートに近い場所
function App() { const [theme, setTheme] = useState('light'); const toggleTheme = () => { setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light')); }; // 2. Provider でコンテキストの値を設定し、子コンポーネントをラップ return ( // value prop で現在のテーマとテーマを切り替える関数を渡す <ThemeContext.Provider value={{ theme, toggleTheme }}> <Layout /> </ThemeContext.Provider> );
}
// 中間コンポーネント (Props をドリルする必要がない)
function Layout() { return ( <div> <Header /> <Content /> </div> );
}
function Header() { // 3-a. useContext フックでコンテキストの値を取得 (関数コンポーネントで推奨) const { theme, toggleTheme } = useContext(ThemeContext); return ( <header className={`navbar ${theme === 'dark' ? 'is-dark' : 'is-light'}`}> <div className="navbar-brand"> <a className="navbar-item" href="#">MyApp</a> </div> <div className="navbar-end"> <div className="navbar-item"> <button className="button" onClick={toggleTheme}> Toggle Theme ({theme}) </button> </div> </div> </header> );
}
function Content() { return ( <section className="section"> <p className="content">メインコンテンツエリア</p> <ThemedButton /> <LegacyThemedComponent /> {/* Consumerを使う例 */} </section> );
}
function ThemedButton() { // useContext を使ってテーマを取得 const { theme } = useContext(ThemeContext); return ( <button className={`button ${theme === 'dark' ? 'is-info' : 'is-primary'}`}> Themed Button ({theme}) </button> );
}
// 3-b. Context.Consumer を使う方法 (クラスコンポーネントや useContext が使えない場合)
function LegacyThemedComponent() { return ( <ThemeContext.Consumer> {({ theme, toggleTheme }) => ( // レンダープロップパターン <div style={{ marginTop: '1em', padding: '1em', border: `1px solid ${theme === 'dark' ? 'white' : 'black'}` }}> <p>Legacy Component using Consumer</p> <p>Current theme: {theme}</p> <button className="button is-small" onClick={toggleTheme}>Toggle from Legacy</button> </div> )} </ThemeContext.Consumer> );
}
Refs と DOM
DOM ノードや React コンポーネントインスタンスに直接アクセスするための仕組みです。
Ref の作成と利用
useRef
フック (関数コンポーネント)
.current
プロパティを持つオブジェクトを返します。このプロパティに DOM ノードや任意の値を保持できます。
import React, { useRef, useEffect, useState } from 'react';
function TextInputWithFocusButton() { // 1. Refオブジェクトを作成 const inputRef = useRef(null); const counterRef = useRef(0); // DOM以外にも使える (レンダリング間で値を保持) const [renderCount, setRenderCount] = useState(0); // 再レンダリング確認用 useEffect(() => { // コンポーネントがマウントされたら input にフォーカスを当てる if (inputRef.current) { inputRef.current.focus(); console.log('Input focused on mount'); } }, []); // 空の依存配列でマウント時にのみ実行 const handleClick = () => { // 3. Refの current プロパティ経由でDOMノードにアクセス if (inputRef.current) { inputRef.current.focus(); console.log('Current input value:', inputRef.current.value); } }; const incrementCounterRef = () => { counterRef.current = counterRef.current + 1; console.log('Counter Ref value:', counterRef.current); // Ref の値を変更しても再レンダリングはトリガーされない }; return ( <div className="box"> {/* 2. 要素の ref 属性に Ref オブジェクトを渡す */} <input type="text" ref={inputRef} className="input" placeholder="Focus me" /> <button onClick={handleClick} className="button is-info mt-2"> テキスト入力にフォーカス </button> <p className="mt-2">Ref Counter: {counterRef.current} (This won't update UI)</p> <button onClick={incrementCounterRef} className="button is-light mt-2"> Increment Ref Counter </button> <button onClick={() => setRenderCount(r => r + 1)} className="button is-light mt-2"> Force Re-render ({renderCount}) </button> </div> );
}
Ref の用途
- 特定の DOM ノードへのアクセス (フォーカス、テキスト選択、メディア再生の管理)
- DOM の測定 (要素のサイズや位置の取得)
- 外部ライブラリ (特に jQuery プラグインなど) との連携
- レンダリング間で値を保持したいが、その値の変更が再レンダリングを引き起こさないようにしたい場合 (タイマー ID、ミュータブルな値など)
Ref のフォワーディング (`React.forwardRef`)
親コンポーネントから子コンポーネント内の DOM ノードやコンポーネントインスタンスに Ref を渡すための仕組みです。これにより、子コンポーネントの実装詳細を隠蔽しつつ、親が必要な場合に子要素にアクセスできます。
import React, { useRef, forwardRef, useImperativeHandle } from 'react';
// 1. forwardRef でラップされたコンポーネントを定義
// 第2引数として ref を受け取る
const FancyInput = forwardRef((props, ref) => { const inputRef = useRef(); // 3. useImperativeHandle で親に公開するメソッドを定義 (オプション) // これにより、親は input 要素全体ではなく、特定の機能 (focus) のみにアクセスできる useImperativeHandle(ref, () => ({ focus: () => { inputRef.current.focus(); console.log('Focus called via useImperativeHandle'); }, getValue: () => { return inputRef.current.value; } // 親に input 要素全体を公開する場合はこの useImperativeHandle は不要で、 // 下の input の ref={ref} を直接使う })); return ( <input ref={inputRef} // 内部で使用する Ref // ref={ref} // 親から渡された ref を直接 DOM 要素に渡す場合 className="input is-primary" placeholder="Fancy Input" {...props} /> );
});
// 親コンポーネント
function ParentComponent() { // 親で Ref を作成 const fancyInputRef = useRef(null); const handleFocusClick = () => { // 2. Ref を使って FancyInput の公開メソッドを呼び出す if (fancyInputRef.current) { fancyInputRef.current.focus(); } }; const handleGetValueClick = () => { if (fancyInputRef.current) { alert(`Value: ${fancyInputRef.current.getValue()}`); } }; return ( <div className="box"> {/* 作成した Ref を子コンポーネントに渡す */} <FancyInput ref={fancyInputRef} /> <button onClick={handleFocusClick} className="button is-success mt-2"> Focus Fancy Input </button> <button onClick={handleGetValueClick} className="button is-info mt-2 ml-2"> Get Fancy Input Value </button> </div> );
}
forwardRef
は、特に再利用可能なコンポーネントライブラリを作成する際に便利です。
高階コンポーネント (Higher-Order Component – HOC)
コンポーネントを引数に取り、新しいコンポーネントを返す関数です。コンポーネント間で共通のロジック (例: データ購読、認証チェック) を再利用するためのパターンです。
フックの登場により、以前ほど頻繁には使われなくなりましたが、既存のコードベースや特定のシナリオでは依然として有効です。
定義方法
コンポーネントを受け取り、追加の props やロジックを注入した新しいコンポーネントを返す関数を作成します。
import React, { useState, useEffect } from 'react';
// HOC の例: マウス位置を追跡し、props として注入する
function withMousePosition(WrappedComponent) { // HOC は新しいコンポーネント (通常は関数コンポーネント) を返す return function WithMousePosition(props) { const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 }); useEffect(() => { const handleMouseMove = (event) => { setMousePosition({ x: event.clientX, y: event.clientY }); }; window.addEventListener('mousemove', handleMouseMove); return () => { window.removeEventListener('mousemove', handleMouseMove); }; }, []); // 元のコンポーネントに mousePosition prop を追加してレンダリング return <WrappedComponent {...props} mousePosition={mousePosition} />; };
}
// HOC の例: データローディング状態を管理する
function withLoadingIndicator(WrappedComponent, fetchData) { return function WithLoading(props) { const [isLoading, setIsLoading] = useState(true); const [data, setData] = useState(null); const [error, setError] = useState(null); useEffect(() => { setIsLoading(true); fetchData(props) // fetchData 関数は外部から渡すなどして柔軟性を持たせる .then(result => { setData(result); setIsLoading(false); }) .catch(err => { setError(err); setIsLoading(false); }); }, [props]); // props の変更に応じて再取得する場合 (依存関係は要調整) if (isLoading) { return <div className="notification is-info">Loading...</div>; } if (error) { return <div className="notification is-danger">Error loading data: {error.message}</div>; } return <WrappedComponent {...props} data={data} />; };
}
使用方法
HOC 関数でラップしたいコンポーネントを引数として渡します。
import React from 'react';
// (上記の withMousePosition, withLoadingIndicator が定義されているとする)
// 表示用コンポーネント 1: マウス位置を表示
function DisplayMousePosition({ mousePosition }) { if (!mousePosition) return null; return ( <div className="box"> Mouse position: X = {mousePosition.x}, Y = {mousePosition.y} </div> );
}
// 表示用コンポーネント 2: ユーザーリストを表示 (データは HOC から注入)
function UserList({ data, title }) { return ( <div className="box"> <h3 className="title is-5">{title}</h3> <ul> {data && data.map(user => <li key={user.id}>{user.name}</li>)} </ul> </div> );
}
// HOC を適用
const MouseTracker = withMousePosition(DisplayMousePosition);
// データ取得関数 (例)
const fetchUsers = async (props) => { // props から API エンドポイントなどを取得できる const response = await fetch('https://jsonplaceholder.typicode.com/users?_limit=5'); if (!response.ok) throw new Error('Failed to fetch users'); return response.json();
};
const UserListWithLoading = withLoadingIndicator(UserList, fetchUsers);
// アプリケーションで使用
function AppWithHOCs() { return ( <div> <h2 className="title is-4">Using HOCs</h2> <MouseTracker /> <UserListWithLoading title="User List (Loaded via HOC)" /> </div> );
}
export default AppWithHOCs;
Render Props
コンポーネント間でロジックを共有するもう一つのパターンです。値が関数である prop を使用して、コンポーネントが何をレンダリングするかを親コンポーネントに委ねます。
HOC と同様、フックの登場で利用頻度は減りましたが、特定のUIパターンやライブラリで使われています。
定義方法
自身の State やロジックの結果を引数として、render
という名前の prop (または任意の名前の関数 prop) を呼び出すコンポーネントを作成します。
import React, { useState, useEffect } from 'react';
// Render Prop コンポーネントの例: マウス位置を提供する
class Mouse extends React.Component { state = { x: 0, y: 0 }; handleMouseMove = (event) => { this.setState({ x: event.clientX, y: event.clientY }); }; componentDidMount() { window.addEventListener('mousemove', this.handleMouseMove); } componentWillUnmount() { window.removeEventListener('mousemove', this.handleMouseMove); } render() { // this.props.render または this.props.children が関数であることを期待し、 // state (マウス位置) を引数として呼び出す if (typeof this.props.render === 'function') { return this.props.render(this.state); } if (typeof this.props.children === 'function') { return this.props.children(this.state); } // フォールバックやエラー処理 console.warn('Mouse component requires a render prop or a function as children.'); return null; }
}
// Render Prop コンポーネントの例: データ取得を提供する (関数コンポーネント版)
function Fetcher({ url, children }) { const [data, setData] = useState(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { setIsLoading(true); fetch(url) .then(res => { if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`); return res.json(); }) .then(fetchedData => { setData(fetchedData); setIsLoading(false); }) .catch(fetchError => { setError(fetchError); setIsLoading(false); }); }, [url]); // children が関数であることを期待し、ロード状態とデータを渡す if (typeof children !== 'function') { console.warn('Fetcher component requires a function as children.'); return null; } return children({ data, isLoading, error });
}
使用方法
Render Prop コンポーネントを使用し、render
prop または children
prop として関数を渡します。その関数内で、受け取ったデータを使って UI をレンダリングします。
import React from 'react';
// (上記の Mouse, Fetcher コンポーネントが定義されているとする)
// Mouse コンポーネントの使用例 1: render prop を使用
function AppWithRenderProps() { return ( <div> <h1 className="title is-4">Move the mouse around!</h1> <Mouse render={({ x, y }) => ( // render propに関数を渡す <div className="box has-background-light"> The mouse position is ({x}, {y}) - via render prop </div> )} /> {/* Mouse コンポーネントの使用例 2: children as a function を使用 */} <Mouse> {({ x, y }) => ( // children propに関数を渡す <p className="notification is-success mt-4"> Mouse coordinates: X={x}, Y={y} - via children prop </p> )} </Mouse> <h2 className="title is-4 mt-5">Fetching Data with Render Prop</h2> <Fetcher url="https://jsonplaceholder.typicode.com/posts/1"> {({ data, isLoading, error }) => { // children propに関数を渡す if (isLoading) return <p>Loading post...</p>; if (error) return <p className="has-text-danger">Error: {error.message}</p>; return ( <div className="box"> <h3 className="title is-5">Post Title: {data?.title}</h3> <p>{data?.body}</p> </div> ); }} </Fetcher> </div> );
}
export default AppWithRenderProps;
ルーティング (React Router)
シングルページアプリケーション (SPA) 内で、URL に応じて表示するコンポーネントを切り替えるためのライブラリです (react-router-dom
が Web アプリケーションで一般的に使われます)。
ここでは React Router v6 の基本的な使い方を説明します。
基本的な設定
アプリケーションのルートで BrowserRouter
(または他のルーター) でラップし、Routes
内に個々のルート定義を Route
として記述します。
// main.jsx または index.js など
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
import 'bulma/css/bulma.min.css'; // bulma スタイル
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render( <React.StrictMode> <BrowserRouter> {/* アプリ全体を BrowserRouter でラップ */} <App /> </BrowserRouter> </React.StrictMode>
);
// App.jsx
import React from 'react';
import { Routes, Route, Link, Outlet, NavLink } from 'react-router-dom';
// 各ページのコンポーネント (仮)
const Home = () => <div><h2 className="title">Home Page </h2><p>Welcome to our site!</p></div>;
const About = () => <div><h2 className="title">About Us </h2><p>Learn more about our company.</p></div>;
const Contact = () => <div><h2 className="title">Contact Us </h2><p>Get in touch!</p></div>;
const NotFound = () => <div><h2 className="title is-danger">404 Not Found </h2><p>Oops! Page not found.</p></div>;
const ProductsLayout = () => ( <div> <h3 className="subtitle">Our Products </h3> <nav className="breadcrumb"> <ul> <li><Link to="/products">All Products</Link></li> <li><Link to="/products/new">New Arrivals</Link></li> </ul> </nav> <Outlet /> {/* ネストされたルートのコンポーネントがここにレンダリングされる */} </div>
);
const ProductList = () => <p>List of all products.</p>;
const NewProducts = () => <p>Check out our new products!</p>;
const ProductDetail = () => { const { productId } = useParams(); // URLパラメータを取得 return <p>Details for Product ID: {productId}</p>;
};
function App() { return ( <div className="container mt-5"> <nav className="navbar is-light mb-5"> <div className="navbar-menu"> <div className="navbar-start"> {/* NavLink はアクティブなリンクにスタイルを適用できる */} <NavLink to="/" className={({ isActive }) => isActive ? "navbar-item is-active" : "navbar-item"} end // 完全一致の場合のみ is-active を適用 > Home </NavLink> <NavLink to="/about" className={({ isActive }) => isActive ? "navbar-item is-active" : "navbar-item"} > About </NavLink> <NavLink to="/products" className={({ isActive }) => isActive ? "navbar-item is-active" : "navbar-item"} > Products </NavLink> <Link to="/contact" className="navbar-item"> {/* Link は基本的なナビゲーション */} Contact </Link> </div> </div> </nav> {/* Routes コンポーネント内でルート定義 */} <Routes> {/* 基本的なルート */} <Route path="/" element={<Home />} /> <Route path="/about" element={<About />} /> <Route path="/contact" element={<Contact />} /> {/* ネストされたルート */} <Route path="/products" element={<ProductsLayout />}> {/* index ルート: 親パス ("/products") にマッチしたときのデフォルト表示 */} <Route index element={<ProductList />} /> <Route path="new" element={<NewProducts />} /> {/* URL パラメータ (:productId) */} <Route path=":productId" element={<ProductDetail />} /> </Route> {/* どのルートにもマッチしなかった場合 (404 Not Found) */} <Route path="*" element={<NotFound />} /> </Routes> </div> );
}
export default App;
ナビゲーション
コンポーネント/フック | 説明 | 使用例 |
---|---|---|
<Link to="/path"> | ページ遷移を行うための基本的なコンポーネント。クリックすると指定したパスに移動します。<a> タグとしてレンダリングされます。 | <Link to="/about">About Us</Link> |
<NavLink to="/path"> | Link と似ていますが、現在の URL が to prop のパスと一致する場合に、アクティブ状態を示すスタイルやクラスを適用できます。ナビゲーションメニューでよく使われます。 | <NavLink to="/" className={({ isActive }) => isActive ? 'active' : ''}>Home</NavLink> |
useNavigate() | プログラム的に(例: フォーム送信後やボタンクリック時など)ページ遷移を行うためのフック。遷移関数を返します。 |
|
URL パラメータとクエリパラメータ
フック/オブジェクト | 説明 | 使用例 |
---|---|---|
useParams() | ルートパスで定義された URL パラメータ (例: /users/:userId の userId ) をキーと値のペアとして含むオブジェクトを返します。 |
|
useSearchParams() | URL のクエリ文字列 (?key=value&key2=value2 ) を操作・取得するためのフック。URLSearchParams インターフェースに似たオブジェクトと、それを更新する関数を返します。 |
|
useLocation() | 現在の location オブジェクト (pathname , search , hash , state , key を含む) を返します。URL の変更を検知したり、詳細な情報にアクセスしたりするのに使います。 |
|
React Router には他にもレイアウトコンポーネント (Outlet
)、リダイレクト (Navigate
コンポーネント)、遅延読み込み (lazy
と Suspense
との組み合わせ) など、多くの機能があります。
その他便利な機能・パターン
コード分割 (`React.lazy` と `Suspense`)
アプリケーションの初期ロード時間を短縮するために、コードを小さなチャンクに分割し、必要になったときに動的に読み込む手法です。
import React, { Suspense, lazy } from 'react';
// 通常のインポートではなく、React.lazy() を使ってコンポーネントを動的にインポート
const OtherComponent = lazy(() => import('./OtherComponent'));
const AnotherComponent = lazy(() => import('./AnotherComponent'));
function MyComponent() { return ( <div> <h1>My Application</h1> {/* Suspense コンポーネントで lazy コンポーネントをラップ */} {/* fallback prop には、コンポーネントの読み込み中に表示する内容を指定 */} <Suspense fallback={<div className="notification is-loading">Loading...</div>}> <section> <h2>Dynamically Loaded Components</h2> {/* これらのコンポーネントは、レンダリングが必要になった時点で初めて読み込まれる */} <OtherComponent /> <AnotherComponent /> </section> </Suspense> </div> );
}
// React Router との組み合わせ
import { Routes, Route } from 'react-router-dom';
const HomePage = lazy(() => import('./pages/HomePage'));
const AboutPage = lazy(() => import('./pages/AboutPage'));
function AppRoutes() { return ( <Suspense fallback={<div>Loading page...</div>}> <Routes> <Route path="/" element={<HomePage />} /> <Route path="/about" element={<AboutPage />} /> </Routes> </Suspense> );
}
export default MyComponent; // or AppRoutes
React.lazy
は現在、デフォルトエクスポートを持つコンポーネントのみをサポートしています。
エラー境界 (Error Boundaries)
コンポーネントツリー内のどこかで発生した JavaScript エラーをキャッチし、フォールバック UI を表示するためのコンポーネントです。これにより、一部の UI のエラーがアプリケーション全体をクラッシュさせるのを防ぎます。
エラー境界は、ライフサイクルメソッド static getDerivedStateFromError()
または componentDidCatch()
のいずれか (または両方) を定義したクラスコンポーネントである必要があります。現時点では、関数コンポーネントだけでエラー境界を実装するフックはありません。
import React, { Component } from 'react';
class ErrorBoundary extends Component { constructor(props) { super(props); this.state = { hasError: false, error: null, errorInfo: null }; } // エラーが発生した後にフォールバックUIをレンダリングするために使用 static getDerivedStateFromError(error) { // 次のレンダリングでフォールバックUIが表示されるように state を更新 return { hasError: true }; } // エラー情報をログに記録するために使用 componentDidCatch(error, errorInfo) { // 例: エラーレポートサービスにエラー情報を送信する console.error("Uncaught error:", error, errorInfo); this.setState({ error: error, errorInfo: errorInfo }); // logErrorToMyService(error, errorInfo); } render() { if (this.state.hasError) { // エラー発生時に表示するカスタムフォールバックUI return ( <div className="message is-danger"> <div className="message-header"> <p>Something went wrong </p> </div> <div className="message-body"> <p>{this.props.fallbackMessage || "An error occurred in this section."}</p> {/* 開発モードではエラー詳細を表示することも可能 */} {process.env.NODE_ENV === 'development' && this.state.error && ( <details style={{ whiteSpace: 'pre-wrap', marginTop: '1em' }}> {this.state.error.toString()} <br /> {this.state.errorInfo?.componentStack} </details> )} </div> </div> ); } // 通常時、子コンポーネントをそのままレンダリング return this.props.children; }
}
// 使用例
function RiskyComponent() { // 何らかの条件でエラーをスローする可能性のあるコンポーネント (例) if (Math.random() > 0.7) { throw new Error("I crashed!"); } return <p>Everything is fine here... for now.</p>;
}
function AppWithErrorBoundary() { return ( <div> <h1>App with Error Boundary</h1> <p>This part is outside the boundary.</p> <ErrorBoundary fallbackMessage="Failed to load the risky component."> <h2>Inside Error Boundary</h2> <RiskyComponent /> <p>Another component inside the boundary.</p> </ErrorBoundary> <ErrorBoundary fallbackMessage="Profile loading failed."> {/* 別のコンポーネントを別の境界でラップ */} {/* <UserProfile /> */} </ErrorBoundary> <p>This part is also outside the boundary.</p> </div> );
}
export default AppWithErrorBoundary;
エラー境界は、自身の内部のエラーではなく、子孫コンポーネントのエラーのみをキャッチします。