banner
他山之石

他山之石

ReactにおけるカスタムHookの実践

Hook は React 16.8 の新機能です。これにより、クラスを作成せずに state やその他の React 機能を使用できます。

image

カスタム 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 を読み取り、対応するライフサイクルを実行でき、ロジックをより細かく抽象化し、ロジックの再利用をより良く行うことができます。

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。