はじめに
Reactが好まれる理由の一つとしてただの関数としてかけるからというのがあります。Reactの関数コンポーネントは関数のように合成できます。React hooksもただの関数ですので合成できます。簡単な例で説明できたら分かりやすいのではないかと思い、書いてみます。
普通のJavaScript
関数がない(一つだけ)の場合は、例えば次のようになります。
const main = (args) => {
const a = args[0] * 2;
const b = args[1] * 2;
console.log([a, b]);
return a + b;
};
関数は、再利用性を増すために使うことができます。
const double = (x) => x * 2;
const main = (args) => {
const a = double(args[0]);
const b = double(args[1]);
console.log([a, b]);
return a + b;
};
また、たった一度しか使わない関数であっても、名前をつけて読みやすくするために関数化する場合があります。
const double = (x) => x * 2;
const printForDebug = (x) => console.log(x);
const main = (args) => {
const a = double(args[0]);
const b = double(args[1]);
printForDebug([a, b]);
return a + b;
};
簡単な例ですが、これが普通のJavaScriptの場合です。
Reactコンポーネント
ReactコンポーネントはReactNodeを返す関数として書くことができます。JSXで書くことが多いので、それにならうと例えば次のようになります。
const App = () => {
return (
<div>
<h1>Hello</h1>
<h2>World</h2>
</div>
);
};
これを関数に分割して合成するには次のように書けます。
const Title = () => <h1>Hello</h1>;
const Subtitle = () => <h2>World</h2>;
const App = () => {
return (
<div>
{Title()}
{Subtitle()}
</div>
);
};
これは完全に同じ結果を生み出します。ただの関数ですから。
しかし、このパターンはあまり見たことがないでしょうし、実際非推奨です。推奨される書き方は、次のようになります。
const Title = () => <h1>Hello</h1>;
const Subtitle = () => <h2>World</h2>;
const App = () => {
return (
<div>
<Title />
<Subtitle />
</div>
);
};
見慣れた書き方かと思われます。こちらが推奨される理由は、TitleやSubtitleの関数を呼び出すタイミングをReactが制御できるからです。
<Title />
は createElement(Title, null)
と同等です。(執筆時点) 参照
この書き方は、Reactコンポーネントを関数合成する際の制約とも言えます。関数として直接合成するのではなく、createElementを介して合成することで、Reactのスケジューリングの恩恵を受けることができます。
React hooks
では、React hooksの場合はどうなるでしょうか。同じように簡単な例を考えます。
const App = () => {
const [a, setA] = useState(0);
const incrementA = () => {
setCountA((c) => c + 1);
};
const [b, setB] = useState(0);
const incrementB = () => {
setCountB((c) => c + 1);
};
useEffect(() => {
console.log([a, b]);
});
return (
<div>
{a} <button onClick={incrementA}>+1</button>
{b} <button onClick={incrementB}>+1</button>
</div>
);
};
共通している処理があるので関数に切り出してカスタムフックにしてみましょう。
const useCount = (initialCount) => {
const [count, setCount] = useState(initialCount);
const increment = () => {
setCount((c) => c + 1);
};
return [count, increment];
};
const App = () => {
const [a, incrementA] = useCount(0);
const [b, incrementB] = useCount(0);
useEffect(() => {
console.log([a, b]);
});
return (
<div>
{a} <button onClick={incrementA}>+1</button>
{b} <button onClick={incrementB}>+1</button>
</div>
);
};
場合によっては、共通処理でなくてもカスタムフックにすることで見通しが良くなるかもしれません。
const usePrintForDebug = (x) => {
useEffect(() => {
console.log(x);
});
};
const App = () => {
const [a, incrementA] = useCount(0);
const [b, incrementB] = useCount(0);
usePrintForDebug([a, b]);
return (
<div>
{a} <button onClick={incrementA}>+1</button>
{b} <button onClick={incrementB}>+1</button>
</div>
);
};
Reactコンポーネントの関数合成に制約があったように、React hooksにおける制約もあります。 Rules of Hooks に詳しく書かれていますが、この制約の範囲内でしかカスタムフックは作れないことになります。
React hooksの制約は直感的ではないと言えるかもしれませんが、Reactコンポーネントの(緩い)制約も決して直感的とは言えないでしょう。制約の数は少ない上、パターン化されているので迷いは少ないと思いますが。React hooksの制約は eslint plugin でほぼチェックできますので、これを使うことが「パターン」として必須と言えるでしょう。
おわりに
React hooksを使う際にどの程度で関数化すなわちカスタムフック化したらいいかは、一言で答えられない問題かと思います。一言で答えるなら、Reactコンポーネントを分割するように分割したらいいのではないかと言います。つまり答えになっていませんが。私自身は関数は小さくしたい派なので、コンポーネントもカスタムフックも小さく分割して合成することを目指します。この辺りはReactとしての制約はなく柔軟なので、プロジェクトでの方針や規約を決めるといいかもしれません。(コンポーネントが大きすぎるとパフォーマンスに影響は出そうです。カスタムフックが大きすぎてもパフォーマンスには関係なさそう)
コンポーネントの場合はAtomic Designなどの方法論があるのでカスタムフックにもそのような方法論があったり、アレンジして適用できるかもしれません。