なぜリアクトで関数型プログラミングを志向するのか
関数型プログラミングとオブジェクト指向プログラミングの長所と短所、およびリアクトで関数型を選択した理由についての探求の記録
コメント
入力したパスワードは秘密コメントの閲覧、修正、削除に使われます。
関数型プログラミングとオブジェクト指向プログラミングの長所と短所、およびリアクトで関数型を選択した理由についての探求の記録
入力したパスワードは秘密コメントの閲覧、修正、削除に使われます。
リアクト(React)は最近数年間、関数型プログラミング(Functional Programming, FP) を志向するようになりました。
単純なUIコンポーネントから複雑なアプリケーション構造に至るまで、関数型アプローチの利点を積極的に活用しています。
今回の記事では、リアクトで関数型プログラミングを志向する理由とそれぞれの根拠を体系的に整理してみたいと思います。
関数型プログラミングの核心は「純粋関数(ピュアファンクション)」です。純粋関数とは、入力が同じであれば常に同じ出力を返し、外部状態を変更しない関数を指します。
クラス型コンポーネントのテスト:
関数型コンポーネント:
関数型コンポーネントはロジックとレンダリングを分離でき、各々を独立してテストできます。
純粋関数の例:
関数型プログラミングではデータは不変(イミュータブル)に扱うのが基本です。
不変性を守るとリアクトの最適化ツールが非常に効果的に動作します:
React.memoの効率的な動作:
useMemoとuseCallbackの正確な依存性検出:
間違った方法 vs 正しい方法:
不変性を守ると参照比較(===)のみで状態変化の検出が可能になります。
深い比較 vs 参照比較:
リアクトは状態が変わったか確認する際、Object.is()(ほぼ===と同じ)を使用します:
このように不変性を維持すれば、複雑なデータ構造でも変化検知がO(1)時間で行われ、性能上大きな利点を得ることができます。
メモイゼーション(Memoization) は同じ入力に対して計算を繰り返さずにキャッシュされた値を返す最適化技法です。関数型プログラミングは純粋関数(pure function) を前提とするため、メモイゼーションが非常に効果的です。
、 のようなフックもこの概念に基づく純粋でない関数はメモイゼーションしにくい:
このように関数型の「不変性 + 純粋性」はメモイゼーションの前提条件と一致するため、両方の概念は互いを強化します。
関数型コンポーネントとフック(useState, useReducer)を活用することで状態管理コードが簡潔になります。
このようにするとUIレンダリングロジックとデータロジックを明確に分離でき、可読性が高まります。
リアクトが関数型プログラミングを採用する理由は、コードの明確さ、テストの容易さ、最適化と変化検知の効率性のためです。
関数型パラダイムはコンポーネントをモジュール化し、複雑度を制御するのに効果的な戦略です。
私の主観的な判断です。
フロントエンド(React) はUIレンダリング、状態変化検知、最適化、テストなどを効率良く行うために関数型パラダイムが適しているようです。
一方、バックエンド(Node.js, Javaなど) では性能問題やリソース管理、プロセスフロー制御などでオブジェクト指向(OOP) が実用的であると思います。
| 区分 | フロントエンド (React など) | バックエンド (Node.js, Java など) |
|---|---|---|
| 主要考慮要素 | UIレンダリング最適化、状態変化追跡 | 性能、リソース管理、トランザクションフロー |
| 適したパラダイム | 関数型プログラミング | オブジェクト指向プログラミング |
| 長所 | テストが容易、不変性で状態変化追跡、コンポーネント再利用性 | クラスベース構造で責任の明確化、状態維持、接続中心の設計、複雑なロジックの構造化容易 |
| 短所 | 複雑な演算フローには不便、無分別なフックの乱用で可読性低下 | テスト時の状態分離が難しい、再レンダリング最適化の構造は複雑 |
class UserProfile extends React.Component {
state = { name: '', loading: false };
async componentDidMount() {
this.setState({ loading: true });
const user = await fetchUser(this.props.userId);
this.setState({ name: user.name, loading: false });
}
render() {
return <div>{this.state.loading ? 'Loading...' : this.state.name}</div>;
}
}
// テスト時にライフサイクルと状態を全て考慮する必要があるfunction UserProfile({ userId }) {
const { user, loading } = useUser(userId);
if (loading) return <div>Loading...</div>;
return <div>{user.name}</div>;
}
// テスト時にpropsのみ確認すれば良い (フックは別途テスト)function formatPrice(price, currency = 'USD') {
return `${currency} ${price.toFixed(2)}`;
}
// 入力だけで結果を予測可能
expect(formatPrice(10.5)).toBe('USD 10.50');
expect(formatPrice(20, 'KRW')).toBe('KRW 20.00');// 不変性を守る場合
const TodoItem = React.memo(({ todo, onToggle }) => (
<div onClick={() => onToggle(todo.id)}>
{todo.text} {todo.completed ? '✓' : '○'}
</div>
));
// propsが参照上同じであれば再レンダリングされませんfunction TodoList({ todos, filter }) {
// todos配列が新たに生成される時のみフィルタリング再実行
const filteredTodos = useMemo(() =>
todos.filter(todo => todo.category === filter),
[todos, filter]
);
// 依存性が変わらなければ関数再生成されません
const handleToggle = useCallback((id) =>
setTodos(prev => prev.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)),
[]
);
return <div>{/* ... */}</div>;
}// ❌ 直接修正 - 最適化ツールが変化を検知できません
function badUpdate() {
const newTodos = todos;
newTodos[0].completed = true;
setTodos(newTodos); // 参照が同じなので再レンダリングされません
}
// ✅ 新しいオブジェクト生成 - 最適化ツールが正確に動作
function goodUpdate() {
setTodos(prev => prev.map((todo, index) =>
index === 0 ? { ...todo, completed: true } : todo
));
}// ❌ 深い比較 - すべての属性を再帰的に確認(遅い)
function deepEqual(obj1, obj2) {
// オブジェクトのすべてのkey-valueを比較する必要があります
return JSON.stringify(obj1) === JSON.stringify(obj2);
}
// ✅ 参照比較 - メモリアドレスのみ確認(速い)
function shallowEqual(obj1, obj2) {
return obj1 === obj2; // 一度の比較で終了
}function Counter() {
const [count, setCount] = useState(0);
const [user, setUser] = useState({ name: 'John', age: 25 });
// ✅ 原始値は自動的に不変性保証
const increment = () => setCount(prev => prev + 1);
// ✅ オブジェクトは新たに生成しないと変化検知されません
const updateAge = () => setUser(prev => ({ ...prev, age: prev.age + 1 }));
// ❌ このようにすると変化が検知されません
const wrongUpdate = () => {
user.age += 1;
setUser(user); // 同じ参照なので再レンダリングされません
};
}function DataTable({ items }) {
// itemsが参照上同じならソートを再度行いません
const sortedItems = useMemo(() => {
console.log('ソート実行!'); // いつ実行されるか確認可能
return [...items].sort((a, b) => a.name.localeCompare(b.name));
}, [items]);
return (
<table>
{sortedItems.map(item => (
<tr key={item.id}>
<td>{item.name}</td>
<td>{item.price}</td>
</tr>
))}
</table>
);
}useCallbackfunction ProductList({ products, searchTerm }) {
// searchTermやproductsが変わらない限りフィルタリングを再実行しません
const filteredProducts = useMemo(() => {
console.log('フィルタリング実行!');
return products.filter(p =>
p.name.toLowerCase().includes(searchTerm.toLowerCase())
);
}, [products, searchTerm]);
return <div>{filteredProducts.map(/* ... */)}</div>;
}// ❌ 純粋ではない - 外部状態に依存
let discount = 0.1;
function calculatePrice(price) {
return price * (1 - discount); // 外部変数参照
}
// ✅ 純粋 - 全ての入力がパラメータで渡される
function calculatePrice(price, discount = 0) {
return price * (1 - discount);
}function todoReducer(state, action) {
switch (action.type) {
case 'ADD_TODO':
return [...state, { id: Date.now(), text: action.text, completed: false }];
case 'TOGGLE_TODO':
return state.map(todo =>
todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
);
case 'DELETE_TODO':
return state.filter(todo => todo.id !== action.id);
default:
return state;
}
}
function TodoApp() {
const [todos, dispatch] = useReducer(todoReducer, []);
return (
<div>
<button onClick={() => dispatch({ type: 'ADD_TODO', text: 'New Todo' })}>
Add Todo
</button>
{/* ... */}
</div>
);
}function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
const saved = localStorage.getItem(key);
return saved ? JSON.parse(saved) : initialValue;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
// さまざまなコンポーネントで再利用可能
function Settings() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
const [language, setLanguage] = useLocalStorage('language', 'ko');
return <div>{/* 設定UI */}</div>;
}アイデア選定からシニアフィードバックを経てMVP実装まで、プロジェクトの土台を築く過程で経験した試行錯誤と技術的挑戦の記録
ベクトルの長さを1にする「正規化」の本質を、ゲームの移動ロジックやBlenderの「Apply Scale」の事例から紐解きます。
6週間のグループスプリントを通じて学んだ設計の試行錯誤、協業のノウハウ、そしてシニアからのフィードバックによる成長の記録
Boostcampメンバーシップで経験した10週間の学習スプリントを振り返り、技術的な学び、設計の悩み、燃え尽き、そしてAI活用についてまとめた記録。
Boostcamp Challenge 期間の振り返り。毎日のミッション、CS学習、ピアフィードバック、チーム活動、そして AI と共に成長する学習方法についてまとめました。
関数型プログラミングとオブジェクト指向プログラミングの長所と短所、およびリアクトで関数型を選択した理由についての探求の記録
ネイバー・ブーストキャンプ Web・Mobile 10期 Basic 受講と問題解決力テストの体験記。
OSCCA マスター期間中に Githru プロジェクトで行った UI 改善、Issue 提案、Pull Request の経験をまとめた記録。
OSCCA チャレンジ期間に Githru プロジェクトへ参加し、Issue 提案や初めての Pull Request を経験した記録。
Open Source Contribution Academy に応募するまでの過程と Githru VSCode Extension を実際に試した感想をまとめた記録。
마지막 아티클까지 모두 확인했습니다.