banner
他山之石

他山之石

自訂Hook在React中的實踐

Hook 是 React 16.8 的新增特性。它可以讓你在不編寫 Class 的情況下使用 state 以及其他的 React 特性。

image

透過自訂 Hook,可以將元件邏輯提取到可重用的函數中。

Hook 介紹#

React 16.8 新增 Hook 特性。它可以讓你在不編寫 Class 的情況下使用 state 以及其他的 React 特性。

透過自訂 Hook,可以將元件邏輯提取到可重用的函數中。

實踐自訂 Hook#

曾經使用 React 的 Class 元件的時候,我遇到這樣一個問題:在一個前端分頁的表格展示頁面中,分頁資料沒有同步到 URL 上,當刷新頁面或者進入詳情頁再後退的時候,前一個分頁的資料丟失,會導致展示為第一頁的資料,用戶體驗很不好。

在 hook 還未出現的時候,這個問題解決起來是很繁瑣的,思路大概是監聽 Table 的 onChange 事件,獲取分頁參數和篩選參數,然後調用 history.replace 同步到 URL,解決方法其實很簡單,但是無法重用,每個元件中都會充斥大量與業務無關的程式碼。

直到 Hooks 的出現。當時看到 umi 開源了 umi-hooks, 於是提交了一個 issue, 看會不會有官方的自訂 hook 實現:

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 實現後,我發現在 Class 元件中,使用高階元件也能實現這種功能,下面是一個簡單的實現:

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,執行相應的生命週期,可以對邏輯進行更細粒度的抽象,更好的重用邏輯。

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。