はじめに
Reactではコンポーネントを純粋関数として扱うことが推奨されています。
純粋関数とmutable,imutableという言葉は非常に密接な関係にあって、これを理解しておかないとコンポーネントを扱っていく上でとてつもなくつまづくことになります。
公式ドキュメントでもmutableな操作ではなくimutableな操作を推奨するような文章がでてきます。
では純粋関数とはなんでしょうか??
JavaScriptの純粋関数
1. 純粋関数とは?
純粋関数は、以下の特徴を持つ関数です:
- 同じ入力に対して常に同じ出力を返す
- 関数の外部に影響を与えない(副作用がない)
つまり、純粋関数は予測可能で、テストしやすく、バグが少ないコードを書くのに役立ちます。
2. 実際のコード例
純粋関数
function add(a, b) {
return a + b;
}
console.log(add(2, 3)); // 常に5を返す
このadd
関数は純粋関数です。なぜなら:
- 同じ入力(2と3)に対して、常に同じ出力(5)を返す
- 関数の外部に影響を与えない
純粋関数ではない
let total = 0;
function addToTotal(value) {
total += value;
return total;
}
console.log(addToTotal(5)); // 5
console.log(addToTotal(5)); // 10
このaddToTotal
関数は純粋関数ではありません。なぜなら:
- 同じ入力(5)に対して、異なる出力を返す
- 関数の外部の変数(
total
)を変更している
3. 純粋関数の利点
- テストしやすい: 入力と出力が明確なので、テストが簡単です。
- 予測可能: 同じ入力に対して常に同じ出力を返すため、動作が予測しやすいです。
- バグが少ない: 外部の状態に依存しないため、バグが発生しにくいです。
- 並行処理に適している: 副作用がないため、並行して実行しても安全です。
4. 純粋関数を書くためのコツ
- 入力を通じてのみデータを受け取る: グローバル変数や外部の状態に依存しないようにしましょう。
- 新しいオブジェクトを返す: 既存のオブジェクトを変更する代わりに、新しいオブジェクトを作成して返しましょう。
- 副作用を避ける: コンソールへの出力、ファイルの書き込み、データベースの更新などの副作用は避けましょう。
5. 実践例:配列の操作
純粋関数を使用した例:
function addItem(arr, item) {
return [...arr, item];
}
const fruits = ['apple', 'banana'];
const newFruits = addItem(fruits, 'orange');
console.log(fruits); // ['apple', 'banana']
console.log(newFruits); // ['apple', 'banana', 'orange']
このaddItem
関数は純粋関数です:
- 元の配列を変更せず、新しい配列を返している
- 同じ入力に対して常に同じ出力を返す
純粋関数ではない例:
function addItem(arr, item) {
arr.push(item);
return arr;
}
const fruits = ['apple', 'banana'];
addItem(fruits, 'orange');
console.log(fruits); // ['apple', 'banana', 'orange']
このaddItem
関数は純粋関数ではありません:
- 元の配列を直接変更している(副作用がある)
6. Reactと純粋関数
Reactは、ユーザーインターフェースを構築するためのJavaScriptライブラリです。Reactの設計思想は純粋関数の概念と密接に関連しています。
純粋な関数コンポーネントの例
function Greeting({ name }) {
return <h1>Hello, {name}!</h1>;
}
このGreeting
コンポーネントは純粋関数です:
- 同じ
props
(この場合はname
)に対して、常に同じJSX要素を返します。 - 外部の状態を変更しません。
Reactにおける純粋関数の利点
-
予測可能性: 純粋なコンポーネントは、与えられた
props
に基づいて常に同じ出力を生成するため、動作が予測しやすくなります。 -
パフォーマンスの最適化: Reactは純粋コンポーネントの性質を利用して、不要な再レンダリングを避けることができます。
-
テストの容易さ: 純粋コンポーネントは、特定の入力(props)に対する出力(JSX)をテストするだけで済むため、ユニットテストが簡単です。
副作用の扱い
純粋関数の原則に従いつつ、実際のアプリケーションでは副作用(APIコールやDOMの操作など)が必要になることがあります。Reactでは、useEffect
フックを使用してこれらの副作用を管理します。
import React, { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
// 副作用(APIコール)をuseEffect内で行う
fetch(`https://api.example.com/users/${userId}`)
.then(response => response.json())
.then(data => setUser(data));
}, [userId]);
if (!user) return <div>Loading...</div>;
return <div>{user.name}</div>;
}
この例では:
-
UserProfile
コンポーネント自体は純粋関数として振る舞います。 - 副作用(APIコール)は
useEffect
内で管理され、コンポーネントのレンダリングロジックから分離されています。
Reactにおける状態管理と純粋性
Reactでは、useState
フックを使用したコンポーネントの状態管理がよく使用されますが、
状態の更新は純粋関数の原則に従って行われることが推奨されています。
function Counter() {
const [count, setCount] = useState(0);
// 良い例:純粋関数的なアプローチ
const increment = () => setCount(prevCount => prevCount + 1);
// 悪い例:外部の状態に依存
const increment = () => setCount(count + 1);
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
}
この例では、increment
関数が純粋関数的なアプローチを取っています。現在の状態に依存せず、前の状態を引数として受け取り、新しい状態を返しています。
7. イミュータブル操作の重要性
Reactと純粋関数を理解する上でイミュータブル(不変)な操作を意識することは非常に重要です。イミュータブルな操作とは、既存のデータ構造を直接変更せず、新しいデータ構造を作成して変更を表現することを指します。
mutable及びimutableなメソッドの詳細はこちらをご覧ください
なぜイミュータブルな操作が重要か?
-
予測可能性: イミュータブルな操作は、元のデータを変更しないため、副作用が少なく、コードの動作が予測しやすくなります。
-
パフォーマンス最適化: Reactは、オブジェクトの参照が変更されたかどうかで再レンダリングの必要性を判断します。イミュータブルな更新により、この比較が簡単かつ高速になります。
-
デバッグの容易さ: データが予期せず変更されることがないため、バグの追跡が容易になります。
-
並行処理の安全性: イミュータブルなデータは、複数の処理が同時にデータにアクセスしても安全です。
ミュータブルな操作 vs イミュータブルな操作
以下に、ミュータブル(変更可能)な操作とイミュータブル(不変)な操作の比較を示します:
配列の操作例:
// ミュータブルな操作(非推奨)
function addItemMutable(arr, item) {
arr.push(item); // 既存の配列を直接変更
return arr;
}
// イミュータブルな操作(推奨)
function addItemImmutable(arr, item) {
return [...arr, item]; // 新しい配列を作成して返す
}
const fruits = ['apple', 'banana'];
// ミュータブルな使用例
const mutableResult = addItemMutable(fruits, 'orange');
console.log(fruits); // ['apple', 'banana', 'orange'] - 元の配列が変更されている
// イミュータブルな使用例
const immutableResult = addItemImmutable(fruits, 'grape');
console.log(fruits); // ['apple', 'banana'] - 元の配列は変更されていない
console.log(immutableResult); // ['apple', 'banana', 'grape'] - 新しい配列が作成された
オブジェクトの操作例:
// ミュータブルな操作(非推奨)
function updateUserMutable(user, key, value) {
user[key] = value; // 既存のオブジェクトを直接変更
return user;
}
// イミュータブルな操作(推奨)
function updateUserImmutable(user, key, value) {
return { ...user, [key]: value }; // 新しいオブジェクトを作成して返す
}
const user = { name: 'Alice', age: 30 };
// ミュータブルな使用例
const mutableUser = updateUserMutable(user, 'age', 31);
console.log(user); // { name: 'Alice', age: 31 } - 元のオブジェクトが変更されている
// イミュータブルな使用例
const immutableUser = updateUserImmutable(user, 'age', 32);
console.log(user); // { name: 'Alice', age: 31 } - 元のオブジェクトは変更されていない
console.log(immutableUser); // { name: 'Alice', age: 32 } - 新しいオブジェクトが作成された
Reactでのイミュータブルな状態更新
Reactでは、状態(state)の更新にイミュータブルな操作を使用することが重要です:
function TodoList() {
const [todos, setTodos] = useState(['Buy groceries', 'Clean house']);
// イミュータブルな方法で新しいTodoを追加
const addTodo = (newTodo) => {
setTodos(prevTodos => [...prevTodos, newTodo]);
};
// イミュータブルな方法でTodoを削除
const removeTodo = (index) => {
setTodos(prevTodos => prevTodos.filter((_, i) => i !== index));
};
return (
// ... レンダリングロジック
);
}
この例では、addTodo
とremoveTodo
の両方がイミュータブルな方法で状態を更新しています。これにより、Reactは効率的に変更を検出し、必要な部分のみを再レンダリングできます。
まとめ
Reactでは純粋関数の概念が重要な役割を果たしています。コンポーネントを純粋関数として設計することで、予測可能性が高まり、テストが容易になり、パフォーマンスの最適化が可能になります。副作用の管理や状態の更新においても、純粋関数の原則を念頭に置くことで、より堅牢で保守性の高いReactアプリケーションを構築することができますので、初学者の頃から心がけておくといいと思います。