Untitled

Learning Next 運営
30 分で読めます

【子から親へ】Reactコールバック関数の使い方完全ガイド

「子コンポーネントから 親コンポーネントの関数を呼びたい」

「ボタンを押したら親の状態を 更新したいんだけど...」

「子コンポーネントのイベントを 親に伝える方法がわからない」

そんな悩みを抱えていませんか?

React開発でよく出てくるのが、 親子間のデータのやりとりです。 特に「子から親へ」の通信は 初心者には少し難しく感じるかもしれません。

でも大丈夫です! コールバック関数という仕組みを使えば 簡単に実現できるんです。

今回は、子コンポーネントから 親コンポーネントの関数を呼び出す方法を 一緒に学んでいきましょう!

コールバック関数って何?

「コールバック関数」と聞くと 難しそうに感じるかもしれませんが、 実はとってもシンプルな概念です。

基本的な仕組み

簡単に言うと、 「関数を手紙として渡す」 イメージです。

// 親コンポーネント
const ParentComponent = () => {
const [message, setMessage] = useState('');
// 子コンポーネントに渡すコールバック関数
const handleChildClick = (childMessage) => {
setMessage(childMessage);
console.log('子からメッセージが届きました:', childMessage);
};
return (
<div>
<h2>親コンポーネント</h2>
<p>受信メッセージ: {message}</p>
{/* 関数をpropsとして渡す */}
<ChildComponent onChildClick={handleChildClick} />
</div>
);
};
// 子コンポーネント
const ChildComponent = ({ onChildClick }) => {
const handleClick = () => {
// 親のコールバック関数を呼び出す
onChildClick('こんにちは、親コンポーネント!');
};
return (
<div>
<h3>子コンポーネント</h3>
<button onClick={handleClick}>
親にメッセージを送る
</button>
</div>
);
};

このコードを見てみましょう。

親コンポーネントの役割

  • handleChildClickという関数を作成
  • その関数をonChildClickという名前で子に渡す
  • 子から呼ばれるとmessageを更新

子コンポーネントの役割

  • propsで受け取ったonChildClick関数を保管
  • ボタンがクリックされたら、その関数を実行
  • 実行時にメッセージを引数として渡す

「なるほど!関数を手紙として 渡してるんですね!」

そうです!この仕組みが コールバック関数の基本です。

実際に使ってみよう:基本パターン

では、具体的な例を使って コールバック関数の使い方を マスターしていきましょう。

パターン1:カウンターアプリ

よくある例として、 カウンターアプリを作ってみます。

const CounterApp = () => {
const [count, setCount] = useState(0);
// カウンターを増やす関数
const incrementCount = () => {
setCount(prev => prev + 1);
};
// カウンターを減らす関数
const decrementCount = () => {
setCount(prev => prev - 1);
};
// カウンターをリセットする関数
const resetCount = () => {
setCount(0);
};
return (
<div>
<h2>カウンター: {count}</h2>
<CounterControls
onIncrement={incrementCount}
onDecrement={decrementCount}
onReset={resetCount}
/>
</div>
);
};
const CounterControls = ({ onIncrement, onDecrement, onReset }) => {
return (
<div>
<button onClick={onIncrement}>+1</button>
<button onClick={onDecrement}>-1</button>
<button onClick={onReset}>リセット</button>
</div>
);
};

このコードのポイントを見てみましょう。

**親コンポーネント(CounterApp)**では:

  • countという状態を管理
  • incrementCount, decrementCount, resetCountの 3つの関数を定義
  • これらの関数を子コンポーネントに渡す

**子コンポーネント(CounterControls)**では:

  • 受け取った3つの関数をそれぞれのボタンに設定
  • ボタンがクリックされると、対応する親の関数が実行される

「子コンポーネントは ボタンの表示だけに集中できて、 親コンポーネントは データ管理に集中できるんですね!」

その通りです! これが関心の分離という 良い設計パターンです。

パターン2:フォーム送信

今度は、もう少し実用的な フォーム送信の例を見てみましょう。

const UserForm = () => {
const [users, setUsers] = useState([]);
// ユーザーを追加する関数
const addUser = (newUser) => {
setUsers(prev => [...prev, {
...newUser,
id: Date.now()
}]);
console.log('新しいユーザーが追加されました:', newUser);
};
return (
<div>
<h2>ユーザー管理</h2>
{/* フォームコンポーネントにコールバックを渡す */}
<AddUserForm onAddUser={addUser} />
<UserList users={users} />
</div>
);
};
const AddUserForm = ({ onAddUser }) => {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
if (name && email) {
// 親のコールバック関数を呼び出してユーザーを追加
onAddUser({ name, email });
// フォームをリセット
setName('');
setEmail('');
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="名前"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<input
type="email"
placeholder="メールアドレス"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<button type="submit">ユーザーを追加</button>
</form>
);
};
const UserList = ({ users }) => (
<ul>
{users.map(user => (
<li key={user.id}>
{user.name} - {user.email}
</li>
))}
</ul>
);

このフォーム例のポイント:

データの流れ

  1. 子コンポーネント(AddUserForm)でフォーム送信
  2. onAddUserコールバック関数を呼び出し
  3. 親コンポーネント(UserForm)のaddUser関数が実行
  4. 親のusers状態が更新
  5. UserListコンポーネントに新しいリストが渡される

引数の活用

  • onAddUser({ name, email })のように オブジェクトとして複数のデータを一度に渡している

パターン3:モーダル制御

もう一つの実用例として、 モーダル(ポップアップ)の制御も 見てみましょう。

const ModalApp = () => {
const [isModalOpen, setIsModalOpen] = useState(false);
const [modalContent, setModalContent] = useState('');
// モーダルを開く関数
const openModal = (content) => {
setModalContent(content);
setIsModalOpen(true);
};
// モーダルを閉じる関数
const closeModal = () => {
setIsModalOpen(false);
setModalContent('');
};
return (
<div>
<h2>モーダル制御アプリ</h2>
<ButtonGroup onOpenModal={openModal} />
{isModalOpen && (
<Modal content={modalContent} onClose={closeModal} />
)}
</div>
);
};
const ButtonGroup = ({ onOpenModal }) => {
return (
<div>
<button onClick={() => onOpenModal('お知らせ内容です')}>
お知らせを表示
</button>
<button onClick={() => onOpenModal('警告メッセージです')}>
警告を表示
</button>
<button onClick={() => onOpenModal('成功メッセージです')}>
成功を表示
</button>
</div>
);
};
const Modal = ({ content, onClose }) => {
return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
<p>{content}</p>
<button onClick={onClose}>閉じる</button>
</div>
</div>
);
};

このモーダル例の特徴:

双方向の通信

  • ButtonGroupから親へ:「モーダルを開いて」
  • Modalから親へ:「モーダルを閉じて」

引数を使った詳細な制御

  • onOpenModal('お知らせ内容です')のように 表示する内容も一緒に渡している

複数のデータを扱う応用パターン

実際のアプリでは、 もう少し複雑なデータのやりとりが 必要になることがあります。

TODOアプリの例

const TodoApp = () => {
const [todos, setTodos] = useState([
{ id: 1, text: 'Reactを学習する', completed: false },
{ id: 2, text: '買い物に行く', completed: true }
]);
// TODOの完了状態を切り替える
const toggleTodo = (id) => {
setTodos(prev =>
prev.map(todo =>
todo.id === id
? { ...todo, completed: !todo.completed }
: todo
)
);
};
// TODOを削除する
const deleteTodo = (id) => {
setTodos(prev => prev.filter(todo => todo.id !== id));
};
// TODOを編集する
const editTodo = (id, newText) => {
setTodos(prev =>
prev.map(todo =>
todo.id === id
? { ...todo, text: newText }
: todo
)
);
};
return (
<div>
<h2>TODOリスト</h2>
<TodoList
todos={todos}
onToggle={toggleTodo}
onDelete={deleteTodo}
onEdit={editTodo}
/>
</div>
);
};
const TodoList = ({ todos, onToggle, onDelete, onEdit }) => {
return (
<ul>
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={onToggle}
onDelete={onDelete}
onEdit={onEdit}
/>
))}
</ul>
);
};
const TodoItem = ({ todo, onToggle, onDelete, onEdit }) => {
const [isEditing, setIsEditing] = useState(false);
const [editText, setEditText] = useState(todo.text);
const handleEdit = () => {
onEdit(todo.id, editText);
setIsEditing(false);
};
return (
<li>
{isEditing ? (
<div>
<input
value={editText}
onChange={(e) => setEditText(e.target.value)}
/>
<button onClick={handleEdit}>保存</button>
<button onClick={() => setIsEditing(false)}>
キャンセル
</button>
</div>
) : (
<div>
<span
style={{
textDecoration: todo.completed ? 'line-through' : 'none'
}}
>
{todo.text}
</span>
<button onClick={() => onToggle(todo.id)}>
{todo.completed ? '未完了' : '完了'}
</button>
<button onClick={() => setIsEditing(true)}>
編集
</button>
<button onClick={() => onDelete(todo.id)}>
削除
</button>
</div>
)}
</li>
);
};

このTODOアプリを詳しく見てみましょう。

複数のコールバック関数

  • onToggle: 完了状態の切り替え
  • onDelete: TODO削除
  • onEdit: TODO編集

これらはそれぞれ違う目的で使われています。

引数の使い方

  • onToggle(todo.id): IDだけを渡す
  • onEdit(todo.id, editText): IDと新しいテキストを渡す

「一つのコンポーネントに 複数のコールバック関数を 渡せるんですね!」

そうです!必要に応じて いくつでもコールバック関数を 渡すことができます。

詳細な情報を渡すパターン

もっと複雑なデータを扱う場合の 例も見てみましょう。

ショッピングカートアプリ

const ShoppingCart = () => {
const [cart, setCart] = useState([]);
const [total, setTotal] = useState(0);
// 商品をカートに追加
const addToCart = (product, quantity = 1) => {
const existingItem = cart.find(item => item.id === product.id);
if (existingItem) {
setCart(prev =>
prev.map(item =>
item.id === product.id
? { ...item, quantity: item.quantity + quantity }
: item
)
);
} else {
setCart(prev => [...prev, { ...product, quantity }]);
}
updateTotal();
};
// 商品の数量を変更
const updateQuantity = (productId, newQuantity) => {
if (newQuantity === 0) {
removeFromCart(productId);
} else {
setCart(prev =>
prev.map(item =>
item.id === productId
? { ...item, quantity: newQuantity }
: item
)
);
}
updateTotal();
};
// 商品をカートから削除
const removeFromCart = (productId) => {
setCart(prev => prev.filter(item => item.id !== productId));
updateTotal();
};
// 合計金額を更新
const updateTotal = () => {
const newTotal = cart.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
setTotal(newTotal);
};
useEffect(() => {
updateTotal();
}, [cart]);
return (
<div>
<h2>ショッピングカート</h2>
<ProductList onAddToCart={addToCart} />
<CartSummary
cart={cart}
total={total}
onUpdateQuantity={updateQuantity}
onRemoveItem={removeFromCart}
/>
</div>
);
};
const ProductList = ({ onAddToCart }) => {
const products = [
{ id: 1, name: 'ノートPC', price: 80000 },
{ id: 2, name: 'マウス', price: 2000 },
{ id: 3, name: 'キーボード', price: 5000 }
];
return (
<div>
<h3>商品一覧</h3>
{products.map(product => (
<div key={product.id}>
<span>
{product.name} - ¥{product.price.toLocaleString()}
</span>
<button onClick={() => onAddToCart(product)}>
カートに追加
</button>
</div>
))}
</div>
);
};
const CartSummary = ({ cart, total, onUpdateQuantity, onRemoveItem }) => {
return (
<div>
<h3>カート内容</h3>
{cart.length === 0 ? (
<p>カートは空です</p>
) : (
<>
{cart.map(item => (
<div key={item.id}>
<span>{item.name}</span>
<input
type="number"
min="0"
value={item.quantity}
onChange={(e) =>
onUpdateQuantity(item.id, parseInt(e.target.value) || 0)
}
/>
<span>
¥{(item.price * item.quantity).toLocaleString()}
</span>
<button onClick={() => onRemoveItem(item.id)}>
削除
</button>
</div>
))}
<div>
<strong>合計: ¥{total.toLocaleString()}</strong>
</div>
</>
)}
</div>
);
};

このショッピングカートの例では:

オブジェクトを引数として渡す

  • onAddToCart(product)で商品オブジェクト全体を渡す

複数の引数を組み合わせ

  • onUpdateQuantity(item.id, parseInt(e.target.value) || 0)で IDと数量の両方を渡す

「複雑なデータでも コールバック関数で やりとりできるんですね!」

はい!オブジェクトでも配列でも どんなデータでも引数として 渡すことができます。

パフォーマンスを考慮した最適化

コールバック関数を使う時に 気をつけたいのがパフォーマンスです。

useCallbackを使った最適化

const OptimizedParent = () => {
const [items, setItems] = useState([]);
const [filter, setFilter] = useState('');
// useCallbackでコールバック関数をメモ化
const addItem = useCallback((newItem) => {
setItems(prev => [...prev, {
...newItem,
id: Date.now()
}]);
}, []);
const deleteItem = useCallback((itemId) => {
setItems(prev => prev.filter(item => item.id !== itemId));
}, []);
const updateFilter = useCallback((newFilter) => {
setFilter(newFilter);
}, []);
const filteredItems = useMemo(() => {
return items.filter(item =>
item.name.toLowerCase().includes(filter.toLowerCase())
);
}, [items, filter]);
return (
<div>
<ItemForm onAddItem={addItem} />
<FilterInput filter={filter} onUpdateFilter={updateFilter} />
<ItemList items={filteredItems} onDeleteItem={deleteItem} />
</div>
);
};
// React.memoで子コンポーネントを最適化
const ItemForm = React.memo(({ onAddItem }) => {
const [name, setName] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
if (name.trim()) {
onAddItem({ name: name.trim() });
setName('');
}
};
return (
<form onSubmit={handleSubmit}>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="アイテム名"
/>
<button type="submit">追加</button>
</form>
);
});

この最適化のポイント:

useCallbackの活用

  • コールバック関数が毎回新しく作られるのを防ぐ
  • 子コンポーネントの不要な再レンダリングを防止

React.memoの活用

  • propsが変わらない限り、子コンポーネントを再レンダリングしない

「毎回新しい関数が作られると なにか問題があるんですか?」

良い質問ですね! 新しい関数が作られると、 子コンポーネントが「propsが変わった」と 判断して無駄に再レンダリングしてしまうんです。

エラーハンドリングも忘れずに

実際のアプリでは、 エラーが発生する可能性も 考慮する必要があります。

安全なコールバック実装

const SafeCallbackExample = () => {
const [status, setStatus] = useState('');
const [loading, setLoading] = useState(false);
// エラーハンドリングを含むコールバック
const handleAsyncOperation = useCallback(async (data) => {
try {
setLoading(true);
setStatus('処理中...');
// 非同期処理のシミュレーション
await new Promise(resolve => setTimeout(resolve, 2000));
// ランダムでエラーを発生させる
if (Math.random() < 0.3) {
throw new Error('処理に失敗しました');
}
setStatus('処理が完了しました');
console.log('処理成功:', data);
} catch (error) {
setStatus(`エラー: ${error.message}`);
console.error('処理エラー:', error);
} finally {
setLoading(false);
}
}, []);
return (
<div>
<h2>安全なコールバック例</h2>
<p>ステータス: {status}</p>
<AsyncOperationButton
onExecute={handleAsyncOperation}
disabled={loading}
/>
</div>
);
};
const AsyncOperationButton = ({ onExecute, disabled }) => {
const handleClick = () => {
const data = {
timestamp: new Date().toISOString(),
operation: 'test-operation'
};
onExecute(data);
};
return (
<button onClick={handleClick} disabled={disabled}>
{disabled ? '処理中...' : '非同期処理を実行'}
</button>
);
};

このエラーハンドリングの例では:

try-catch文の活用

  • 非同期処理でエラーが発生した場合の対応

loadingステートの管理

  • 処理中はボタンを無効化
  • ユーザーに処理状況を伝える

finally文の活用

  • エラーが発生してもloadingを解除

まとめ:コールバック関数をマスターしよう

お疲れさまでした! Reactのコールバック関数について 詳しく学んできました。

重要なポイントのおさらい

基本的な使い方

  • 親コンポーネントで関数を定義
  • その関数をpropsとして子に渡す
  • 子コンポーネントで受け取った関数を実行

実践的なパターン

  • 単純なイベント処理(ボタンクリック)
  • フォーム送信とデータ追加
  • モーダルの開閉制御
  • 複数のデータ操作(TODO、ショッピングカート)

パフォーマンス最適化

  • useCallbackでコールバック関数をメモ化
  • React.memoで子コンポーネントの再レンダリング制御
  • useMemoで計算結果をメモ化

エラーハンドリング

  • try-catch文で安全な処理
  • ローディング状態の管理
  • ユーザーへの適切なフィードバック

次のステップ

コールバック関数をマスターしたら、 こんなことにもチャレンジしてみてください:

より高度なパターン

  • Context APIとの組み合わせ
  • カスタムフックでのコールバック活用
  • TypeScriptでの型安全なコールバック
  • テストしやすいコールバック設計

実践的なプロジェクト

  • 本格的なTODOアプリ
  • ショッピングカートシステム
  • チャットアプリケーション
  • ダッシュボード管理画面

最後に

コールバック関数は、 Reactの基本中の基本です。

最初は少し難しく感じるかもしれませんが、 慣れてしまえば自然に使えるようになります。

大切なのは

  • 小さな例から始めること
  • 実際に手を動かして試すこと
  • エラーを恐れずにチャレンジすること
  • 段階的に複雑な例に挑戦すること

ぜひ今回学んだことを活かして、 あなただけの素敵なReactアプリを 作ってみてくださいね!

きっと、親子間の通信が スムーズにできるようになって、 もっとReactが楽しくなりますよ!🚀

関連記事