使用React写业务也有挺长一段时间了,总结下我认为的最佳实践。
状态管理最佳实践
主流的状态管理多多少少都用过,多少都有学习成本。 真的有必要花大把时间去学其他的状态管理吗?真的实用吗?如果不是为了应付面试,我是不会花时间去了解那些状态管理的。
后面发现 useContext hook 完全够用,而且对逻辑的划分非常有帮助,真正做到了逻辑的复用。 如果你习惯用自定义 hook 去抽离逻辑,那么结合 useContext 会是个很棒的体验。
这是对 useContext 很棒的封装: github.com/jamiebuilds…
举例:
import React, { useState } from 'react';
import { createContainer } from "unstated-next"
import { render } from "react-dom"
// 定义 A 相关的状态和逻辑
const useA = () => {
const [a, setA] = useState(0)
const handleChangeA = () => {
setA(a + 1)
}
return {
a,
handleChangeA
}
}
const AContainer = createContainer(useA)
// 定义 subType 相关的状态和逻辑
const subTypeMap = {
0: 'all',
1: 'active',
2: 'completed'
}
const useSubTypeSelect = () => {
const [subType, setSubType] = useState(0);
const subTypeOptions = Object.entries(subTypeMap).map(([key, value]) => ({ value: key, label: value }))
// ReactNode
const subTypeNode = <div>
<h5>subType: {subTypeMap[subType]} </h5>
<ul>
{subTypeOptions.map((item) => (
<li
key={item.value}
onClick={() => setSubType(item.value)}
>
{item.label}
</li>
))}
</ul>
</div>
// ... others
return {
subType,
subTypeNode
}
}
const SubTypeSelectContainer = createContainer(useSubTypeSelect)
// UI
const Main = () => {
// 使用 A 模块相关状态和逻辑
const { a, handleChangeA } = AContainer.useContainer()
// 使用 subType 模块相关状态和逻辑
const { subType, subTypeNode } = SubTypeSelectContainer.useContainer()
return <div>
{/* A */}
<button onClick={handleChangeA}>A: {a}</button>
{/* subTypeSelect */}
{subTypeNode}
{/* ... */}
</div>
}
const App = () => {
return (
<AContainer.Provider>
<SubTypeSelectContainer.Provider>
<Main />
</SubTypeSelectContainer.Provider>
</AContainer.Provider>
)
}
render(<App />, document.getElementById("root"))
效果:
state 与 queryString 联动
有些时候,你希望你的state在变更时同时变更URL里的queryString,那么可以这么做:
- 定义 useQSState
import React, { useState, useEffect } from 'react';
import { useHistory } from 'react-router-dom';
export const encodeParams: <T> (obj: T) => string = obj => encodeURIComponent(JSON.stringify(obj));
// export const encodeParamsWithOptionsProperty: <T> (obj: T) => string = obj => encodeParams({ options: obj });
export const encodeParamsSplicedOptions: <T>(obj: T) => string = obj => `?options=${encodeParams(obj)}`;
export const deCodeParams = (qs: string) => {
try {
return JSON.parse(decodeURIComponent(qs));
} catch (error) {
return {}
}
};
const getQSObjectWithSplitedOptions: () => Record<string, unknown> = () => {
const _qs = window.location.href?.split('?')?.[1];
return deCodeParams(_qs?.split('=')?.[1])
};
export const useQueryString = () => {
const queryStringObj: Record<string, any> = getQSObjectWithSplitedOptions();
const history = useHistory();
const setQueryString = rawQueryString => {
if (history.location.search !== rawQueryString) {
history.replace({
search: rawQueryString,
});
}
};
const setQSByObj = (newObject): void => {
const rawQueryString = encodeParamsSplicedOptions(newObject);
setQueryString(rawQueryString);
};
const injectQS = injectedObj => {
const _qs = getQSObjectWithSplitedOptions();
const newObject = {
..._qs,
...injectedObj,
};
setQSByObj(newObject);
};
const injectQSByProperty = (propName: string, propValue) => {
injectQS({ [propName]: propValue });
};
return { queryStringObj, setQueryString, setQSByObj, injectQS, injectQSByProperty, };
};
// state 与 queryString 联动
export const useQSState: <T>(field: string, defaultValue: T) => [T, React.Dispatch<React.SetStateAction<T>>] = (field, defaultValue) => {
const { queryStringObj, injectQSByProperty } = useQueryString();
const _defaultValue = queryStringObj[field] || defaultValue;
const [value, setValue] = useState(_defaultValue);
useUpdateEffect(() => {
injectQSByProperty(field, value);
}, [value]);
return [value, setValue];
}
- 使用 useQSState
import React, { useState } from 'react';
import { createContainer } from "unstated-next"
import { render } from "react-dom"
import { useQSState } from './useQueryString'
// 定义 A 相关的状态和逻辑
const useA = () => {
const [a, setA] = useQSState('a', 0) // useState 被替换成 useQSState
const handleChangeA = () => {
setA(a + 1)
}
return {
a,
handleChangeA
}
}
const AContainer = createContainer(useA)
// 定义 subType 相关的状态和逻辑
const subTypeMap = {
0: 'all',
1: 'active',
2: 'completed'
}
const useSubTypeSelect = () => {
const [subType, setSubType] = useQSState('subType', 0); // useState 被替换成 useQSState
const subTypeOptions = Object.entries(subTypeMap).map(([key, value]) => ({ value: key, label: value }))
// ReactNode
const subTypeNode = <div>
<h5>subType: {subTypeMap[subType]} </h5>
<ul>
{subTypeOptions.map((item) => (
<li
key={item.value}
onClick={() => setSubType(item.value)}
>
{item.label}
</li>
))}
</ul>
</div>
// ... others
return {
subType,
subTypeNode
}
}
const SubTypeSelectContainer = createContainer(useSubTypeSelect)
// UI
const Main = () => {
// 使用 A 模块相关状态和逻辑
const { a, handleChangeA } = AContainer.useContainer()
// 使用 subType 模块相关状态和逻辑
const { subType, subTypeNode } = SubTypeSelectContainer.useContainer()
return <div>
{/* A */}
<button onClick={handleChangeA}>A: {a}</button>
{/* subTypeSelect */}
{subTypeNode}
{/* ... */}
</div>
}
const App = () => {
return (
<AContainer.Provider>
<SubTypeSelectContainer.Provider>
<Main />
</SubTypeSelectContainer.Provider>
</AContainer.Provider>
)
}
render(<App />, document.getElementById("root"))
state 之间的联动
有些时候,改变某个state,其他state会受到影响 比如列表页,当我们变更筛选项或者排序时,需要初始化page,让page变为1,
import React, { useEffect, useRef } from 'react';
import { createContainer } from "unstated-next"
import { render } from "react-dom"
import { useQSState } from './useQueryString'
// useEffect只在依赖更新时执行
export const useUpdateEffect: typeof useEffect = (effect, deps) => {
const isInitialMount = useRef(true);
useEffect(
isInitialMount.current
? () => {
isInitialMount.current = false;
}
: effect,
deps
);
};
// 定义 A 相关的状态和逻辑
const useA = () => {
const [a, setA] = useQSState('a', 0) // useState 被替换成 useQSState
const handleChangeA = () => {
setA(a + 1)
}
return {
a,
handleChangeA
}
}
const AContainer = createContainer(useA)
// 定义 subType 相关的状态和逻辑
const subTypeMap = {
0: 'all',
1: 'active',
2: 'completed'
}
const useSubTypeSelect = () => {
const [subType, setSubType] = useQSState('subType', 0); // useState 被替换成 useQSState
const subTypeOptions = Object.entries(subTypeMap).map(([key, value]) => ({ value: key, label: value }))
// ReactNode
const subTypeNode = <div>
<h5>subType: {subTypeMap[subType]} </h5>
<ul>
{subTypeOptions.map((item) => (
<li
key={item.value}
onClick={() => setSubType(item.value)}
>
{item.label}
</li>
))}
</ul>
</div>
// ... others
return {
subType,
subTypeNode
}
}
const SubTypeSelectContainer = createContainer(useSubTypeSelect)
// 分页逻辑
interface IPagination {
defaultValue?: {
page: number;
pageSize: number;
};
}
export const usePagination = (props: IPagination) => {
const { defaultValue = { page: 1, pageSize: 10 } } = props || {};
const [value, setValue] = useQSState<{
page: number;
pageSize: number;
}>('pagination', defaultValue);
const setCurrentPage = (page: number) => {
setValue(prev => ({
page,
pageSize: prev.pageSize,
}));
};
const setPageSize = (size: number) => {
setValue(prev => ({
page: prev.page,
pageSize: size,
}));
};
const resetPage = () => {
setCurrentPage(1);
};
const pagination = {
currentPage: value.page,
pageSize: value.pageSize,
onPageChange: setCurrentPage,
onPageSizeChange: setPageSize,
showSizeChanger: true,
pageSizeOpts: [10, 20, 30, 50],
// ...
};
return {
currentPage: value.page,
pageSize: value.pageSize,
setPageSize,
setCurrentPage,
resetPage,
pagination,
};
};
export const PaginationContainer = createContainer(usePagination);
// 业务逻辑,跟随具体需要联动的 State 创建
const usePaginationLinkage = () => {
const { a } = AContainer.useContainer()
const { subType } = SubTypeSelectContainer.useContainer()
const { currentPage, pageSize, resetPage } = PaginationContainer.useContainer();
const _deps = [
a,
subType
];
useUpdateEffect(() => {
// 当 a , subType 变化时,重置分页
resetPage();
}, _deps);
const deps = [..._deps, currentPage, pageSize];
return {
a,
subType,
currentPage,
pageSize,
deps,
};
};
export const PaginationLinkageModel = createContainer(usePaginationLinkage);
// UI
const Main = () => {
// 使用 A 模块相关状态和逻辑
const { a, handleChangeA } = AContainer.useContainer()
// 使用 subType 模块相关状态和逻辑
const { subType, subTypeNode } = SubTypeSelectContainer.useContainer()
// 使用 pagination 模块相关状态和逻辑
const { currentPage, pageSize, deps } = PaginationLinkageModel.useContainer()
return <div>
{/* A */}
<button onClick={handleChangeA}>A: {a}</button>
{/* subTypeSelect */}
{subTypeNode}
{/* ... */}
</div>
}
const App = () => {
return (
<AContainer.Provider>
<SubTypeSelectContainer.Provider>
<PaginationContainer.Provider>
<PaginationLinkageModel.Provider>
<Main />
</PaginationLinkageModel.Provider>
</PaginationContainer.Provider>
</SubTypeSelectContainer.Provider>
</AContainer.Provider>
)
}
render(<App />, document.getElementById("root"))
请求的防抖
请求受到多个state影响,当多个state变更会导致请求触发多次,那么可以这么解决:
- 定义 useDebounceFn hook
import React, { DependencyList, useCallback, useEffect, useRef } from 'react';
import { useUpdateEffect } from './useUpdateEffect';
export interface ReturnValue<T extends any[]> {
run: (...args: T) => void;
cancel: () => void;
}
export interface IUseDebounceFnOptions {
immediately?: boolean;
trailing?: boolean;
leading?: boolean;
}
/**
* 使用防抖函数
* @param fn - 要防抖的函数
* @param wait - 延迟时间(默认为 1000 毫秒)
* @param deps - 依赖项
* @param options - 配置选项
* @returns {ReturnValue<T>} - 返回运行函数和取消函数的对象
*/
function useDebounceFn<T extends any[]>(
fn: (...args: T) => any,
wait: number = 1000,
deps: DependencyList = [],
options: IUseDebounceFnOptions = {}
): ReturnValue<T> {
const { immediately = false, trailing = true, leading = false } = options;
const timer = useRef<number>();
const haveRun = useRef<boolean>(false);
const interval = useRef<boolean>(false);
const fnRef = useRef<(...args: any[]) => any>(fn);
fnRef.current = fn;
const currentArgs = useRef<any[]>([]);
/**
* 设置定时器
* @param trailing - 是否在延迟后执行函数
* @returns {number} - 返回定时器的 ID
*/
const setTimer = (trailing: boolean): number => {
return window.setTimeout(() => {
trailing ? fnRef.current(...currentArgs.current) : null;
interval.current = false;
}, wait);
};
/**
* 取消定时器
*/
const cancel = useCallback(() => {
if (timer.current) {
clearTimeout(timer.current);
}
}, []);
/**
* 运行防抖函数
* @param args - 传递给函数的参数
*/
const run = useCallback((...args: any[]): void => {
currentArgs.current = args;
if (immediately && !haveRun.current) {
fnRef.current(...currentArgs.current);
haveRun.current = true;
return;
}
cancel();
if (leading) {
if (!interval.current) {
fnRef.current(...currentArgs.current);
interval.current = true;
}
}
timer.current = setTimer(trailing);
}, [wait, cancel]);
useUpdateEffect(() => {
run();
return cancel;
}, [...deps, run]);
useEffect(() => cancel, []);
return { run, cancel };
}
export default useDebounceFn;
- 定义 useDebounceRequest hook
export function useDebounceRequest<T>({
fetch,
deps = [],
defaultValue,
}: {
fetch: () => Promise<T>;
deps?: any[];
defaultValue?: any;
}) {
const [data, setData] = useState<T>(defaultValue);
const [loading, setLoading] = useState(false);
// 优化:使用 async/await 处理异步操作
const _fetchCb = async () => {
if (loading) {
return;
}
setLoading(true);
const response = await fetch();
setData(response);
setLoading(false);
};
// 使用 useDebounceFn 进行防抖处理
const { run } = useDebounceFn(_fetchCb, 100);
// 监听依赖项变化时执行 run
useEffect(run, deps);
return { data, loading };
}
- 使用
import React, { useEffect, useRef } from 'react';
import { createContainer } from "unstated-next"
import { render } from "react-dom"
import { useQSState } from './useQueryString'
import { useDebounceRequest } from './useDebounceRequest';
// useEffect只在依赖更新时执行
export const useUpdateEffect: typeof useEffect = (effect, deps) => {
const isInitialMount = useRef(true);
useEffect(
isInitialMount.current
? () => {
isInitialMount.current = false;
}
: effect,
deps
);
};
// 定义 A 相关的状态和逻辑
const useA = () => {
const [a, setA] = useQSState('a', 0) // useState 被替换成 useQSState
const handleChangeA = () => {
setA(a + 1)
}
return {
a,
handleChangeA
}
}
const AContainer = createContainer(useA)
// 定义 subType 相关的状态和逻辑
const subTypeMap = {
0: 'all',
1: 'active',
2: 'completed'
}
const useSubTypeSelect = () => {
const [subType, setSubType] = useQSState('subType', 0); // useState 被替换成 useQSState
const subTypeOptions = Object.entries(subTypeMap).map(([key, value]) => ({ value: key, label: value }))
// ReactNode
const subTypeNode = <div>
<h5>subType: {subTypeMap[subType]} </h5>
<ul>
{subTypeOptions.map((item) => (
<li
key={item.value}
onClick={() => setSubType(item.value)}
>
{item.label}
</li>
))}
</ul>
</div>
// ... others
return {
subType,
subTypeNode
}
}
const SubTypeSelectContainer = createContainer(useSubTypeSelect)
// 分页逻辑
interface IPagination {
defaultValue?: {
page: number;
pageSize: number;
};
}
export const usePagination = (props: IPagination) => {
const { defaultValue = { page: 1, pageSize: 10 } } = props || {};
const [value, setValue] = useQSState<{
page: number;
pageSize: number;
}>('pagination', defaultValue);
const setCurrentPage = (page: number) => {
setValue(prev => ({
page,
pageSize: prev.pageSize,
}));
};
const setPageSize = (size: number) => {
setValue(prev => ({
page: prev.page,
pageSize: size,
}));
};
const resetPage = () => {
setCurrentPage(1);
};
const pagination = {
currentPage: value.page,
pageSize: value.pageSize,
onPageChange: setCurrentPage,
onPageSizeChange: setPageSize,
showSizeChanger: true,
pageSizeOpts: [10, 20, 30, 50],
// ...
};
return {
currentPage: value.page,
pageSize: value.pageSize,
setPageSize,
setCurrentPage,
resetPage,
pagination,
};
};
export const PaginationContainer = createContainer(usePagination);
// 业务逻辑,跟随具体需要联动的 State 创建
const usePaginationLinkage = () => {
const { a } = AContainer.useContainer()
const { subType } = SubTypeSelectContainer.useContainer()
const { currentPage, pageSize, resetPage } = PaginationContainer.useContainer();
const _deps = [
a,
subType
];
useUpdateEffect(() => {
// 当 a , subType 变化时,重置分页
resetPage();
}, _deps);
const deps = [..._deps, currentPage, pageSize];
return {
a,
subType,
currentPage,
pageSize,
deps,
};
};
export const PaginationLinkageModel = createContainer(usePaginationLinkage);
const useFetchDemoData = () => {
const { a, subType, currentPage, pageSize } = PaginationLinkageModel.useContainer()
const fetch = async () => {
const res = await Promise.resolve({
data: {
list: [],
total: 0
}
})
// format data ...
// do something ...
return res
}
// 使用 useDebounceRequest 进行防抖
const { data, loading } = useDebounceRequest({
fetch,
deps: [a, subType, currentPage, pageSize],
defaultValue: {
data: {
list: [],
total: 0
}
}
})
return {
data,
loading
}
}
export const FetchDemoDataContainer = createContainer(useFetchDemoData)
// UI
const Main = () => {
// 使用 A 模块相关状态和逻辑
const { a, handleChangeA } = AContainer.useContainer()
// 使用 subType 模块相关状态和逻辑
const { subType, subTypeNode } = SubTypeSelectContainer.useContainer()
// 使用 pagination 模块相关状态和逻辑
const { currentPage, pageSize, deps } = PaginationLinkageModel.useContainer()
return <div>
{/* A */}
<button onClick={handleChangeA}>A: {a}</button>
{/* subTypeSelect */}
{subTypeNode}
{/* ... */}
</div>
}
const App = () => {
return (
<AContainer.Provider>
<SubTypeSelectContainer.Provider>
<PaginationContainer.Provider>
<PaginationLinkageModel.Provider>
<FetchDemoDataContainer.Provider>
<Main />
</FetchDemoDataContainer.Provider>
</PaginationLinkageModel.Provider>
</PaginationContainer.Provider>
</SubTypeSelectContainer.Provider>
</AContainer.Provider>
)
}
render(<App />, document.getElementById("root"))