本文是深入淺出 ahooks 源碼系列文章的第十一篇,該系列已整理成文檔-地址。覺得還不錯,給個 star 支持一下哈,Thanks。 本文來講下 ahooks 中的 useUrlState。 通過 url query 來管理 state 的 Hook。 useUrlState 的特殊 在之前的架構 ...
本文是深入淺出 ahooks 源碼系列文章的第十一篇,該系列已整理成文檔-地址。覺得還不錯,給個 star 支持一下哈,Thanks。
本文來講下 ahooks 中的 useUrlState。
通過 url query 來管理 state 的 Hook。
useUrlState 的特殊
在之前的架構篇中我們就提到,ahooks 這個項目是一個 monoRepo
。它的項目管理是通過 lerna 進行管理的。可以從官網以及源碼中看到 useUrlState 是獨立一個倉庫進行管理的。
也就是你必須單獨安裝:
npm i @ahooksjs/use-url-state -S
我認為官方這麼做的理由是 useUrlState 基於 react-router 的 useLocation & useHistory & useNavigate 進行 query 管理。所以你必須要安裝 react-router 的 5.x 或者 6.x 版本。但其實很多 React 項目都不一定使用 react-router。假如將這個 hook 內置到 ahooks 中的話,可能會導致包體積變大。
另外,該 hook 是依賴於 query-string 這個 npm 包的。使用這個包,我認為理由有以下幾點:
- 一來是其功能強大,支持很多的 options 選項,滿足我們各類業務需求。
- 二來它業內也比較成熟,避免重覆造輪子。
- 三來它的包體積也很小,沒什麼負擔。我們主要用到它的 parse 和 stringify 方法,壓縮後只有 2.4 k。
通過示例簡單介紹下,這兩個方法:
qs.parse(string, [options])
qs.parse('?name=jim') // {name: 'jim'}
qs.parse('#token=123') // {token: '123'}
qs.parse('name=jim&name=lily&age=22') // {name: ['jim', 'lily'], age: 22}
qs.parse('foo[]=1&foo[]=2&foo[]=3', {arrayFormat: 'bracket'});
//=> {foo: ['1', '2', '3']}
qs.stringify(object, [options])
qs.stringify({name: 'jim', age: 22}); // 'age=22&name=jim'
qs.stringify({name: ['jim', 'lily'], age: 22}); // 'age=22&name=jim&name=lily'
qs.stringify({foo: [1, 2, 3]}, {arrayFormat: 'bracket'});
//=> 'foo[]=1&foo[]=2&foo[]=3'
useUrlState 源碼解析
直接看代碼,顯示初始值部分。
- 第一個參數為初始狀態,第二個參數為 url 的配置,包括狀態變更時切換 history 的方式、query-string parse 和 stringify 的配置。
- 通過 react-router 的 useLocation 獲取到 URL 的 location 對象。
- react-router 相容 5.x 和 6.x,其中 5.x 使用 useHistory,6.x 使用 useNavigate。
- queryFromUrl 是調用 query-string 的 parse 方法,將 location 對象的 search 處理成對象值。
- targetQuery 就是處理之後的最終值-將 queryFromUrl 和初始值進行 merge 之後的結果。
// ...
import * as tmp from 'react-router';
// ...
const useUrlState = <S extends UrlState = UrlState>(
// 初始狀態
initialState?: S | (() => S),
// url 配置
options?: Options,
) => {
type State = Partial<{ [key in keyof S]: any }>;
const {
// 狀態變更時切換 history 的方式
navigateMode = 'push',
// query-string parse 的配置
parseOptions,
// query-string stringify 的配置
stringifyOptions,
} = options || {};
const mergedParseOptions = { ...baseParseConfig, ...parseOptions };
const mergedStringifyOptions = { ...baseStringifyConfig, ...stringifyOptions };
// useLocation鉤子返回表示當前URL的location對象。您可以將它想象成一個useState,它在URL更改時返回一個新值。
const location = rc.useLocation();
// https://v5.reactrouter.com/web/api/Hooks/usehistory
// useHistory 鉤子可以訪問用來導航的歷史實例。
// react-router v5
const history = rc.useHistory?.();
// react-router v6
const navigate = rc.useNavigate?.();
const update = useUpdate();
const initialStateRef = useRef(
typeof initialState === 'function' ? (initialState as () => S)() : initialState || {},
);
// 根據 url query
const queryFromUrl = useMemo(() => {
return parse(location.search, mergedParseOptions);
}, [location.search]);
const targetQuery: State = useMemo(
() => ({
...initialStateRef.current,
...queryFromUrl,
}),
[queryFromUrl],
);
// 省略部分代碼
}
接下來看 url 的狀態設置:
- 首先是根據傳入的 s 值,獲取到新的狀態 newQuery,支持 function 方式。
- 根據不同的 react-router 的版本調用不同的方法進行更新。
- 假如是 5.x 版本,調用 useHistory 方法,更新對應的狀態。
- 假如是 6.x 版本,調用 useNavigate 方法,更新對應的狀態。
- 通過 update() -
useUpdate()
更新狀態。
// 設置 url 狀態
const setState = (s: React.SetStateAction<State>) => {
const newQuery = typeof s === 'function' ? s(targetQuery) : s;
// 1. 如果 setState 後,search 沒變化,就需要 update 來觸發一次更新。比如 demo1 直接點擊 clear,就需要 update 來觸發更新。
// 2. update 和 history 的更新會合併,不會造成多次更新
update();
if (history) {
history[navigateMode]({
hash: location.hash,
search: stringify({ ...queryFromUrl, ...newQuery }, mergedStringifyOptions) || '?',
});
}
if (navigate) {
navigate(
{
hash: location.hash,
search: stringify({ ...queryFromUrl, ...newQuery }, mergedStringifyOptions) || '?',
},
{
replace: navigateMode === 'replace',
},
);
}
};
思考與總結
工具庫中假如某個工具函數/hook 依賴於一個開發者可能並不會使用的包,而且這個包的體積還比較大的時候,可以將這個工具函數/hook 獨立成一個 npm 包,開發者使用的時候才進行安裝。另外這種可以考慮使用 monoRepo 的包管理方法,方便進行文檔管理以及一些公共包管理等。
本文已收錄到個人博客中,歡迎關註~