JavaScriptのHigher Order FunctionsとReactのHigher Order Componentsの理解
JavaScriptやReactを学習中でHigher Order Functions、Higher Order Components(HOC)という名称を目にする機会があるかと思います。名称は知っているけどどういうものか実はわからないという人向けにシンプルな例を使いながらHigher Order Functions | Componentsについて説明を行っています。本文書を読み終えると必ず理解は深まっているはずです。
Higher Order FunctionsとHigher Order Components、名称が似ているので同じものではと思われるかもしれませんが同じものではありません。Higer Order Functionsは名前の通り関数を扱うものでHigh Order ComponentsはReactのコンポーネントを扱うものです。しかしコンセプトが似ているので同時に学ぶことで効率よく理解を深めることができます。
目次
Higher Order Functionsとは
Higher Order Functionsは日本語では高階関数といいます。英語のHigher Order Functionsでも日本語の高階関数どちらの名前でも名前だけではどのようなことを表しているのかイメージすることができません。しかし定義は非常にシンプルでHigher Order Functionsは引数に関数を取ることができるまたは関数を戻すことができる関数です。引数に関数を取り、関数を戻す関数も同様に高階関数です。Higher Order Functionsという名称に馴染みがなかったとしても実はmap関数、filter関数、forEach関数を介してHigher Order Functionsを使っています。
map関数の場合
JavaScriptの中でも特にReactでコードを記述している人ならmap関数を使ったことがないという人はいないのではないでしょうか。これまであまり気にしていなかったと思いますがmap関数は引数に関数を取るのでHigher Order Functionsです。下記のようにmap関数の引数に関数(name) => console.log(name)を取っています。
const names = ['John', 'Kevin', 'David'];
names.map((name) => console.log(name));
//結果
John
Kevin
David
アロー関数に慣れていない人であれば上のコードを見てもどこに関数があるのかわからないという人もいるかもしれません。上記のコードは下記のように書き換えることができます。これでmap関数の引数に関数が入っていることがしっかりわかるようになりました。
const names = ['John', 'Kevin', 'David'];
const namefun = function (name) {
console.log(name);
};
names.map(namefun);
//結果
John
Kevin
David
map関数がHigher Order FunctionsであることがわかればHigher Order Functionsもかなり身近に感じされるようになったかと思いますがmap関数を見ただけでは結局Higher Order Functionsが何なのかがわからないと思います。
以下では”引数に関数を取る場合”, “関数を戻す場合”, “引数に関数を取り、関数を戻す場合”の3つのシンプルな例を利用してHiger Order Functionsをどのように記述することができるのか確認していきます。
引数に関数を取る場合
引数に利用する関数callNameを作成します。引数にnameを取り、引数のnameを戻す関数です。
function callName(name) {
return name;
}
次に関数を引数に取ることができるgreeting関数を作成します。第一引数に関数を取りその中で引数で受け取った関数を実行しています。
function greeting(fn, name, word) {
console.log(`${word} ${fn(name)}`);
}
下記のようにgreetingに引数を設定することで実行することができます。作成しておいたcallNameを引数に入れています。引数に関数を使えることを知らなかった人もコードの理解は簡単にできるかと思います。
greeting(callName, 'John', 'Hi');
greeting(callName, 'Kevin', 'Hello');
// 結果
Hi John
Hello Kevin
関数を戻す場合
次は関数を戻すgreeting関数を作成します。関数を引数に取るよりも慣れないと少し違和感があるかもしれません。returnで関数を戻していることに注目してください。
function greeting(word) {
return function (name) {
return `${word} ${name}`;
};
}
greeting関数の引数のwordに文字列を入れて変数hi, helloにそれぞれ保存します。
const hi = greeting('Hi');
const hello = greeting('Hello');
hiとhelloにはgreeting関数で戻された関数が入っているので実行することができます。実行する際の引数はgreeting関数の中で設定したnameに対応します。
console.log(hi('John'));
console.log(hello('Kevin'));
// 結果
Hi John
Hello Kevin
下記のように記述することもできますがHigher Order Fuctionsを理解していなければ下記のコードを見た時に一体何が行われるのかわからないのではないでしょうか。Higher Order Functionsを理解していればgreeting(‘Hi’)を実行したら関数が戻されるのかその戻された関数の引数に’John’を入れていることが想像できます。
console.log(greeting('Hi')('John'));
console.log(greeting('Hello')('Kevin'));
// 結果
Hi John
Hello Kevin
引数に関数を取り、関数を戻す場合
引数を関数に取り、関数を戻すHigher Order Functionsを作成していきます。
引数に利用する関数は先程作成したcallNameを利用します。
function callName(name) {
return name;
}
geetingを下記のように作成します。関数を引数に取り、returnで新しい関数を戻しています
function greeting(fn) {
return function (word, name) {
console.log(`${word} ${fn(name)} `);
};
}
作成したgreeting関数を実行します。引数にはcallNameを入れています。HiとHelloにはgreeting関数で戻された関数が入っています。
const Hi = greeting(callName);
HiとHelloは引数のwordとnameを取ることができるので文字列を入れます。引数に関数を取り、関数を戻すHigher Order Functionsを作成することができました。
Hi('hi', 'John');
Hi('hello', 'Kevin');
// 結果
hi John
hello Kevin
下記のように記述することもできます。
greeting(callName)('hi', 'John');
greeting(callName)('hello', 'John');
// 結果
hi John
hello Kevin
ここまででHigher Order Functionsの理解は進んだと思いますがもう一つ別の例を記述しておきます。今回は引数に利用する関数を2つ用意しました。calculationは変更することなく引数に取る関数によって処理を変えることができます。calculation関数の中ではreturnが2回含まれていることもポイントです。operationの前にreturnがなければ処理を行うことができません。
function add(num1, num2) {
return num1 + num2;
}
function multiple(num1, num2) {
return num1 * num2;
}
function calculation(operation) {
return function (num1, num2) {
return operation(num1, num2);
};
}
const addCal = calculation(add);
const multipleCal = calculation(multiple);
console.log(addCal(6, 3));
console.log(multipleCal(6, 3));
// 結果
9
18
num1, num2の代わりに…argsを利用することも可能です。書き換えると下記のようになりますが結果は同じです。
function add(num1, num2) {
return num1 + num2;
}
function multiple(num1, num2) {
return num1 * num2;
}
function calculation(operation) {
return function (...args) {
return operation(...args);
};
}
const addCal = calculation(add);
const multipleCal = calculation(multiple);
console.log(addCal(6, 3));
console.log(multipleCal(6, 3));
// 結果
9
18
下記のように記述することもできます。
console.log(calculation(add)(6, 3));
console.log(calculation(multiple)(6, 3));
// 結果
9
18
アロー関数で記述
3つのパターンでHigher Order Functionsの作成方法を確認することができました。すべてアロー関数に書き換えることができるので最後の関数のみ書き換えてみます。結果は同じでコードはかなり短くなり以下のように書き換えることができます。
const callName = (name) => name;
const greeting = (fn) => (word, name) => console.log(`${word} ${fn(name)} `);
const Hi = greeting(callName);
const Hello = greeting(callName);
Hi('hi', 'John');
Hi('hello', 'Kevin');
// 結果
hi John
hello Kevin
Higher Order Componentsとは
Higher Order Functionsでは引数に関数を取ることができるまたは関数を戻すことができる関数でした。Higher Order Componentsは関数を引数に取るのではなくコンポーネントを引数に取り、新しいコンポーネントを戻します。Higher Order Componentsを利用することで引数に取ったコンポーネントに外部リソースから取得したデータをpropsを使って追加したり、引数に取ったコンポーネントに機能の拡張を行うことができます。
Higher Order ComponentsについてはReactの旧ドキュメントに記載されています。
Higher Order Componentsの形
最初にHigher Order Compomentsの記述方法を確認しておきます。先程言葉で説明した通り、引数にコンポーネントを取り、returnで新しいコンポーネントを戻しています。
const HOC = (Component) => {
return function (props) {
return <Component {...props} />;
};
};
上記のコードでは新しいコンポーネントを戻すというのが少しわかりにくいかもしれませんが下記のように書き換えることで新しいコンポーネントを戻していることがよりクリアになるかと思います。これがHigher Order Componentsの基本的な記述方法です。
const HOC = (Component) => {
const NewComponent = (props) => {
return <Component {...props} />;
};
return NewComponent;
};
実際に作成したHigher Order Componentsを利用する場合は下記のように記述し新しいコンポーネントを保存して使用します。Enhancedという単語には強化する/高めるといった意味があります。EnhancedComponentという名前をつかっているのはオリジナルコンポーネントをHOCに通すことで機能やデータ追加などなにか強化が行われた新しいコンポーネントが作成されると意味を持っています。
const EnhancedComponent = HOC(OriginalComponent);
ここまでの説明ではコンポーネントを引数にとり新しいコンポーネントを戻すというイメージはつかめたと思いますがHigher Order Componentsをどのように作成するかもわからないと思うので実際にコードを記述することで理解を深めていきます。
オリジナルコンポーネントの作成
Higher Order Componentsの引数で渡すコンポーネントを作成します。postsという名前のpropsを受け取ってリスト表示するPostListコンポーネントを作成しています。
const PostList = ({ posts }) => {
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
};
export default PostList;
HOCの作成
先程記述したHigher Order Componentsの形を元にwithPostsコンポーネント(withPosts.js)を作成します。PostListコンポーネントではpropsのpostsを受け取っているのでwithPostsコンポーネント内ではpropsでpostsを渡しています。
const withPosts = (Component) => {
return function (props) {
return <Component posts={posts} {...props} />;
};
};
export default withPosts;
withPostsコンポーネントでComponentに対してpropsでpostsを渡しているのでpostsデータを取得するコードを追加します。postsデータはJSONPlaceHolderからfetch関数を利用して取得しています。useStateフックでposts変数を定義し、useEffectフックの中で非同期でJSONPlaceHolderからデータを取得しています。postsデータの取得については外部からデータを取得する際の一般的な処理でHOCだからといって特別な処理は行っていません。
import { useState, useEffect } from 'react';
const withPosts = (Component) => {
return function (props) {
const [posts, setPosts] = useState([]);
const getPosts = async () => {
const res = await fetch('https://jsonplaceholder.typicode.com/posts');
const data = await res.json();
setPosts(data);
};
useEffect(() => {
getPosts();
}, []);
return <Component posts={posts} {...props} />;
};
};
export default withPosts;
srcフォルダにあるApp.jsファイルの中でHigher Order Componentsをimportして利用してみます。importしたwithPostsの引数にPostListコンポーネントを渡し戻されるコンポーネントをCurrentPostListに保存して利用しています。
import PostList from './components/PostList';
import withPosts from './components/withPosts';
const CurrentPostList = withPosts(PostList);
function App() {
return (
<div>
<h1>ブログ一覧</h1>
<CurrentPostList />
</div>
);
}
export default App;
ブラウザ上にはJSONPlaceHolderから取得したデータのtitleが表示されます。withPostsからPostListはpostsデータを受け取れていることがわかります。これが非常にシンプルなHOCの例です。
withPostsコンポーネントの中でデータ取得だけではなく機能の追加を行うこともできます。例えばpostsが外部リソースからデータを取得できるまでにブラウザ上にLoading中を表示させたい場合は下記のように記述することで実装することができます。このようにHOCを利用することでPostListコンポーネントが持っていない機能を追加することができます。
const withPosts = (Component) => {
return function (props) {
const [posts, setPosts] = useState([]);
const getPosts = async () => {
const res = await fetch('https://jsonplaceholder.typicode.com/posts');
const data = await res.json();
// console.log(data);
setPosts(data);
};
useEffect(() => {
getPosts();
}, []);
if (!posts) return <p>Loading...</p>;
return <Component posts={posts} {...props} />;
};
};
またpropsを利用してデータを渡すこともできます。CurrentPostListにpropsでtitleを渡します。
import PostList from './components/PostList';
import withPosts from './components/withPosts';
const CurrentPost = withPosts(PostList);
function App() {
return (
<div>
<h1>ブログ一覧</h1>
<CurrentPost title="リスト表示" />
</div>
);
}
export default App;
PostListコンポーネントではCurrentPostListで渡したpropsを受け取ることができます。
const PostList = ({ posts, title }) => {
return (
<>
<h2>{title}</h2>
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</>
);
};
export default PostList;
propsで渡したtitleが表示されます。
テーブル表示のコンポーネント作成
HOCの引数に入れたコンポーネントはリスト表示でしたが表示をテーブル表示に変更するために新たにPostTableコンポーネントを作成します。
PostList.jsファイルを元にulタグのリスト表示からtableタグのテーブル表示へと変更します。
const PostTable = ({ posts, title }) => {
return (
<>
<h2>{title}</h2>
<table border="1">
<thead>
<tr>
<th>タイトル</th>
</tr>
</thead>
<tbody>
{posts.map((post) => (
<tr key={post.id}>
<td>{post.title}</td>
</tr>
))}
</tbody>
</table>
</>
);
};
export default PostTable;
データの取得などの処理はHOCのwithPostsコンポーネントに記述されているのでその他の変更は必要ありません。データ取得など共有部分はHOCで行っているのでコードを効率よく利用することができます。App.jsでは作成したPostTableコンポーネントをimportしてwithPostsの引数に設定を行います。
// import PostList from './components/PostList';
import PostTable from './components/PostTable';
import withPosts from './components/withPosts';
// const CurrentPost = withPosts(PostList);
const CurrentPost = withPosts(PostTable);
function App() {
return (
<div>
<h1>ブログ一覧</h1>
<CurrentPost title="テーブル表示" />
</div>
);
}
export default App;
ブラウザで確認するとリスト表示からテーブル表示に変わったことが確認できます。
Reduxのconnect関数がHOCとは
Reduxのconnect関数ではHigher Order Componentsを利用しています。どの部分がHOCなのか確認します。
connect関数は以下のように記述することができます。ほとんどの人が初めてconnect関数の処理部分を見た時にこれは何をしているの?と疑問を持った箇所ではないでしょうか。下記の処理にHOCが利用されていることを理解できていなくてもReduxを利用することができるので気にしていない人もいるかと思いますがHOCはどの部分でしょう?
export default connect(mapState, mapDispatch)(App)
connect(mapState, mapDispatch)で戻されるのがHigher Order Componentsです。AppのコンポーネントがHOCの引数に入れるコンポーネントで本文書であればPostList, PostTableコンポーネントです。Appには下記のようにpropsを使って受け取るデータを設定しているのでwithPostがPostList, PostTableにpostsデータを渡したようにconnect(mapStateToProps, mapDispatchToProps)で戻されるHOCがcount, increase, decreaseをAppに渡しています。connect関数でRedux storeにアクセスを行いそれらの情報を取得しているようです。
function App({ count, increase, decrease }) {
//略
export default connect(mapStateToProps, mapDispatchToProps)(App)
以下のように記述すれば先程よりもconnect(mapState, mapDispatch)がHOCだということが理解しやすいかもしれません。
const connectApp = connect(mapState, mapDispatch)
export default connectApp(App)
React Hookを利用して書き換え
Reactのカスタムフックを作成して先程記述したHigher Order ComponentsのHOCを利用せず同じ処理を行うことができます。
usePost.jsファイルを作成してJSONPLaceHolderからデータを取得するコードを記述します。
import { useState, useEffect } from 'react';
const usePost = () => {
const [posts, setPosts] = useState([]);
const getPosts = async () => {
const res = await fetch('https://jsonplaceholder.typicode.com/posts');
const data = await res.json();
setPosts(data);
};
useEffect(() => {
getPosts();
}, []);
return posts;
};
export default usePost;
withPostの代わりに作成したカスタムフックusePostを利用します。usePostからはpostsのデータを取りだし、propsでPostTableコンポーネントに渡しています。
import PostTable from './components/PostTable';
import usePost from './components/usePost';
function App() {
const posts = usePost();
return (
<div>
<h1>ブログ一覧</h1>
<PostTable posts={posts} title="テーブル表示" />
</div>
);
}
export default App;
この方法でも先程と同様に一覧がブラウザ上に表示されます。PostTableからPostListに変更するだけでテーブル表示からリスト表示への変更も可能です。
ここまで読み進めるとHigher Order Functions、Higher Order Componentsがどういうものかの理解は進んだと思います。あとは実際に利用されるコードを見て理解を深めていくしかないと思うのでHigher Order FunctionsやHigher Order Componentsを利用しているサードパーティのライブラリに出会ったらコードを読んで理解を深めてください。