banner
他山之石

他山之石

Practical implementation of custom Hooks in React.

Hooks are a new feature introduced in React 16.8. They allow you to use state and other React features without writing a class.

image

By using custom hooks, you can extract component logic into reusable functions.

Introduction to Hooks#

Hooks are a new feature introduced in React 16.8. They allow you to use state and other React features without writing a class.

By using custom hooks, you can extract component logic into reusable functions.

Practice of Custom Hooks#

I encountered a problem when using React's class components. In a frontend pagination table display page, the pagination data was not synchronized with the URL. When refreshing the page or navigating back from a detail page, the data from the previous pagination would be lost, resulting in the display of the first page's data, which had a poor user experience.

Before the introduction of hooks, solving this problem was cumbersome. The general idea was to listen to the onChange event of the table, get the pagination and filtering parameters, and then use history.replace to synchronize them with the URL. The solution was actually simple, but it couldn't be reused, and each component would be filled with a large amount of code unrelated to the business.

Until the introduction of Hooks. At that time, I saw that umi open-sourced umi-hooks, so I submitted an issue to see if there would be an official implementation of custom hooks:

https://github.com/alibaba/hooks/issues/232

After a while, as I gained a deeper understanding of hooks, I suddenly had an idea to implement it using hooks. The code is roughly as follows:

// 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];
};

Usage:

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;

From the above code, it can be seen that the code for useQuery is quite simple. useQuery receives an object as the initial state and uses useState to save the internal state. The setQuery in the return value of useQuery can modify the query state. The changes in query are monitored by useEffect, and every time there is a change in query, it is synchronized to the URL using history.replace. This way, when the page is switched and the previous page is re-rendered using history.goBack(), useQuery is re-initialized. During initialization, the query parameters are obtained from the URL, and the query state is initialized. The external component can also get the new query and render accordingly based on the search or pagination.

Implementation of Higher-Order Component#

After using Hooks to implement this, I found that in class components, the same functionality can be achieved using higher-order components. Here is a simple implementation:

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} />
  }
}

Here is how withQuery is used, with some code omitted:

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);

Comparing the implementation of custom hooks and higher-order components:

  • Both custom hooks and higher-order components can achieve logic reuse.
  • Both can read props internally, save their own state, and execute the corresponding lifecycle methods. The core principle is closures.
  • The usage of custom hooks is more intuitive, while higher-order components require nesting of components, which can lead to component name conflicts and poor readability.
  • Higher-order components can only receive external data through props or context, while custom hooks can receive external input data through props or other custom hooks, making the data source clearer.

Conclusion#

The introduction of Hooks has brought the use of state and lifecycles to functional components. By using custom hooks, you can save state, read props, and execute the corresponding lifecycles. This allows for more granular abstraction of logic and better reuse of logic.

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.