1. 首页

React Hook 系列(二):自定义hook的一些实践

React Hook 系列(二):自定义hook的一些实践

——Smartisan Pro 2S 摄于·北京

前言

从React 16.8 稳定版hook发布近一年多,使用hook并不普遍,原因可能有两方面: 一、官方并没有完全取代class;二、迭代项目完全hook话需要成本,官方也不推荐。恰巧新项目伊始,就全面采用hook,这也是写这篇文章的原由,接上一篇 ,这篇主要是自定义hook的一些实践, 不一定是最佳,希望我的一点分享总结,能给认真阅读的你带来收益。源码在这,,,在线demo

正文

下面是项目中一些有代表性的hook,目前也是项目中的一些最佳实践。

🐤 1. HOC 到 Render Props 再到 hook

业务代码常用实现双向绑定, 分别用以上三种实现,如下:

HOC写法


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} />; } }; // 用法 const Input = props => ( <> <p>HocBind实现 value:{props.value}</p> <input placeholder="input" {...props} /> </> ); const HocInput = HocBind(Input); <HocInput initialValue="init" onChange={val => { console.log("HocInput", val); }} /> // 前端进阶资源教程 https://www.javascriptc.com/

Render Props写法


// props 两个参数initialValue 输入,onChange输出 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 })} </> ); } } // 用法 <HocBind initialValue="init" onChange={val => { console.log("HocBind", val); }} > {props => ( <> <p>HocBind实现 value:{props.value}</p> <input placeholder="input" {...props} /> </> )} </HocBind> // 前端进阶资源教程 https://www.javascriptc.com/

再看hook写法


// initialValue默认输入 function useBind(initialValue) { const [value, setValue] = useState(initialValue || ""); const onChange = e => { setValue(e.target.value); }; return { value, onChange }; } // 用法 function InputBind() { const inputProps = useBind("init"); return ( <p> <p>useBind实现 value:{inputProps.value}</p> <input {...inputProps} /> </p> ); } // 前端进阶资源教程 https://www.javascriptc.com/

比较发现,HOC和render props方式都会侵入代码,使得代码阅读性下降,也不够优雅,组件内部暴露的value值,在外部也很难拿到, 反观 hook 的写法,逻辑完全解耦,使用场景最大化且不侵入代码,在组件顶层可以拿到双向绑定的值,比之前优雅很多。 源码

总结

  • hook 可读性高,也易于维护。
  • hook 不会侵入代码, 不会造成嵌套。
  • hook UI和逻辑彻底拆分,更容易复用。

🐤 2. 摆脱重复fetch, 自定义useFetch

fetch数据基本是最常见的需要封装逻辑,先看看我第一版的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; } // 用法 import { getSsq } from "../api"; function Ssq() { const data = useFetch(getSsq, { code: "ssq" }); return <div>双色球开奖号码:{data.openCode}</div>; } // api导出方法 export const getSsq = params => { const url = "https://www.mxnzp.com/api/lottery/common/latest?" + objToString(params); return fetch(url).then(res => res.json()); }; // 前端进阶资源教程 https://www.javascriptc.com/

结果: CPU爆表💥,浏览器陷入死循环,思考一下, why?

fix bug开始,更改一下调用方式:


... const params = useMemo(() => ({ code: "ssq" }), []); const data = useFetch(getSsq, params); ... // 前端进阶资源教程 https://www.javascriptc.com/

🤡惊讶,是想要的结果,但是,why?(如果你不知道为什么,欢迎查阅React Hook 系列一),因为调用useFetch(getSsq, { code: "ssq" }); 第二个参数在useFetch中被useCallback依赖,页面的执行过程:render => 执行useEffect => 调用useCallback方法 => 更新data => render => useEffect => 调用useCallback方法 判断依赖是否变化 确定是否跳过这次执行 ... ,对于useCallback 来说 params 对象每次都是新的对象, 所以这个渲染流程会一直执行,造成死循环。useMemo的作用就是帮你缓存params且返回一个memoized的值, 当useMemo的依赖值没有变化,memoized就是不变的,所以useCallback会跳过此次执行。

你以为就这样结束了?

诡异的微笑😎😜,每次在使用useFetch都需要用useMemo包裹params,一点儿也不优雅,再改改?

要解决的问题:如何保持params不变时,保持唯一?

首先想到JSON.stringify,码上const data = useFetch(getSsq, JSON.stringify({ code: "ssq" })), 再见吧,烦人的对象,每当参数不变时他就是个不变的字符串,在fetch传入的时候JSON.parse(params), 🤩好机智。但是好像哪里不对, 这要是被大佬看到,大佬: “emmmm,你这还是不够优雅,虽然问题解决了,再改改?“,我说:”嗯!!!“。

useState, 对就是他, 他可以缓存params,经过他包裹的,当他没有变化时useCallback和useEffect都认为他是不变的,会跳过执行回调,于是乎useFetch变成了以下样子:


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; } // 调用 const data = useFetch(getSsq, { code: "ssq" }); // 前端进阶资源教程 https://www.javascriptc.com/

👏👏👏欣喜若狂。

我: ”大佬, 这样好像没啥问题了“。

大佬:”emmm, 我要更新以下参数,还会fetch数据吗?“

我: ”嗯?!?“

大佬: “你再看看?”

我:”好(怯怯的说,注意读 轻声)“

那好,不就是想更新params吗,更新肯定是用户的操作, 多以暴露更新newParams的方法就OK吧,于是:


function useFetch(fetch, params) { ... const doFetch = useCallback(rest => { setNewParams(rest); }, []); return { data, doFetch }; } // 调用 const { data, doFetch } = useFetch(getSsq, { code: "ssq" }); console.log("render"); return ( <div> 开奖号码:{data.openCode} <button onClick={() => doFetch({ code: "fc3d" })}>福彩3D</button> </div> ); // 前端进阶资源教程 https://www.javascriptc.com/

🙃🙂🙃🙂淡定微笑。

不行,这次不能让大佬说 你在看看吧, 我必须未雨绸缪,fetch数据的场景我必需分析一下:

  • 页面首次进入或刷新。
  • 用户改变fetch数据的参数时。
  • 用户点击modal后加载数据, 或者当请求参数依赖某个数据的有无才会fetch数据。
  • 在不改变参数的情况下,用户手动点击刷新页面按钮。
  • fetch数据时页面loading。

第三、四、五果然不满足,辛亏啊。。。差点又🐶, 于是5分钟后:


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 }; } // 前端进阶资源教程 https://www.javascriptc.com/

最后大佬说,这个版本目前能满足业务的需求,先用用看,emmmm,🍻🍻🍻。源码

但是useFetch还可以封装的更健壮,不需要传入api方法,直接将fetch的参数以及过程封装起来,系列文章写完,计划基于原生fetch封装 useFetch 轮子, 期待ing…

🐤 3. 扯淡的table,自定义useTable

为什么要写这个hook哪, 先看看没有useTable之前的代码,前提我们使用了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} /> // 前端进阶资源教程 https://www.javascriptc.com/

哇,类似中台系统,每个页面基本都有个table,且都长得很相似,重复代码有点多,于是乎开始想如何偷懒。

首先有table的每个页面基本都涉及到分页,都是重复的逻辑,所以先搞个usePagination来处理分页的逻辑达到复用,输入值为默认值,暴露change供用户操作,那么:

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 =>
        `每页 ${pagination.pageSize} 条  第 ${pagination.current}页 共 ${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;
}

以上用户的操作逻辑和change后的动作解耦, total作为fetch后动态变化的,所以不能省略。尝试直接在Pagination组件中使用,也没有问题。

同理rowSelection作为公共的逻辑,也可以按照以上的逻辑,将其自定义成hook:


const { rowSelection, selectedList, selectedRowKey, resetSelection } = useRowSelection(options); // options 为rowSelection的所有属性,可不输入。 // rowSelection, selectedList, selectedRowKey为暴露属性和已选数据。 // resetSelection 取消所有选中

就长这样,很简单:


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); } } }; // 操作完取消选中 const resetSelection = useCallback(() => { setSelectedList([]); setSelectedRowKeys([]); }, []); }, [selectedList, selectedRowKey, options]); return { rowSelection, selectedList, selectedRowKey, resetSelection }; }

最终table用起来可能长这样:


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: "笑话内容", dataIndex: "content" }, { title: "更新时间", dataIndex: "updateTime" } ]; console.log("render"); return ( <Table rowKey="content" loading={loading} pagination={pagination} rowSelection={rowSelection} columns={columns} dataSource={data.list} /> );

👎👊👎不是说好的useTable吗, 现在也没见着个人影啊,好好下面开始useTable的由来。

经过观察pagination和dataSource都依赖fetch后的数据, 所以fetch的过程可以放在useTable中,rowSelection也只需返回值配置项即可,只有columns和rowKey是依赖页面的业务逻辑不需要封装,需要用户操作的只需暴露交给用户,其他的只是返回默认值即可,那useTable 的样子大概出来了:


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

不过 table 还需要能filter,幸好table有个onChange的API 暴露了分页搜索和排序的所有响应,所以:


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; // 用法 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} /> // 前端进阶资源教程 https://www.javascriptc.com/

Js中文网 – 前端进阶资源教程 www.javascriptC.com,typescript 中文手册
专注分享前端知识,你想要的,在这里都能找到

以上可以满足目前业务关于table的所有需求,欢迎来踩。

总结:

  • 不要试图在公共组件写入业务逻辑(也不要试图猜测用户的操作后的相应)。
  • 健壮的组件需要默认值,也允许用户去修改默认值。

🐤 4. 其他

其他与页面副作用相关的工具函数都可以抽象成hook, 例如基于Rxjsuse-observable,定时器 use-interval ,基于localStorage 的封装 use-localStorage,基于Form的use-form, 基于Modal的use-modal 等等。

🐤 5. 总结

hook 真香🤡🤡,代码可读性提高,比HOC、Render Props更优雅, UI和逻辑耦合度更低,组件复用程度趋于最大化;当然Hook也不是万能的,复杂的数据管理还需要类似redux的工具,譬如redux 有 middleware, 而hook没有, 所以技术选型还需从业务的角度去衡量。一点总结如下:

  • 一般自定义 hook 只负责逻辑,不负责渲染。
  • 公共逻辑 hook尽量细分,按照组件的单一原则划分,单一hook只负责单一的职责。
  • 复杂的计算可以考虑用 useCallback, useMemo 去优化。

作者:郭盖_
链接:https://juejin.im/post/5df78ba6f265da33d74428f9

看完两件小事

如果你觉得这篇文章对你挺有启发,我想请你帮我两个小忙:

  1. 关注我们的 GitHub 博客,让我们成为长期关系
  2. 把这篇文章分享给你的朋友 / 交流群,让更多的人看到,一起进步,一起成长!
  3. 关注公众号 「画漫画的程序员」,公众号后台回复「资源」 免费领取我精心整理的前端进阶资源教程

JS中文网是中国领先的新一代开发者社区和专业的技术媒体,一个帮助开发者成长的社区,目前已经覆盖和服务了超过 300 万开发者,你每天都可以在这里找到技术世界的头条内容。欢迎热爱技术的你一起加入交流与学习,JS中文网的使命是帮助开发者用代码改变世界

本文著作权归作者所有,如若转载,请注明出处

转载请注明:文章转载自「 Js中文网 · 前端进阶资源教程 」https://www.javascriptc.com

标题:React Hook 系列(二):自定义hook的一些实践

链接:https://www.javascriptc.com/3811.html

« React Hook的日常使用,创建一个可排序表格组件
打造工业级推荐系统(五):推荐系统冷启动全解析»
Flutter 中文教程资源

相关推荐

QR code