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,执行相应的生命周期,可以对逻辑进行更细粒度的抽象,更好的复用逻辑。

加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。