— Smartisan Pro 2S in Beijing

preface

The React 16.8 stable version of Hook was released nearly a year ago. There are probably two reasons why hook use is not common. Second, it will cost to fully hook the iterative project, and it is not officially recommended. It happened that hook was fully adopted at the beginning of the new project, which is also the reason for writing this article. The next one is mainly about some practices of custom hook, which may not be the best. I hope my sharing and summary can bring benefits to you who read carefully. Source code here ,,,, online demo.

The body of the

Here are some representative hooks from the project and some of the best practices from the project so far.

🐤 1. HOC to Render Props to hook

Business code is commonly used to implement bidirectional binding, using the above three implementations respectively, as follows:

HOC written

const HocBind = WrapperComponent= >
  class extends React.Component {
    state = {
      value: this.props.initialValue
    };
    onChange = e= > {
      this.setState({ value: e.target.value });
      if (this.props.onChange) {
        this.props.onChange(e.target.value); }};render() {
      const newProps = {
        value: this.state.value,
        onChange: this.onChange
      };
      return <WrapperComponent {. newProps} / >; }};/ / usage
const Input = props= > (
  <>
    <p>HocBind implementation value: the value} {props.</p>
    <input placeholder="input" {. props} / >
  </>
);
const HocInput = HocBind(Input);
<HocInput
  initialValue="init"
  onChange={val= >{ console.log("HocInput", val); }} / >
Copy the code

Render Props writing

// props two parameters initialValue input, onChange output
class HocBind extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      value: props.initialValue
    };
  }
  onChange = e= > {
    this.setState({ value: e.target.value });
    if (this.props.onChange) {
      this.props.onChange(e.target.value); }};render() {
    return (
      <>
        {this.props.children({
          value: this.state.value,
          onChange: this.onChange
        })}
      </>); }}/ / usage
<HocBind
  initialValue="init"
  onChange={val= > {
    console.log("HocBind", val);
  }}
>
  {props= > (
    <>
      <p>HocBind implementation value: the value} {props.</p>
      <input placeholder="input" {. props} / >
    </>
  )}
</HocBind>
Copy the code

Let’s look at hook

// initialValue Specifies the default value
function useBind(initialValue) {
  const [value, setValue] = useState(initialValue || "");
  const onChange = e= > {
    setValue(e.target.value);
  };
  return { value, onChange };
}
/ / usage
function InputBind() {
  const inputProps = useBind("init");
  return (
    <p>
      <p>UseBind implementation value: the value} {inputProps.</p>
      <input {. inputProps} / >
    </p>
  );
}
Copy the code

By comparison, it is found that HOC and render props both invade the code, making the code less readable and elegant. The value exposed inside the component is also hard to get outside. In contrast, the hook writing method is completely decoupled with logic, maximizes the use of scenarios without intruded into the code, and the bidirectional binding value can be obtained at the top of the component. Much more elegant than before. The source code

conclusion

  • Hooks are readable and easy to maintain.
  • Hooks do not invade code and do not create nesting.
  • Hook UI and logic completely split, easier to reuse.

🐤 2. Get rid of duplicate fetch and customize useFetch

Fetch data is basically the most common logic that needs to be encapsulated. Let’s take a look at my first version of useFetch:

function useFetch(fetch, params) {
  const [data, setData] = useState({});

  const fetchApi = useCallback(async() = > {const res = await fetch(params);
    if (res.code === 1) {
      setData(res.data);
    }
  }, [fetch, params]);

  useEffect(() = > {
    fetchApi();
  }, [fetchApi]);

  return data;
}
/ / usage
import { getSsq } from ".. /api";
function Ssq() {
  const data = useFetch(getSsq, { code: "ssq" });
  return <div>Double color ball drawing number: {data.opencode}</div>;
}
// API export method
export const getSsq = params= > {
  const url =
    "https://www.mxnzp.com/api/lottery/common/latest?" + objToString(params);
  return fetch(url).then(res= > res.json());
};
Copy the code

Results: CPU burst 💥, browser into an infinite loop, think about it, why?

To fix the bug, change the call method:

.const params = useMemo(() = > ({ code: "ssq" }), []);  
constdata = useFetch(getSsq, params); .Copy the code

🤡 surprise, is the desired result, but, why? (If you don’t know why, check React Hook series 1) because useFetch(getSsq, {code: “SSQ”}); The second argument is relied on by useCallback in useFetch. Render => execute useEffect => call useCallback method => update data => render => call useCallback method to determine whether dependencies have changed to determine whether to skip this execution… For useCallback, params objects are new objects every time, so the rendering process keeps executing, creating an infinite loop. The purpose of useMemo is to cache params for you and return an Memoized value. When the useMemo dependency value does not change, Memoized is the same, so useCallback will skip this execution.

You think that’s it?

Eerie smile 😎😜, every time I use useFetch, I need to use useMemo to wrap params, which is not elegant at all, and then change it?

Problem to solve: How to keep params constant while remaining unique?

Const data = useFetch(getSsq, json.stringify ({code: Parse (params), 🤩 Very clever. But there seems to be something wrong, if this is seen by the boss, the boss: “Emmmm, you are still not elegant, although the problem is solved, then change? “And I said,” HMM!! “.

UseState, yes, it can cache params that pass through it. When it does not change, useCallback and useEffect consider it unchanged and skip the callback, so useFetch looks like this:

function useFetch(fetch, params) {
  const [data, setData] = useState({});
  const [newParams] = useState(params);
  const fetchApi = useCallback(async() = > {console.log("useCallback");
    const res = await fetch(newParams);
    if (res.code === 1) {
      setData(res.data);
    }
  }, [fetch, newParams]);

  useEffect(() = > {
    console.log("useEffect");
    fetchApi();
  }, [fetchApi]);

  return data;
}
/ / call
const data = useFetch(getSsq, { code: "ssq" });
Copy the code

👏👏👏 is ecstatic.

Me: “Big man, there seems to be no problem.”

Big guy: “EMMM, I need to update the following parameters, will I fetch data? “

Me: “Huh? ! ? “

Big guy: “You look again?”

Me: “Ok.”

If you want to update params, you should update newParams. If you want to update params, you should update newParams.

function useFetch(fetch, params) {...const doFetch = useCallback(rest= >{ setNewParams(rest); } []);return { data, doFetch };
}
/ / call
const { data, doFetch } = useFetch(getSsq, { code: "ssq" });
console.log("render");
return (
  <div>OpenCode: {data.opencode}<button onClick={()= >DoFetch ({code: "fc3D"})}> fc3D</button>
  </div>
);
Copy the code

🙃🙂🙃🙂 Calm smile.

No, I can’t let the big guy say you are looking at it this time, I must prepare for a rainy day, I must analyze the scene of fetch data:

  • The page is displayed or refreshed for the first time.
  • When the user changes the parameters of the FETCH data.
  • The user clicks Modal to load data, or fetch data only when the request parameter depends on the availability of a data.
  • Without changing the parameters, the user manually clicks the refresh page button.
  • Fetch Data when the page loading.

The third, the fourth, the fifth as expected do not meet, xin kui ah… Almost 🐶 again, so 5 minutes later:

function useFetch(fetch, params, visible = true) { const [data, setData] = useState({}); const [loading, setLoading] = useState(false); const [newParams, setNewParams] = useState(params); const fetchApi = useCallback(async () => { console.log("useCallback"); if (visible) { setLoading(true); const res = await fetch(newParams); if (res.code === 1) { setData(res.data); } setLoading(false); } }, [fetch, newParams, visible]); useEffect(() => { console.log("useEffect"); fetchApi(); }, [fetchApi]); const doFetch = useCallback(rest => { setNewParams(rest); } []); const reFetch = () => { setNewParams(Object.assign({}, newParams)); }; return { loading, data, doFetch, reFetch }; }Copy the code

Finally, the big guy said, this version can meet the needs of the business at present, use it first, emmmm, 🍻🍻🍻. The source code

However, useFetch can also be more robust, without passing in API methods, directly encapsulate fetch parameters and procedures. After writing a series of articles, I plan to encapsulate useFetch wheels based on native FETCH, expecting ing…

🐤 3. Table of CD, user-defined useTable

Let’s look at the code before useTable, if we use Ant-Design.

const rowSelection = {
  selectedRowKeys,
  onChange: this.onSelectChange,
};
<Table
  rowKey="manage_ip"
  pagination={{
    . pagination.total.current: pagination.page,}}onChange={p= > {
    getSearchList({ page: p.current, pageSize: p.pageSize });
  }}
  rowSelection={rowSelection}
  loading={{ spinning: loading.OperationComputeList, delay: TABLE_DELAY }}
  columns={columns}
  dataSource={list}
/>
Copy the code

Wow, similar to the mid-stage system, each page basically has a table, and look very similar, repeat code a little bit, so it began to think how lazy.

First of all, every page of the table basically involves paging, which is repetitive logic. Therefore, usePagination is first conducted to process paging logic to achieve reuse. The input value is the default value, and change is exposed for user operation.

export const defaultPagination = {
  pageSize: 10.current: 1
};
function usePagination(config = defaultPagination) {
  const [pagination, setPagination] = useState({
    pageSize: config.pageSize || defaultPagination.pageSize,
    current: config.page || config.defaultCurrent || defaultPagination.current
  });

  const paginationConfig = useMemo(() = > {
    return {
      ...defaultPagination,
      showTotal: total= >
        ` per page${pagination.pageSize}The article first${pagination.current}Page,${total}`. config,pageSize: pagination.pageSize,
      current: pagination.current,
      onChange: (current, pageSize) = > {
        if (config.onChange) {
          config.onChange(current, pageSize);
        }
        setPagination({ pageSize, current });
      },
      onShowSizeChange: (current, pageSize) = > {
        if(config.onChange) { config.onChange(current, pageSize); } setPagination({ pageSize, current }); }}; }, [config, pagination]);return paginationConfig;
}
Copy the code

The operation logic of the above user is decoupled from the action after change. As total changes dynamically after fetch, it cannot be omitted. Try using it directly in the Pagination component, and there’s no problem.

Similarly, rowSelection, as a common logic, can also be customized as hook according to the above logic:

const { rowSelection, selectedList, selectedRowKey, resetSelection } = useRowSelection(options); // Options are all rowSelection attributes. // rowSelection, selectedList, selectedRowKey are exposed attributes and selected data. // resetSelection deselects all selectionsCopy the code

It looks like this. It’s very simple:

function useRowSelection(options = {}) {
  const [selectedList, setSelectedList] = useState(options.selectedList || []);
  const [selectedRowKey, setSelectedRowKeys] = useState(
    options.selectedRowKey || []
  );
  const rowSelection = useMemo(() = > {
    return {
      columnWidth: "44px". options, selectedList, selectedRowKey,onChange: (selectedRowKeys, selectedRows) = > {
        setSelectedRowKeys(selectedRowKeys);
        setSelectedList(selectedRows);
        if(options.onChange) { options.onChange(selectedRowKeys, selectedRows); }}};// Clear the check
    const resetSelection = useCallback(() = >{ setSelectedList([]); setSelectedRowKeys([]); } []); }, [selectedList, selectedRowKey, options]);return { rowSelection, selectedList, selectedRowKey, resetSelection };
}
Copy the code

Eventually tables will look like this:

const { data = {}, loading, doFetch } = useFetch(getJokes, {
  page: 1
});
const pagination = usePagination({
  total: data.totalCount,
  onChange: (page, limit) = >{ doFetch({ page, limit }); }});const { rowSelection, selectedList, selectedRowKey, resetSelection } = useRowSelection();
const columns = [
  { title: "Joke content".dataIndex: "content" },
  { title: "Update Time".dataIndex: "updateTime"}];console.log("render");
return (
  <Table
    rowKey="content"
    loading={loading}
    pagination={pagination}
    rowSelection={rowSelection}
    columns={columns}
    dataSource={data.list}
    />
);
Copy the code

👎👊👎 is not said good useTable, now also did not see a figure ah, well below begin the origin of useTable.

It is observed that pagination and dataSource depend on the data after fetch, so the fetch process can be put in useTable, and rowSelection only needs to return configuration items. Only columns and rowKey are page-dependent business logic without encapsulation. The default value of useTable is returned. The default value of useTable is returned.

const [tableProps, resetSelection, selectedList, selectedRowKey] = useTable({
  fetch:fetchData
  params: {},
	pagination: {
		// init
		onChange: () = >{... }},rowSelection: {
		// init
		onChange: () = >{... ,}}});<Table rowKey='id' columns={columns} {. tableProps} / >
Copy the code

The table also needs to be able to filter, but the table has an onChange API that exposes all responses to paging search and sorting.

import { useCallback } from "react";
import usePagination, { defaultPagination } from "./use-pagination";
import useFetch from "./use-fetch";
import useRowSelection from "./use-row-selection";

function useTable(options) {
  const { data = {}, loading, doFetch: dofetch, reFetch } = useFetch( options.fetch, { ... defaultPagination, ... options.params } );const tableProps = {
    dataSource: data.list,
    loading,
    onChange: (pagination, filters, sorter, extra: { currentDataSource: [] }) = > {
      if(options.onChange) { options.onChange(pagination, filters, sorter, extra); }}};const { paginationConfig, setPagination } = usePagination({
    total: data.totalCount, ... (options.pagination || {}),onChange: (page, pageSize) = > {
      if(! options.onChange) {if (options.pagination && options.pagination.onChange) {
          options.pagination.onChange(page, pageSize);
        } else{ doFetch({ page, pageSize }); }}}});if (options.pagination === false) {
    tableProps.pagination = false;
  } else {
    tableProps.pagination = paginationConfig;
  }

  const {
    rowSelection,
    selectedList,
    selectedRowKeys,
    resetSelection
  } = useRowSelection(
    typeof options.rowSelection === "object" ? options.rowSelection : {}
  );
  if (options.rowSelection) {
    tableProps.rowSelection = rowSelection;
  }

  const doFetch = useCallback(
    params= > {
      dofetch(params);
      if (params.page) {
        setPagination({
          pageSize: paginationConfig.pageSize,
          current: params.page
        });
      }
    },
    [paginationConfig, setPagination, dofetch]
  );

  return {
    tableProps,
    resetSelection,
    selectedList,
    selectedRowKeys,
    doFetch,
    reFetch
  };
}
export default useTable;

/ / usage
const {
  tableProps,
  resetSelection,
  selectedList,
  selectedRowKeys,
  doFetch,
  reFetch
} = useTable({
  fetch: getJokes,
  params: null.onChange: (pagination, filters, sorter, extra: { currentDataSource: [] }) = > {
  // doFetch({ page: pagination.current, ... filters });
  console.log("onChange", pagination, filters, sorter, extra);
}
// pagination: false
// pagination: true
// pagination: {
// onChange: (page, pageSize) => {
// console.log("pagination", page, pageSize);
// doFetch({ page, pageSize });
/ /}
// },
// rowSelection: false,
// rowSelection: true
// rowSelection: {
// onChange: (rowKey, rows) => {
// console.log("rowSelection", rowKey, rows);
/ /}
// }
});

<Table rowKey="content" columns={columns} {. tableProps} / >
Copy the code

The above can meet all the needs of table in the current business, welcome to step on it.

Conclusion:

  • Don’t try to write business logic to common components (and don’t try to guess what the user will do).
  • Robust components require default values and allow users to change them.

🐤 4. Other

Other tools and functions related to page side effects can be abstracting into hooks, such as use-Observable based on Rxjs, use-interval timer, encapsulation use-localstorage based on localStorage, Use-form based on Form, use-modal based on Modal, and so on.

🐤 5. Summary

Hook true incense 🤡🤡, code readability is improved, more elegant than HOC, Render Props, UI and logic coupling degree is lower, component reuse degree tends to maximize; Of course, Hooks are not a panacea. Complex data management requires tools like Redux, such as Middleware, which Hook does not have, so technology selection needs to be measured from a business perspective. One point is summarized as follows:

  • Generally custom hooks are only responsible for logic, not rendering.
  • The common logic hooks are subdivided as far as possible according to the principle of a single component, and a single hook is only responsible for a single responsibility.
  • Complex calculations can be considered using useCallback, useMemo to optimize.

Welcome to 🤝🤝🤝~

React Hook series

  • React Hook: Learn how to use it once and for all.
  • React Hook series (3) : Remember the precipitation of Hook in the Middle stage project

Guest officer, quality three even! 🤡 🤡 🤡