這篇文章教你如何根據業務封裝自定義hooks

前言

ReactHooks已經推出有老長一段時間了,想必各位Reacter用起來也算是比較駕輕就熟,可是,據我觀察我周圍寫Hooks的同事,他們對於Hooks的使用大部分是因爲寫起來比類組件簡單快捷,固然,這也是Hooks推出的緣由之一,可是主要緣由並非爲了讓開發者寫起來便捷,而是能夠對React中一些重複的邏輯進行封裝。api

請注意:對React中一些重複的邏輯進行封裝並非封裝公共的方法,而是一些組件內部重複的邏輯加以封裝,使之能夠重複使用。
關於公共hooks的封裝,阿里開源的ahook有很多可使用的hook,固然也有關於對antd使用的hook,但是每一個項目的邏輯或許只差一步,就沒法使用。因此,開發人員必須掌握如何根據本身的業務來封裝特定hook,才能使開發提效,代碼優美,才能達到使用React-Hooks的目的。
下面我會根據我本身系統的項目來舉例說明,如何封裝特定的hooks。markdown

實例

上面是我司某項目一個經典列表頁,具體邏輯爲antd

  1. 搜索條件一、二、三、4互相聯動,及搜索條件1影響搜索條件2,搜索條件2影響搜索條件3...以此類推。因此當搜索條件1選擇值發生變化時,搜索條件二、三、4須要清空從新選擇,以此類推...
  2. 當某個搜索條件值變化時,下邊的table須要從新請求,更新數據,也就是搜索條件影響table的數據。

這種邏輯的列表頁在我司這個系統有不少,因此我把這段邏輯提成了兩個Hooks。將搜索條件之間的限制提成一個hooks,將搜索條件與table的聯動提成一個hooksasync

自定義hooks

總體頁面代碼

const columns = [
  {
    title: '姓名',
    dataIndex: 'name',
  },
  {
    title: '年齡',
    dataIndex: 'age',
  },
  {
    title: '畢業院校',
    dataIndex: 'school',
  },
  {
    title: '所在單位',
    dataIndex: 'work',
  },
  {
    title: '家鄉',
    dataIndex: 'home',
  },
  {
    title: '備註',
    dataIndex: 'command',
  },
];

const dataSource = [
  {
    name: '張三',
    age: 29,
    school: '北大',
    work: '阿里巴巴',
    home: '北京',
    command:'暫無'
  },
  {
    name: '李四',
    age: 19,
    school: '',
    work: '',
    home: '北京',
    command:'暫無'
  },
  {
    name: '馬武',
    age: 88,
    school: '北大',
    work: '阿里巴巴',
    home: '天津',
    command:'暫無'
  },
  {
    name: '趙六',
    age: 27,
    school: '北大',
    work: '百度',
    home: '傷害',
    command:'暫無'
  },
  {
    name: '整齊',
    age: 59,
    school: '五道口職業技術學院',
    work: '騰訊',
    home: '北京',
    command:'暫無'
  },
  {
    name: '老⑧',
    age: 59,
    school: '無',
    work: '騰訊',
    home: '北京',
    command:'奧利給'
  },
]

const request = (url, param) => {
  const paramsLength = Object.values(param).filter(Boolean).length
  return new Promise(res => {
    setTimeout(() => {
      res({items:dataSource.slice(0,paramsLength),total:dataSource.length})
    },3000)
  })
}
const Page = () => {
  const [selectValue, setSelectValue] = useState({
    one:undefined,
    two: undefined,
    three:undefined,
    four:undefined,
  });

  const { onSelectChange } = useLimitSelect(selectValue, setSelectValue);

  const { loading, tableProps } = useTable('/api/fetch', selectValue, request);

  useEffect(() => {
  console.log(selectValue);
  }, Object.values(selectValue))

  return (
    <div className='page-one'> <Row gutter={16}> <Col span={6}> <div>搜索條件1</div> <Select value={selectValue.one} onChange={onSelectChange('one')}> {[1, 2, 3].map((item) => ( <Option key={item} value={item}> {item} </Option> ))} </Select> </Col> <Col span={6}> <div>搜索條件2</div> <Select value={selectValue.two} onChange={onSelectChange('two')}> {[4, 5, 6].map((item) => ( <Option key={item} value={item}> {item} </Option> ))} </Select> </Col> <Col span={6}> <div>搜索條件3</div> <Select value={selectValue.three} onChange={onSelectChange('three')}> {[7, 8, 9].map((item) => ( <Option key={item} value={item}> {item} </Option> ))} </Select> </Col> <Col span={6}> <div>搜索條件4</div> <Select value={selectValue.four} onChange={onSelectChange('four')}> {[10, 11, 12].map((item) => ( <Option key={item} value={item}> {item} </Option> ))} </Select> </Col> </Row> <Table columns={columns} style={{ marginTop: '20px' }} loading={loading} {...tableProps} /> </div>
  );
};
複製代碼
  1. request爲請求接口的函數,使用Promise和settimeout模擬一下
  2. selectValue中四個屬性分別對應四個搜索條件值,初始化都爲空值
  3. useLimitSelect爲Select之間的自定義hooks、useTable爲Select和Table聯動的自定義hooks
  4. onSelectChange爲Select下拉菜單onChange事件
  5. tableProps爲Table組件須要的一系列Props

useLimitSelect

export const useLimitSelect = (value, setValue) => {
    const preValue = useRef(value);
    useEffect(() => {
        const preV = preValue.current;
        const keys = Object.keys(value);
        let change = false;
        const obj = {};
        for (let i = 0; i < keys.length; i++) {
            if (change) {
                obj[keys[i]] = undefined;
            }
            if (preV[keys[i]] !== value[keys[i]]) {
                change = true;
            }
        }
        setValue(pre => ({ ...pre, ...obj }));
        preValue.current = value;
    }, Object.values(value));
  
    const onSelectChange = useCallback((type) => (value) => {
        setValue(pre => ({ ...pre, [type]: value }));
    }, [])
    return { onSelectChange };
};
複製代碼

將selectValue和setSelectValue做爲參數傳入,useRef用於保存上一次的selectValue,每次value值發生變化,對value中的參數進行遍歷,逐個比較,若是有一個發生了改變,那麼從發生改變的下一個起,重置他們的值爲undefined,保存到一個對象中,遍歷完後統一更新value。
這樣,只要將須要依次聯動的select的value值按順序傳入,只要一個值變化,後續的值都會從新置空,與此同時想要作一些別的操做,徹底能夠在組件內部經過useEffect進行監聽,避免邏輯耦合。函數

useFetch

export const useFetch = (url,param,fetcher,options) => {
    const [loading, setLoading] = useState(false);
    const [data, setData] = useState(undefined);
    const [isError, setIsError] = useState(false);

    const request = useCallback( async () => {
        setLoading(true);
        try {
            const data = await fetcher(url, param)
            options.onSuccess && options.onSuccess(data, url, param);
            unstable_batchedUpdates(() => {
                setData(data);
            })
        } catch (error) {
            setIsError(true)
            options.onError && options.onError(error, url, param)
        }
        setLoading(false);
    }, [fetcher,...Object.values(param),url])
    
    useEffect(() => {
        request();
    }, Object.values(param));
    return { data, loading, isError, request };
}
複製代碼

useFetch是爲了統一處理請求我項目table數據接口封裝的hooks。url爲請求的接口、param爲請求的參數、fetcher爲請求的函數(好比組件內部模擬的request函數就是)、options是請求額外的處理函數,好比說請求成功須要作一些操做,請求失敗須要作一些操做。只要是param參數變化,就須要從新請求table數據。fetch

useTable

export const useTable = (url = '',param = {},fetcher,options = {}) => {
    const { defaultParams = { page:1, pageSize:15 }, onResponse = Response } = options;
    const [query, setQuery] = useState(() => ({ page: defaultParams.page, pageSize: defaultParams.pageSize }));
    const { data, loading, isError, request } = useFetch(url, { ...query, ...param }, fetcher, options);
    console.log(data);
    const onTableChange = useCallback((pagination) => {
        const { current, pageSize } = pagination;
        setQuery((prev) => ({ ...prev, current, pageSize }));
    },[])

    useEffect(() => {
        setQuery(prev => ({ ...prev, page: 1, pageSize: 15 }));
    }, Object.values(param));

    const refresh = useCallback(() => {
        setQuery((prev) => ({ ...prev, current: 1, pageSize: 15 }));
    }, [])
    
    const newData = onResponse ? onResponse(data) : data;
    return {
        tableProps: {
            onChange: onTableChange,
            dataSource: newData.data,
            pagination: {
                total: newData.total, 
                pageSize: query.pageSize,
                current: query.current,
                size: 'small',
                position: ['bottomCenter'],
            }
        },
        loading,
        isError,
        request,
        refresh,
    }
}:
複製代碼

useTable的代碼稍微多一些,但並不複雜。總共能夠傳入4個參數:ui

  1. url 請求table數據的URL
  2. param 請求table數據的參數
  3. fetcher 請求table數據的函數
  4. options 額外的參數,包括修改默認參數、數據返回數據處理等

因爲我這個系統table默認都是從第一頁開始、每頁15條數據,因此table的頁碼參數就默認爲一、15。onResponse是預留出對請求來的數據作統一處理的函數,這個看本身項目的邏輯。
將頁碼信息保存到一個state中是爲table頁碼改變留出邏輯,同時搜索條件改變,須要從新請求第一頁的數據。
useTable中調用上邊的useFetch,將頁碼信息和搜索條件信息一同傳入就能夠了。
onResponse中對請求回來的數據單獨作一些處理,好比說字段值更改之類的。url

最後

以上只是本人針對本身的項目總結的部分自定義Hooks,其中每一個Hook單獨拿出來也能夠直接使用。封裝自定義Hooks必定要提煉出本身業務中那些公共的邏輯,而且每個Hook只處理一段邏輯,避免耦合
封裝自定義Hooks不只要求對邏輯抽象、提取有必定能力,同時也要求開發人員對本身項目總體業務走向有必定的掌控力,若是一個自定義Hook只能在一個地方被使用,那麼就沒有封裝的必要了。
多總結、多練習、願讀完這篇文章的developer都能成爲技術大牛!spa