这篇文章教你如何根据业务封装自定义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