React: コンポーネントのロジックをカスタムフックに切り出す ー カウンターの作例で


Reactにフックが採り入れられて、関数コンポーネントに状態をもたせられるようになりました。

「フックとは、関数コンポーネントにstateやライフサイクルといった Reactの機能を「接続する」(hook into)ための関数です」(「要するにフックとは?」)。さらに、フックを独自につくって、コンポーネントからロジックを切り出すこともできます。そうすれば、コンポーネントのコードがすっきり見やすくなるとともに、そのカスタムフックを使い回すこともできるのです。

自分独自のフックを作成することで、コンポーネントからロジックを抽出して再利用可能な関数を作ることが可能です。
(「独自フックの作成」より)

本稿は簡単なカウンターの作例をとおして、フックの役割や考え方について解説します。

Create React Appでアプリケーションのひな形をつくる

まず、Reactアプリケーションのひな形は、Create React Appでつくりましょう。コマンドラインツールでnpx create-react-appにつづけて、アプリケーション名(今回はreact-custom-hook)を打ち込んでください。

npx create-react-app react-custom-hook

アプリケーション名でつくられたディレクトリに切り替えて(cd react-custom-hook)、コマンドyarn startでひな形アプリケーションのページがローカルホスト(http://localhost:3000/)で開くはずです。

useStateとプロパティ(props)でカウンターをつくる

まずは、カスタムフックは用いず、useStateとプロパティ(props)によりカウンターをつくりました(コード001)。

アプリケーションのモジュールsrc/App.jsuseStateで状態変数(count)を定め、関数としてはカウンターの減算(decrement())と加算(increment())が備わっています。それらをプロパティ(counter)として受け取るのが、このあと定めるカウンター表示のコンポーネントCounterDisplayです。

コード001■useStateとプロパティを用いたアプリケーションモジュール

src/App.js
import React, { useState } from 'react';
import CounterDisplay from './CounterDisplay';
import './App.css';

const initialCount = 0;
function App() {
	const [count, setCount] = useState(initialCount);
	const decrement = () => setCount(count - 1);
	const increment = () => setCount(count + 1);
	return (
		<div className="App">
			<CounterDisplay counter={{ count, decrement, increment }} />
		</div>
	);
}

export default App;

カウンター表示のモジュールsrc/CounterDisplay.jsは、アプリケーションから受け取ったプロパティ(counter)により、カウンタの値(counter.count)表示と減算(counter.decrement)・加算(counter.increment)の処理を行います(コード002)。

コード002■カウンター表示のモジュール

src/CounterDisplay.js
import React from "react";

const CounterDisplay = ({ counter }) => {
	return (
		<div>
			<button onClick={counter.decrement}>-</button>
			<span>{counter.count}</span>
			<button onClick={counter.increment}>+</button>
		</div>
	);
}
export default CounterDisplay;

これで簡単なカウンターができ上がりました(図001)。ただカウンターのカウントアップ・ダウンをするだけで、アプリケーションモジュールには、状態を使って何か行うという処理がありません。あとで加わるという想定にして、今回の作例からは省きました。また、CSS(src/index.csssrc/App.css)は、基本的なフォントや余白の設定のみです。確かめたい方は、最後に掲げるCodeSandboxの作例(サンプル001)をご覧ください。

図001■でき上がったカウンター

アプリケーションのロジックをカスタムフックに切り出す

アプリケーションモジュールsrc/App.jsのロジック、つまり状態変数とその処理関数を、このあと定めるカスタムフックに切り出しましょう。

src/App.js
// import React, { useState } from 'react';
import React from 'react';

// const initialCount = 0;
function App() {
	/* カスタムフックに切り出す
	const [count, setCount] = useState(initialCount);
	const decrement = () => setCount(count - 1);
	const increment = () => setCount(count + 1);
	*/

}

つぎのコード003が、カウンターのカスタムフックのモジュール(src/useCounter.js)です。フックの名前はuseではじめるお約束になっています。状態が備えられ、フックも使えるのは、関数コンポーネントと同じです。違いとしては、JSXの要素を返さなくて構いません。戻り値は、カウンターの値(count)と減算(decrement())・加算(incfrement())の関数を収めたオブジェクトとしました。

コード003■カウンターのカスタムフック

src/useCounter.js
import { useState } from 'react';

export const useCounter = (initialCount = 0) => {
	const [count, setCount] = useState(initialCount);
	const decrement = () => setCount(count - 1);
	const increment = () => setCount(count + 1);
	return { count, decrement, increment };
};

カスタムフックを使う

カウンター表示モジュール(src/CounterDisplay.js)はカスタムフック(useCounter)の呼び出しにより、カウンターの状態を操作するための参照(countdecrementおよびincrement)が得られます。もはや、親コンポーネントからプロパティで受け取らなくてよいのです。

src/CounterDisplay.js
import { useCounter } from "./useCounter";

// const CounterDisplay = ({ counter }) => {
const CounterDisplay = () => {
	const { count, decrement, increment } = useCounter();
	return (
		<div>
			{/* <button onClick={counter.decrement}>-</button> */}
			<button onClick={decrement}>-</button>
			{/* <span>{counter.count}</span> */}
			<span>{count}</span>
			{/* <button onClick={counter.increment}>+</button> */}
			<button onClick={increment}>+</button>
		</div>
	);
}

親のアプリケーションモジュール(src/App.js)から、子コンポーネント(CounterDisplay)に渡していたプロパティ(counter)は除いてください。

src/App.js
function App() {
	return (
		<div className="App">
			{/* <CounterDisplay counter={{ count, decrement, increment }} /> */}
			<CounterDisplay />
		</div>
	);
}

カウンター表示(src/CounterDisplay.js)とアプリケーション(src/App.js)モジュールの記述全体は、それぞれつぎのコード004のとおりです。ロジックを切り離したので、ふたつのコンポーネントはともに表示に専念することになりました。作例をCodeSandboxに公開します(サンプル001)。

コード004■ロジックを切り離したコンポーネントモジュール

src/CounterDisplay.js
import React from "react";
import { useCounter } from "./useCounter";

const CounterDisplay = () => {
	const { count, decrement, increment } = useCounter();
	return (
		<div>
			<button onClick={decrement}>-</button>
			<span>{count}</span>
			<button onClick={increment}>+</button>
		</div>
	);
}

export default CounterDisplay;
src/App.js
import React from 'react';
import CounterDisplay from './CounterDisplay';
import './App.css';

function App() {
	return (
		<div className="App">
			<CounterDisplay />
		</div>
	);
}

export default App;

サンプル001■カスタムフックを使ったカウンター


>> CodeSandboxへ