React のカスタムフックの利点をオブジェクト指向の観点で考えてみる
こんにちは。ぶっちーです。
普段は kintone というプロダクトの新機能開発を行っており、最近は、フロントエンドの技術刷新に取り組んでいます。
この技術刷新では、Closure Tools から React への置き換えを行っています。詳しくは、以下の記事をご覧ください。
刷新をする中で、React を書いていくうちに React の設計、特に React Hooks に対する考え方が難しいと感じました。
そこで、React Hooks について学習し、気付いた点があったのでこの記事にまとめます。
命令的 UI と宣言的 UI
技術刷新前の Closure Tools は、class 構文を使用したオブジェクト指向をベースとして、命令的 UI を構築しています。
刷新をする中で、最終的にどのような UI を構築するのかを把握する必要があります。このときに、コードベースから理解をしようとすると、命令的 UI だと、最終的にどのような UI になるのかを理解するまでに様々な処理を追っていく必要があり、理解に時間がかかります。
下記の例は、簡単な Todo リスト画面を命令的 UI で実現した例です。
<head>
<script type="text/javascript">
window.onload = function () {
const input = document.querySelector("input");
const button = document.querySelector("button");
const div = document.querySelector("#todo");
let ul = null;
button.addEventListener("click", function () {
const text = input.value;
input.value = "";
if (!div.querySelector("ul")) {
ul = document.createElement("ul");
div.appendChild(ul);
}
const li = document.createElement("li");
li.textContent = text;
ul.appendChild(li);
if (ul.querySelector("li")) {
div.removeChild(div.querySelector("p"));
}
});
};
</script>
</head>
<body>
<div>
<h1>Todo</h1>
<div id="todo">
<p>Todoはありません</p>
</div>
</div>
<input type="text" />
<button>Add</button>
</body>
これに対して、宣言的 UI だと、最終的にどのような状態になっているのかを把握することが、命令的 UI と比較すると容易に感じます。
宣言的 UI だと、UI としてどのような振る舞いをするのかについて定義されていて、そのために必要な内部ロジックも同じコンポーネント内に記述されています。
下記の例は、上記と同様に、Todo リスト画面を宣言的 UI で実現した例です。
function TodoList() {
const [todos, setTodos] = useState([]);
const [text, setText] = useState('');
const addTodo = () => {
setTodos([...todos, text]);
setText('');
};
return (
<div>
<h1>Todo</h1>
{todos.length === 0 ? (
<p>Todoはありません</p>
) : (
<ul>
{todos.map((todo, index) => (
<li key={index}>{todo}</li>
))}
</ul>
)}
<input type="text" value={text} onChange={(e) => setText(e.target.value)} />
<button onClick={addTodo}>Add</button>
</div>
);
}
コードを書く際に意識としては、今書いているコンポーネントが振る舞いとして提供するインタフェースを先に記述する感覚です。
そして UI を実際に描画するための詳細は仮想 DOM によって、DOM を更新する詳細を意識することなく、UI を構築していくことができます。DOM 更新に関する詳細が React によって隠蔽されていて、公開されているインタフェースを使用している状態です。
カスタムフックの必要性
そして、そのコンポーネントがインタフェースとして提供するものを実現するための適切な変化を表すロジックを記述していく必要があります。
React Hooks は、この変化を記述することに大きく関わる機能で、コンポーネントのライフサイクル内の挙動等を記述していくことができます。
上記を踏まえると、コンポーネントはビューのレンダリングとビューを構築するためのロジックの責務を持ったオブジェクトと捉えることができそうです。
React のドキュメントには以下のように書かれています。
React アプリはコンポーネントで構成されています。コンポーネントとは、独自のロジックと外見を持つ UI(ユーザインターフェース)の部品のことです。コンポーネントは、ボタンのような小さなものである場合も、ページ全体を表す大きなものである場合もあります。
React におけるコンポーネントとは、マークアップを返す JavaScript 関数です。
クイックスタート – React
このクライアントに提供するマークマップを返す JavaScript 関数は、最終的にどのような UI になるのか知っておく必要があります。
そして、最終的なビューを構築するためのロジックの詳細を知っておく必要はなさそうです。
すなわち、ロジックを隠蔽することで UI とロジックが疎結合になり、コンポーネントの保守性が向上すると考えられます。
そして隠蔽の手段として、カスタムフックが有用です。
カスタムフックの役割
カスタムフックは、宣言的 UI をより宣言的に記述することにおいて、非常に重要な役割を担っていると感じました。
React の公式ドキュメントにあるコードの一部を例にカスタムフックの役割を見てます。
まずはカスタムフックを使用していない例です。
function StatusBar() {
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
function handleOnline() {
setIsOnline(true);
}
function handleOffline() {
setIsOnline(false);
}
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
return <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</h1>;
}
次にカスタムフックを利用した例です。
function StatusBar() {
const isOnline = useOnlineStatus();
return <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</h1>;
}
export function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
function handleOnline() {
setIsOnline(true);
}
function handleOffline() {
setIsOnline(false);
}
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
return isOnline;
}
カスタムフックを用いることで、 StatusBar
コンポーネントがどんなデータを意図して使用しているのかが明確になっていると思います。
また、カスタムフックを用いることで、ロジック単独でモジュール化することができるようになっています。
これによって、コンポーネントのロジックを再利用することができるようになり、コンポーネントのコードがより宣言的になります。
React の公式ドキュメントのカスタムフックでロジックを再利用するにおいて、カスタムフックの重要性を示す記述がありました。
- これで、コンポーネント間のロジックの重複が減りました。さらに重要なのは、コンポーネント内のコードが、「オンラインステータスを使用 (use) する」という、何をしたいのかの記述になっているということです。どのようにして実現するのか(ブラウザのイベントに登録する)ではありません。
- ロジックをカスタムフックに抽出することで、外部システムやブラウザ API とのやり取りに関する面倒な詳細を隠蔽することができます。あなたのコンポーネントのコードは、実装方法ではなく意図を表現するようになるのです。
- カスタムフックに抽出することで、データの流れが明示的になります。
カスタムフックによって、コンポーネントがどのようなデータを必要としているのかという「意図」が記述され、コンポーネント本来の責務が凝集されたレンダー関数を作成することができます。
カスタムフックを作る際のオブジェクト指向
刷新当初、このカスタムフックを作る際の考え方がいまいちピンときませんでした。
しかし、改めて考えてみると、カスタムフックを作る際には、オブジェクト指向の設計思想が活きると感じました。
カスタムフックを作るということは、コンポーネントからロジックを分離し、詳細を隠蔽します。すなわち「カプセル化」を行っています。
また、作成するカスタムフックを使用するクライアントに対して、どのような操作を提供するべきかを決定し、必要であればその振る舞いに対する引数を考えます。
そして、引数と返り値に反しないロジックを内部に実装していくことになります。
この考え方はかなりオブジェクト指向におけるクラス設計と近いものがあると感じています。
また、React の公式ドキュメントにも記述がありますが、ロジックとビューの間で責務を分離することで、React が useSyncExternalStore
のような新しい機能を追加したときに、コンポーネントを変更せずにエフェクトを削除できるようになります。つまり、保守性の高いコードを書くことができます。
これは、オブジェクト指向における SOLID の原則の一つである、単一責任の原則を実現した結果、モジュール性の高いコードが実現されていることを表しているように思っています。
そして、モジュール性の向上を図ることは、クラス設計の重要な要素の一つです。
再度 React の公式ドキュメントの参照になりますが、以下のような記述もされています。
良いカスタムフックとは、動作を制約することで呼び出し側のコードをより宣言的にするものである。
カスタムフックでロジックを再利用する
この「動作を制約する」とは、オブジェクト指向における「カプセル化」の考え方に近いものがあると感じました。
ただ、ロジックを隠蔽するためになんでもカスタムフックを作成すると良いというわけではないです。
カスタムフックが表現するエフェクトは、React の世界から「踏み出して」、外部と同期するために使用されるものです。
この意図を持っていないものは、カスタムフックとして隠蔽するのではなく、イベントハンドラとして切り出すなど他の方法を検討した方が良いと考えます。
さいごに
最初は難しいと感じた React における設計の考え方でしたが、改めて考えてみると、今までの知識や経験を生かせる部分がありました。
全てをオブジェクト指向に当てはめるのが良いということではないと思いますが、オブジェクト指向の設計思想を活かすことで、保守性の高いコードを書くことができると感じました。
そのベースに加えて、React の公式ドキュメントに記載されているような、React の基本となる内容を意識することで、より良いコードを書くことができると感じました。
今後も引き続き、メンテナンス性の高いコードを意識しながら、刷新を行っていきたいと思います。
Discussion