前言
最近在使用 react hooks 重構(gòu)公司系統(tǒng),在使用由于 useEffect 的特性,當(dāng)有多個(gè)依賴(lài)項(xiàng)時(shí), 如果同時(shí)修改了多個(gè)依賴(lài)項(xiàng), useEffect 會(huì)調(diào)用多次,可能會(huì)發(fā)生非預(yù)期的調(diào)用。
舉個(gè)??
我們有如下一個(gè)界面, 要實(shí)現(xiàn)如下需求
- 修改狀態(tài)時(shí),拉取最新的數(shù)據(jù)。
- 修改類(lèi)型時(shí),將狀態(tài)重置為:全部預(yù)約,拉取最新的數(shù)據(jù)。
image.png
我們用代碼簡(jiǎn)單實(shí)現(xiàn)下:
import { useMemo } from 'react';
import { useEffect, useState } from 'react';
export default function Records() {
const types = useMemo(() => {
return ['個(gè)人記錄', '家屬記錄'];
}, []);
const statusList = useMemo(() => {
return ['全部預(yù)約', '待支付', '已受理'];
}, []);
const [type, setType] = useState('個(gè)人記錄');
const [status, setStatus] = useState('全部預(yù)約');
const getData = () => {
console.log(`獲取最新列表,類(lèi)型:${type}, 狀態(tài):${status}, ${Date.now()}`);
};
// 類(lèi)型更改,重置狀態(tài)
useEffect(() => {
setStatus('全部預(yù)約');
}, [type]);
// 當(dāng)狀態(tài)或類(lèi)型更改的時(shí)候拉取最新的數(shù)據(jù)
useEffect(() => {
getData();
}, [type, status]);
return (
<div>
<div>
{types.map((val) => {
return (
<button style={{ color: val == type ? 'red' : 'black' }} key={val} onClick={() => setType(val)}>
{val}
</button>
);
})}
</div>
<div>
{statusList.map((val) => {
return (
<button style={{ color: val == status ? 'red' : 'black' }} key={val} onClick={() => setStatus(val)}>
{val}
</button>
);
})}
</div>
</div>
);
}

2.gif
從運(yùn)行結(jié)果來(lái)看,兩個(gè)需求都實(shí)現(xiàn)了, 但是有一個(gè)問(wèn)題,當(dāng)狀態(tài)和記錄類(lèi)型同時(shí)更改的時(shí)候會(huì)獲取兩次最新的數(shù)據(jù)。
為了解決這個(gè)問(wèn)題,我們有如下幾種方式:
-
方式1(個(gè)人不推薦)
不使用useEffect獲取最新數(shù)據(jù),將getData調(diào)用方式放在按鈕的點(diǎn)擊事件上。
缺點(diǎn):開(kāi)發(fā)者需要關(guān)注的點(diǎn)變多,不利于后期維護(hù)。getData函數(shù)還需要增加兩個(gè)形參, 代碼復(fù)雜度提升
代碼如下:
import { useMemo } from 'react';
import { useEffect, useState } from 'react';
export default function Records() {
const types = useMemo(() => {
return ['個(gè)人記錄', '家屬記錄'];
}, []);
const statusList = useMemo(() => {
return ['全部預(yù)約', '待支付', '已受理'];
}, []);
const [type, setType] = useState('');
const [status, setStatus] = useState('');
const getData = (type: string, status: string) => {
setType(type);
setStatus(status);
console.log(`獲取最新列表,類(lèi)型:${type}, 狀態(tài):${status}, ${Date.now()}`);
};
// 第一次初始化,獲取數(shù)據(jù)
useEffect(() => {
getData('個(gè)人記錄', '全部預(yù)約');
}, []);
return (
<div>
<div>
{types.map((val) => {
return (
<button style={{ color: val == type ? 'red' : 'black' }} key={val} onClick={() => getData(val, '全部預(yù)約')}>
{val}
</button>
);
})}
</div>
<div>
{statusList.map((val) => {
return (
<button style={{ color: val == status ? 'red' : 'black' }} key={val} onClick={() => getData(type, val)}>
{val}
</button>
);
})}
</div>
</div>
);
}

1.gif
-
方式2 (個(gè)人不推薦)
合并type和status到一個(gè)state, 這樣依賴(lài)性就變成了一個(gè), 也不會(huì)出發(fā)兩次useEffect的副作用。
缺點(diǎn): 其實(shí)兩個(gè)狀態(tài)關(guān)聯(lián)關(guān)系并不是很強(qiáng), 可讀性不是非常好, 調(diào)用和更新某個(gè)狀態(tài)的時(shí)候還要注意另一個(gè)狀態(tài)的值, 果如依賴(lài)的個(gè)數(shù)繼續(xù)增加, 復(fù)雜度直線上升。
代碼如下:
import { useMemo } from 'react';
import { useEffect, useState } from 'react';
export default function Records() {
const types = useMemo(() => {
return ['個(gè)人記錄', '家屬記錄'];
}, []);
const statusList = useMemo(() => {
return ['全部預(yù)約', '待支付', '已受理'];
}, []);
const [state, setState] = useState({
type: '個(gè)人記錄',
status: '全部預(yù)約',
});
const getData = () => {
console.log(`獲取最新列表,類(lèi)型:${state.type}, 狀態(tài):${state.status}, ${Date.now()}`);
};
// 當(dāng)狀態(tài)或類(lèi)型更改的時(shí)候拉取最新的數(shù)據(jù)
useEffect(() => {
getData();
}, [state]);
return (
<div>
<div>
{types.map((val) => {
return (
<button style={{ color: val == state.type ? 'red' : 'black' }} key={val} onClick={() => setState({status:"全部預(yù)約", type: val})}>
{val}
</button>
);
})}
</div>
<div>
{statusList.map((val) => {
return (
<button style={{ color: val == state.status ? 'red' : 'black' }} key={val} onClick={() => setState({...state, status: val})}>
{val}
</button>
);
})}
</div>
</div>
);
}
-
方式3 (個(gè)人相對(duì)推薦)
其實(shí)我們想要實(shí)現(xiàn)的是, 當(dāng)有多個(gè)依賴(lài)項(xiàng)同時(shí)更改時(shí), 回調(diào)函數(shù)只執(zhí)行最新的一次。這和防抖的思想是不是很相似, 所以我們可以先用防抖的思想處理getData函數(shù)。
由于react hooks+閉包有值捕獲的特性,需要用useRef實(shí)時(shí)記錄最新的回調(diào)函數(shù),具體細(xì)節(jié)不贅述。
我們先手動(dòng)實(shí)現(xiàn)一個(gè)防抖Hooks, 這里先借用下lodash-es的防抖實(shí)現(xiàn)
import { useRef } from 'react';
import { debounce } from 'lodash-es';
export function useDebounce<T extends Function>(fn: T, wait = 1000) {
const func = useRef(fn);
func.current = fn
const debounceWrapper = useRef(debounce((args) => func.current?.(args), wait));
return (debounceWrapper.current as unknown) as T;
}
全部代碼:
import { useDebounce } from '@/hooks/useInit';
import { useMemo } from 'react';
import { useEffect, useState } from 'react';
export default function Records() {
const types = useMemo(() => {
return ['個(gè)人記錄', '家屬記錄'];
}, []);
const statusList = useMemo(() => {
return ['全部預(yù)約', '待支付', '已受理'];
}, []);
const [type, setType] = useState('個(gè)人記錄');
const [status, setStatus] = useState('全部預(yù)約');
const getData = useDebounce(() => {
console.log(`獲取最新列表,類(lèi)型:${type}, 狀態(tài):${status}, ${Date.now()}`);
}, 0);
useEffect(() => {
setStatus('全部預(yù)約');
}, [type]);
useEffect(() => {
getData();
}, [type, status]);
return (
<div>
<div>
{types.map((val) => {
return (
<button style={{ color: val == type ? 'red' : 'black' }} key={val} onClick={() => setType(val)}>
{val}
</button>
);
})}
</div>
<div>
{statusList.map((val) => {
return (
<button style={{ color: val == status ? 'red' : 'black' }} key={val} onClick={() => setStatus(val)}>
{val}
</button>
);
})}
</div>
</div>
);
}
執(zhí)行結(jié)果:

3.gif
這樣雖然實(shí)現(xiàn)了我們的需求, 但是總覺(jué)得不是非常優(yōu)雅, 需要具體到某個(gè)函數(shù), 而且修改了原函數(shù)的實(shí)現(xiàn), 如果在
useEffect中做其他操作, 比較麻煩, 語(yǔ)義上也不直觀, 其他開(kāi)發(fā)者可能不明白為什么要用防抖。比如:
useEffect(() => {
getData();
//其他操作1
//其他操作2
}, [type, status]);
為了解決上面問(wèn)題, 我們可以進(jìn)一步封裝, 實(shí)現(xiàn)一個(gè)useBatchEffect。
useBatchEffect代碼如下:
/**
* 批量設(shè)置更新
*/
export function useBatchEffect(effect: EffectCallback, deps?: DependencyList, wait = 0) {
const fn = useDebounce(effect, wait);
useEffect(fn, deps);
}
在這里, 我們利用了剛上面實(shí)現(xiàn)的防抖hooks, 對(duì)useEffect中做的所有操作進(jìn)行防抖, 變相實(shí)現(xiàn)了useEffect有多個(gè)依賴(lài)同時(shí)更改的一個(gè)批量操作的操作。
全部代碼:
import { useBatchEffect } from '@/hooks/useInit';
import { useMemo } from 'react';
import { useEffect, useState } from 'react';
export default function Records() {
const types = useMemo(() => {
return ['個(gè)人記錄', '家屬記錄'];
}, []);
const statusList = useMemo(() => {
return ['全部預(yù)約', '待支付', '已受理'];
}, []);
const [type, setType] = useState('個(gè)人記錄');
const [status, setStatus] = useState('全部預(yù)約');
const getData = () => {
console.log(`獲取最新列表,類(lèi)型:${type}, 狀態(tài):${status}, ${Date.now()}`);
}
useEffect(() => {
setStatus('全部預(yù)約');
}, [type]);
// 批量操作
useBatchEffect(() => {
getData();
}, [type, status]);
return (
<div>
<div>
{types.map((val) => {
return (
<button style={{ color: val == type ? 'red' : 'black' }} key={val} onClick={() => setType(val)}>
{val}
</button>
);
})}
</div>
<div>
{statusList.map((val) => {
return (
<button style={{ color: val == status ? 'red' : 'black' }} key={val} onClick={() => setStatus(val)}>
{val}
</button>
);
})}
</div>
</div>
);
}

4.gif
總結(jié)
對(duì)比上面三個(gè)方案的實(shí)現(xiàn), 個(gè)人比較推薦第三種方式, 相對(duì)另外兩種方式來(lái)說(shuō), 代碼改動(dòng)最小, 語(yǔ)義上也比較直觀。如果大家有什么更好建議, 可以在評(píng)論區(qū)提出。
