——Smartisan Pro 2S 摄于·北京react
从React 16.8 稳定版hook发布近一年多,使用hook并不广泛,缘由可能有两方面: 1、官方并无彻底取代class;2、迭代项目彻底hook话须要成本,官方也不推荐。恰巧新项目伊始,就全面采用hook,这也是写这篇文章的起因,接上一篇 ,这篇主要是自定义hook的一些实践, 不必定是最佳,但愿个人一点分享总结,能给认真阅读的你带来收益。源码在这,,,,在线demo。git
下面是项目中一些有表明性的hook,目前也是项目中的一些最佳实践。github
业务代码经常使用实现双向绑定, 分别用以上三种实现,以下:json
HOC写法redux
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} />
</>
);
<HocInput
initialValue="init"
onChange={val => {
console.log("HocInput", val);
}}
/>
复制代码
Render Props写法api
// 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>
复制代码
再看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> ); } 复制代码
比较发现,HOC和render props
方式都会侵入代码,使得代码阅读性降低,也不够优雅,组件内部暴露的value值,在外部也很难拿到, 反观 hook 的写法,逻辑彻底解耦,使用场景最大化且不侵入代码,在组件顶层能够拿到双向绑定的值,比以前优雅不少。 源码缓存
总结bash
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());
};
复制代码
结果: CPU爆表💥,浏览器陷入死循环,思考一下, why?
fix bug
开始,更改一下调用方式:
...
const params = useMemo(() => ({ code: "ssq" }), []);
const data = useFetch(getSsq, params);
...
复制代码
🤡惊讶,是想要的结果,可是,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" });
复制代码
👏👏👏欣喜若狂。
我: ”大佬, 这样好像没啥问题了“。
大佬:”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>
);
复制代码
🙃🙂🙃🙂淡定微笑。
不行,此次不能让大佬说 你在看看吧, 我必须未雨绸缪,fetch数据的场景我必需分析一下:
第3、4、五果真不知足,辛亏啊。。。差点又🐶, 因而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
};
}
复制代码
最后大佬说,这个版本目前能知足业务的需求,先用用看,emmmm,🍻🍻🍻。源码
可是useFetch
还能够封装的更健壮,不须要传入api方法,直接将fetch的参数以及过程封装起来,系列文章写完,计划基于原生fetch封装 useFetch 轮子, 期待ing...
为何要写这个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} /> 复制代码
哇,相似中台系统,每一个页面基本都有个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} /> 复制代码
以上能够知足目前业务关于table的全部需求,欢迎来踩。
总结:
其余与页面反作用相关的工具函数均可以抽象成hook, 例如基于Rxjs
的use-observable
,定时器 use-interval
,基于localStorage 的封装 use-localStorage
,基于Form的use-form
, 基于Modal的use-modal
等等。
hook
真香🤡🤡,代码可读性提升,比HOC、Render Props更优雅, UI和逻辑耦合度更低,组件复用程度趋于最大化;固然Hook也不是万能的,复杂的数据管理还须要相似redux的工具,譬如redux 有 middleware, 而hook没有, 因此技术选型还需从业务的角度去衡量。一点总结以下:
微信:gwt385260 欢迎交流🤝🤝🤝~