Hook は React 16.8 の新機能です。これにより、クラスを作成せずに state やその他の React 機能を使用できます。
カスタム Hook を使用することで、コンポーネントのロジックを再利用可能な関数に抽出できます。
Hook の紹介#
React 16.8 で新たに追加された Hook 機能です。これにより、クラスを作成せずに state やその他の React 機能を使用できます。
カスタム Hook を使用することで、コンポーネントのロジックを再利用可能な関数に抽出できます。
カスタム Hook の実践#
React のクラスコンポーネントを使用していたとき、私は次のような問題に直面しました:フロントエンドのページネーションを持つテーブル表示ページで、ページネーションデータが URL に同期されていないため、ページをリフレッシュしたり詳細ページに移動して戻ったりすると、前のページのデータが失われ、最初のページのデータが表示され、ユーザー体験が非常に悪くなります。
Hook が登場する前は、この問題を解決するのは非常に面倒でした。考え方は、Table の onChange イベントをリッスンしてページネーションパラメータとフィルターパラメータを取得し、history.replace を呼び出して URL に同期するというものでした。解決策自体は非常にシンプルですが、再利用できず、各コンポーネントにはビジネスに無関係なコードが大量に含まれていました。
Hooks の登場まで待ちました。その時、umi が umi-hooks をオープンソースにしたのを見て、公式のカスタム Hook の実装があるかどうかを確認するために issue を提出しました:
https://github.com/alibaba/hooks/issues/232
しばらくして、Hook についての理解が深まるにつれて、Hook を使用して実装するアイデアが突然浮かび、概ね以下のようなコードになりました:
// useQuery.js
import { useEffect, useState } from "react";
import { useHistory } from "react-router-dom";
import qs from "qs";
export const useQuery = (initQuery = {}) => {
const history = useHistory();
const url = history.location.search;
const params = qs.parse(url.split("?")[1]);
const [query, setQuery] = useState({
offset: 0,
...initQuery,
...params
});
useEffect(() => {
history.replace(`?${qs.stringify(query)}`);
}, [query]);
return [query, setQuery];
};
使用方法:
import React, { useEffect, useState } from "react";
import { Layout, Button, Table, Input } from "antd";
import "./index.less";
import { doctorService } from "../../services/doctor.service";
import { useQuery } from "../../common/useQuery";
const { Search } = Input;
const DoctorList = () => {
const [doctorList, setData] = useState([]);
const [loading, setLoading] = useState(false);
const [query, setQuery] = useQuery({
search: ""
});
const columns = [
{
title: "番号",
dataIndex: "doctor_id",
key: "id",
defaultSortOrder: "descend",
sorter: (a, b) => a.doctor_id - b.doctor_id
},
{
title: "名前",
dataIndex: "name",
key: "name"
},
{
title: "病院",
dataIndex: "hospital",
key: "hospital"
},
{
title: "地域",
dataIndex: "zone",
key: "zone"
},
{
title: "販売",
dataIndex: "sales",
key: "sales"
},
{
title: "操作",
dataIndex: "action",
key: "action",
render: (_, record) => {
return (
<Button href={`#/doctor/${record.guid}${record.doctor_id}/patient`} type="primary">
患者を表示
</Button>
);
}
}
];
useEffect(() => {
setLoading(true);
doctorService.getDoctorList({ company_id: 1 }).then(res => {
setData(res ? res.doctor : []);
setLoading(false);
});
}, []);
return (
<Layout>
<div className="search">
<Search
defaultValue={query.search}
placeholder="名前または病院を入力して検索"
onSearch={val => {
setQuery({
...query,
search: val
});
}}
style={{ width: 200 }}
/>
<Button href="#/doctor/add" type="primary">
医者を追加
</Button>
</div>
<Table
loading={loading}
dataSource={
query.search
? doctorList.filter(
item => new RegExp(query.search).test(item.name) || new RegExp(query.search).test(item.hospital)
)
: doctorList
}
columns={columns}
rowKey="doctor_id"
pagination={{ defaultPageSize: 20, current: Number(query.offset) }}
onChange={(pagination, filters, sorter) => {
setQuery({ ...query, offset: pagination.current });
}}
/>
</Layout>
);
};
export default DoctorList;
上記からわかるように、useQuery のコードは非常にシンプルです。useQuery は初期状態としてオブジェクトを受け取り、useState を使用して内部状態を保存します。useQuery の戻り値の中の setQuery は query 状態を変更でき、query の変化は useEffect で監視され、query が変化するたびに history.replace を使用して URL に同期します。これにより、ページが切り替わるとき、次のページで history.goBack () を使用して戻ると、前のページが再レンダリングされ、useQuery が再初期化され、初期化時に URL から query パラメータを取得し、query state を初期化します。外部のコンポーネントも新しい query を取得できるため、検索やページネーションに応じたレンダリングが可能になります。
高階コンポーネントの実装#
Hook を使用して実装した後、クラスコンポーネントでも高階コンポーネントを使用して同様の機能を実現できることに気付きました。以下は簡単な実装です:
import { React } from "react";
import qs from "qs";
export const withQuery = (initQuery = {}) => (Comp) => {
constructor(props) {
const url = props.history.location.search;
const params = qs.parse(url.split("?")[1]);
this.state = {
offset: 0,
...initQuery,
...params
}
}
changeQuery = (query) => {
this.setState({
...query,
}, () => {
this.history.replace(`?${qs.stringify(query)}`);
})
}
render() {
const {query} = this.state;
return <Comp {...this.props} query={query} setQuery={this.changeQuery} />
}
}
以下は withQuery の使用例で、部分コードを省略しています:
class List extends React.Component {
render() {
const {query, setQuery} = this.props;
return (
<Layout>
<div className="search">
<Search
defaultValue={query.search}
placeholder="名前または病院を入力して検索"
onSearch={val => {
setQuery({
...query,
search: val
});
}}
style={{ width: 200 }}
/>
<Button href="#/doctor/add" type="primary">
医者を追加
</Button>
</div>
<Table
loading={loading}
dataSource={
query.search
? doctorList.filter(
item => new RegExp(query.search).test(item.name) || new RegExp(query.search).test(item.hospital)
)
: doctorList
}
columns={columns}
rowKey="doctor_id"
pagination={{ defaultPageSize: 20, current: Number(query.offset) }}
onChange={(pagination, filters, sorter) => {
setQuery({ ...query, offset: pagination.current });
}}
/>
</Layout>
)
}
}
export default withQuery({search: ""})(List);
カスタム Hook と高階コンポーネントの実装を比較すると:
- カスタム Hook と高階コンポーネントの両方がロジックの再利用を実現できます。
- 両者は内部で props を読み取り、自分の state を保存し、対応するライフサイクルを実行できます。コアの原理はクロージャです。
- カスタム Hook の使用はより直感的で、高階コンポーネントはコンポーネントをネストする必要があり、コンポーネント名の重複や可読性の低下の問題があります。
- 高階コンポーネントは props や context を介して外部データを受け取ることしかできませんが、カスタム Hook は props や他のカスタム Hook を介して外部入力データを受け取ることができ、データソースがより明確です。
まとめ#
Hooks の登場により、関数型コンポーネントに状態やライフサイクルの使用がもたらされ、カスタム Hook を使用することで状態を保存し、props を読み取り、対応するライフサイクルを実行でき、ロジックをより細かく抽象化し、ロジックの再利用をより良く行うことができます。