前言
ReactHooks已经推出有老长一段时间了,想必各位Reacter用起来也算是比较得心应手,但是,据我观察我周围写Hooks的同事,他们对于Hooks的使用大部分是由于写起来比类组件简单快捷,当然,这也是Hooks推出的原因之一,但是主要原因并不是为了让开发者写起来便捷,而是可以对React中一些重复的逻辑进行封装。
请注意: 对React中一些重复的逻辑进行封装 并不是封装公共的方法,而是一些组件内部重复的逻辑加以封装,使之可以重复使用。
关于公共hooks的封装,阿里开源的 ahook
有不少可以使用的hook,当然也有关于对antd使用的hook,可是每个项目的逻辑或许只差一步,就无法使用。所以,开发人员必须掌握如何根据自己的业务来封装特定hook,才能使开发提效,代码优美,才能达到使用React-Hooks的目的。
下面我会根据我自己系统的项目来举例说明,如何封装特定的hooks。
实例
上面是我司某项目一个经典列表页,具体逻辑为
- 搜索条件1、2、3、4互相联动,及搜索条件1影响搜索条件2,搜索条件2影响搜索条件3…以此类推。所以当搜索条件1选择值发生变化时,搜索条件2、3、4需要清空重新选择,以此类推…
- 当某个搜索条件值变化时,下边的table需要重新请求,更新数据,也就是搜索条件影响table的数据。
这种逻辑的列表页在我司这个系统有很多,所以我把这段逻辑提成了两个Hooks。 将搜索条件之间的限制提成一个hooks,将搜索条件与table的联动提成一个hooks
。
自定义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> ); }; 复制代码
-
request
为请求接口的函数,使用Promise和settimeout模拟一下 -
selectValue
中四个属性分别对应四个搜索条件值,初始化都为空值 -
useLimitSelect
为Select之间的自定义hooks、useTable
为Select和Table联动的自定义hooks -
onSelectChange
为Select下拉菜单onChange事件 -
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数据。
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个参数:
- url 请求table数据的URL
- param 请求table数据的参数
- fetcher 请求table数据的函数
- options 额外的参数,包括修改默认参数、数据返回数据处理等
由于我这个系统table默认都是从第一页开始、每页15条数据,所以table的页码参数就默认为1、15。 onResponse
是预留出对请求来的数据做统一处理的函数,这个看自己项目的逻辑。
将页码信息保存到一个 state
中是为table页码改变留出逻辑,同时搜索条件改变,需要重新请求第一页的数据。
在 useTable
中调用上边的 useFetch
,将页码信息和搜索条件信息一同传入就可以了。
onResponse
中对请求回来的数据单独做一些处理,比如说字段值更改之类的。
最后
以上只是本人针对自己的项目总结的部分自定义Hooks,其中每个Hook单独拿出来也可以直接使用。封装自定义Hooks一定要 提炼出自己业务中那些公共的逻辑,并且每一个Hook只处理一段逻辑,避免耦合 。
封装自定义Hooks不仅要求对逻辑抽象、提取有一定能力,同时也要求开发人员对自己项目整体业务走向有一定的掌控力,如果一个自定义Hook只能在一个地方被使用,那么就没有封装的必要了。
多总结、多练习、愿读完这篇文章的developer都能成为技术大牛!