React Hooks useEffect多個(gè)依賴(lài)批量操作

前言

最近在使用 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è)人不推薦)
    合并typestatus到一個(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ū)提出。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容