——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 歡迎交流🤝🤝🤝~