自从react16.8,react-hooks
诞生以来,在工做中一直使用hooks
,一年多的时间里,接触的react项目,渐渐使用function
无状态组件代替了classs
声明的有状态组件,期间也总结了一些心得。尤为对于近期三个月的项目里,一点点用自定义hooks来处理公司项目中重复逻辑,整体感受还不错。今天给你们讲讲我在工做中对react-hooks
心得,和一些自定义hooks的设计思想,把在工做中的经验分享给你们。css
react-hooks
是react16.8之后,react新增的钩子API,目的是增长代码的可复用性,逻辑性,弥补无状态组件没有生命周期,没有数据管理状态state的缺陷。笔者认为,react-hooks
思想和初衷,也是把组件,颗粒化,单元化,造成独立的渲染环境,减小渲染次数,优化性能。前端
还不明白react-hooks的伙伴能够看的另一篇文章: react-hooks如何使用?react
自定义hooks是在react-hooks
基础上的一个拓展,能够根据业务须要制定知足业务须要的hooks,更注重的是逻辑单元。经过业务场景不一样,咱们到底须要react-hooks
作什么,怎么样把一段逻辑封装起来,作到复用,这是自定义hooks产生的初衷。ios
hooks 专一的就是逻辑复用, 是咱们的项目,不只仅停留在组件复用的层面上。hooks让咱们能够将一段通用的逻辑存封起来。将咱们须要它的时候,开箱即用便可。css3
hooks
本质上是一个函数。函数的执行,决定与无状态组件组件自身的执行上下文。每次函数的执行(本质上就是组件的更新)就会执行自定义hooks
的执行,因而可知组件自己执行和hooks的执行一模一样。git
那么prop
的修改,useState,useReducer
使用是无状态组件更新条件,那么就是驱动hooks执行的条件。 咱们用一幅图来表示如上关系。github
咱们设计的自定义react-hooks
应该是长的这样的。web
const [ xxx , ... ] = useXXX(参数A,参数B...)
复制代码
在咱们在编写自定义hooks的时候,要特别~特别~特别关注的是传进去什么,返回什么。 返回的东西是咱们真正须要的。更像一个工厂,把原材料加工,最后返回咱们。正以下图所示json
若是自定义hooks没有设计好,好比返回一个改变state的函数,可是没有加条件限定限定,就有可能形成没必要要的上下文的执行,更有甚的是组件的循环渲染执行。后端
好比:咱们写一个很是简单hooks来格式化数组将小写转成大写。
import React , { useState } from 'react'
/* 自定义hooks 用于格式化数组将小写转成大写 */
function useFormatList(list){
return list.map(item=>{
console.log(1111)
return item.toUpperCase()
})
}
/* 父组件传过来的list = [ 'aaa' , 'bbb' , 'ccc' ] */
function index({ list }){
const [ number ,setNumber ] = useState(0)
const newList = useFormatList(list)
return <div> <div className="list" > { newList.map(item=><div key={item} >{ item }</div>) } </div> <div className="number" > <div>{ number }</div> <button onClick={()=> setNumber(number + 1) } >add</button> </div> </div>
}
export default index
复制代码
如上述问题,咱们格式化父组件传递过来的list
数组,并将小写变成大写,可是当咱们点击add
。 理想状态下数组不须要从新format
,可是实际跟着执行format
。无疑增长了性能开销。
因此咱们在设置自定义hooks的时候,必定要把条件限定-性能开销加进去。
因而乎咱们这样处理一下。
function useFormatList(list) {
return useMemo(() => list.map(item => {
console.log(1111)
return item.toUpperCase()
}), [])
}
复制代码
华丽丽的解决了如上的问题。
因此一个好用的自定义hooks,必定要配合useMemo ,useCallback
等api
一块儿使用。
为了将实际的业务情景和自定义hooks
链接在一块儿,我这里用 taro-h5
构建了一个移动端react
项目。用于描述实际工做中用到自定义hooks
的场景。
demo项目地址 : 自定义hooks,demo项目
后续会更新更多自定义hooks,或者感兴趣的同窗能够关注一下这个项目,或者也能够一块儿维护这个项目。
项目结构
page
文件夹里包括自定义hooks展现demo
页面,hooks
文件夹里面是自定义hooks内容。
展现效果
每一个listItem
记录每个完成自定义hooks展现效果,陆续还有其余的hooks
。咱们接下来看看hooks具体实现。
useScroll
背景:公司的一个h5项目,在滚动条滚动的过程当中,须要控制 渐变 + 高度 + 吸顶效果。
1 首先红色色块有吸顶效果。 2 粉色色块,是固定上边可是有少许偏移,加上逐渐变透明效果。
useScroll
设计思路须要实现功能:
1 监听滚动条滚动。 2 计算吸顶临界值,渐变值,透明度。 3 改变state
渲染视图。
好吧,接下来让咱们用一个hooks
来实现上述工做。
页面
import React from 'react'
import { View, Swiper, SwiperItem } from '@tarojs/components'
import useScroll from '../../hooks/useScroll'
import './index.less'
export default function Index() {
const [scrollOptions,domRef] = useScroll()
/* scrollOptions 保存控制透明度 ,top值 ,吸顶开关等变量 */
const { opacity, top, suctionTop } = scrollOptions
return <View style={{ position: 'static', height: '2000px' }} > <View className='white' /> <View id='box' style={{ opacity, transform: `translateY(${top}px)` }} > <Swiper className='swiper' > <SwiperItem className='SwiperItem' > <View className='imgae' /> </SwiperItem> </Swiper> </View> <View className={suctionTop ? 'box_card suctionTop' : 'box_card'}> <View style={{ background: 'red', boxShadow: '0px 15px 10px -16px #F02F0F' }} className='reultCard' > </View> </View> </View>
}
复制代码
咱们经过一个scrollOptions
来保存透明度 ,top
值 ,吸顶开关等变量,而后经过返回一个ref
做为dom
元素的采集器。接下来就是hooks若是实现的。
useScroll
export default function useScroll() {
const dom = useRef(null)
const [scrollOptions, setScrollOptions] = useState({
top: 0,
suctionTop: false,
opacity: 1
})
useEffect(() => {
const box = (dom.current)
const offsetHeight = box.offsetHeight
const radio = box.offsetHeight / 500 * 20
const handerScroll = () => {
const scrollY = window.scrollY
/* 控制透明度 */
const computerOpacty = 1 - scrollY / 160
/* 控制吸顶效果 */
const offsetTop = offsetHeight - scrollY - offsetHeight / 500 * 84
const top = 0 - scrollY / 5
setScrollOptions({
opacity: computerOpacty <= 0 ? 0 : computerOpacty,
top,
suctionTop: offsetTop < radio
})
}
document.addEventListener('scroll', handerScroll)
return function () {
document.removeEventListener('scroll', handerScroll)
}
}, [])
return [scrollOptions, dom]
}
复制代码
具体设计思路
1 咱们用一个 useRef
来获取须要元素 2 用 useEffect
来初始化绑定/解绑事件 3 用 useState
来保存要改变的状态,通知组件渲染。
中间的计算过程咱们能够先不计,最终达到预期效果。
有关性能优化
这里说一下一个无关hooks自己的性能优化点,咱们在改变top
值的时候 ,尽可能用改变transform
Y值代替直接改变top值,缘由以下
1 transform
是可让GPU加速的CSS3
属性,在性能方便优于直接改变top
值。 2 在ios
端,固定定位频繁改变top
值,会出现闪屏兼容性。
useFormChange
背景:但咱们遇到例如 列表的表头搜索,表单提交等场景,须要逐一改变每一个formItem
的value
值,须要逐一绑定事件是比较麻烦的一件事,因而在平时的开发中,咱们来用一个hooks来统一管理表单的状态。
demo效果以下
获取表单
重置表单
useFormChange
设计思路须要实现功能
1 控制每个表单的值。 2 具备表单提交,获取整个表单数据功能。 3 点击重置,重置表单功能。
页面
import useFormChange from '../../hooks/useFormChange'
import './index.less'
const selector = ['嘿嘿', '哈哈', '嘻嘻']
function index() {
const [formData, setFormItem, reset] = useFormChange()
const {
name,
options,
select
} = formData
return <View className='formbox' > <View className='des' >文本框</View> <AtInput name='value1' title='名称' type='text' placeholder='请输入名称' value={name} onChange={(value) => setFormItem('name', value)} /> <View className='des' >单选</View> <AtRadio options={[ { label: '单选项一', value: 'option1' }, { label: '单选项二', value: 'option2' }, ]} value={options} onClick={(value) => setFormItem('options', value)} /> <View className='des' >下拉框</View> <Picker mode='selector' range={selector} onChange={(e) => setFormItem('select',selector[e.detail.value])} > <AtList> <AtListItem title='当前选择' extraText={select} /> </AtList> </Picker> <View className='btns' > <AtButton type='primary' onClick={() => console.log(formData)} >提交</AtButton> <AtButton className='reset' onClick={reset} >重置</AtButton> </View> </View>
}
复制代码
useFormChange
/* 表单/表头搜素hooks */
function useFormChange() {
const formData = useRef({})
const [, forceUpdate] = useState(null)
const handerForm = useMemo(()=>{
/* 改变表单单元项 */
const setFormItem = (keys, value) => {
const form = formData.current
form[keys] = value
forceUpdate(value)
}
/* 重置表单 */
const resetForm = () => {
const current = formData.current
for (let name in current) {
current[name] = ''
}
forceUpdate('')
}
return [ setFormItem ,resetForm ]
},[])
return [ formData.current ,...handerForm ]
}
复制代码
具体流程分析: 1 咱们用useRef
来缓存整个表单的数据。 2 用useState
单独作更新,不须要读取useState状态。 3 声明重置表单方法resetForm
, 设置表单单元项change
方法,
这里值得一提的问题是 为何用useRef
来缓存formData
数据,而不是直接用useState
。
缘由一 咱们都知道当用useMemo,useCallback
等API
的时候,若是引用了useState
,就要把useState值做为deps传入,否侧因为useMemo,useCallback
缓存了useState
旧的值,没法获得新得值,可是useRef
不一样,能够直接读取/改变useRef
里面缓存的数据。
缘由二 同步useState
useState
在一次使用useState
改变state
值以后,咱们是没法获取最新的state
,以下demo
function index(){
const [ number , setNumber ] = useState(0)
const changeState = ()=>{
setNumber(number+1)
console.log(number) //组件更新 -> 打印number为0 -> 并无获取到最新的值
}
return <View> <Button onClick={changeState} >点击改变state</Button> </View>
}
复制代码
咱们能够用 useRef
和 useState
达到同步效果
function index(){
const number = useRef(0)
const [ , forceUpdate ] = useState(0)
const changeState = ()=>{
number.current++
forceUpdate(number.current)
console.log(number.current) //打印值为1,组件更新,值改变
}
return <View> <Button onClick={changeState} >点击改变state</Button> </View>
}
复制代码
性能优化 用useMemo
来优化setFormItem ,resetForm
方法,避免重复声明,带来的性能开销。
背景:当咱们须要控制带分页,带查询条件的表格/列表的状况下。
1 统一管理表格的数据,包括列表,页码,总页码数等信息 2 实现切换页码,更新数据。
useTableRequset
设计思路1 咱们须要state
来保存列表数据,总页码数,当前页面等信息。 2 须要暴露一个方法用于,改变分页数据,重新请求数据。
解析来咱们看一下具体的实现方案。
页面
function getList(payload){
const query = formateQuery(payload)
return fetch('http://127.0.0.1:7001/page/tag/list?'+ query ).then(res => res.json())
}
export default function index(){
/* 控制表格查询条件 */
const [ query , setQuery ] = useState({})
const [tableData, handerChange] = useTableRequest(query,getList)
const { page ,pageSize,totalCount ,list } = tableData
return <View className='index' > <View className='table' > <View className='table_head' > <View className='col' >技术名称</View> <View className='col' >icon</View> <View className='col' >建立时间</View> </View> <View className='table_body' > { list.map(item=><View className='table_row' key={item.id} > <View className='col' >{ item.name }</View> <View className='col' > <Image className='col col_image' src={Icons[item.icon].default} /></View> <View className='col' >{ item.createdAt.slice(0,10) }</View> </View>) } </View> </View> <AtPagination total={Number(totalCount)} icon pageSize={Number(pageSize)} onPageChange={(mes)=>handerChange({ page:mes.current })} current={Number(page)} ></AtPagination> </View>
}
复制代码
useTableRequset
/* table 数据更新 hooks */
export default function useTableRequset(query, api) {
/* 是不是第一次请求 */
const fisrtRequest = useRef(false)
/* 保存分页信息 */
const [pageOptions, setPageOptions] = useState({
page: 1,
pageSize: 3
})
/* 保存表格数据 */
const [tableData, setTableData] = useState({
list: [],
totalCount: 0,
pageSize: 3,
page:1,
})
/* 请求数据 ,数据处理逻辑根后端协调着来 */
const getList = useMemo(() => {
return async payload => {
if (!api) return
const data = await api(payload || {...query, ...pageOptions})
if (data.code == 0) {
setTableData(data.data)
fisrtRequest.current = true
}
}
}, [])
/* 改变分页,从新请求数据 */
useEffect(() => {
fisrtRequest.current && getList({
...query,
...pageOptions
})
}, [pageOptions])
/* 改变查询条件。从新请求数据 */
useEffect(() => {
getList({
...query,
...pageOptions,
page: 1
})
}, [query])
/* 处理分页逻辑 */
const handerChange = useMemo(() => (options) => setPageOptions({...options }), [])
return [tableData, handerChange, getList]
}
复制代码
具体设计思路:
由于是demo
项目,咱们用本地服务器作了一个数据查询的接口,为的是模拟数据请求。
1 用一个useRef
来缓存是不是第一次请求数据。
2 用useState
保存返回的数据和分页信息。
3 用两个useEffect
分别处理,对于列表查询条件的更改,或者是分页状态更改,启动反作用钩子,从新请求数据,这里为了区别两种状态更改效果,实际也能够用一个effect
来处理。
4 暴露两个方法,分别是请求数据和处理分页逻辑。
性能优化
1 咱们用一个useRef
来缓存是不是第一次渲染,目的是为了,初始化的时候,两个useEffect
钩子都会执行,为了不重复请求数据。
2 对于请求数据和处理分页逻辑,避免重复声明,咱们用useMemo
加以优化。
须要注意的是,这里把请求数据后处理逻辑连同自定义hooks封装在一块儿,在实际项目中,要看和后端约定的数据返回格式来制定属于本身的hooks。
useDrapDrop
背景:用transform
和hooks
实现了拖拽效果,无需设置定位。
独立hooks
绑定独立的dom
元素,使之能实现自由拖拽效果。
useDrapDrop
具体实现思路须要实现的功能:
1 经过自定义hooks
计算出来的 x ,y 值,经过将transform
的translate
属性设置当前计算出来的x,y
实现拖拽效果。
2 自定义hooks
能抓取当前dom
元素容器。
页面
export default function index (){
const [ style1 , dropRef ]= useDrapDrop()
const [style2,dropRef2] = useDrapDrop()
return <View className='index'> <View className='drop1' ref={dropRef} style={{transform:`translate(${style1.x}px, ${style1.y}px)`}} >drop1</View> <View className='drop2' ref={dropRef2} style={{transform:`translate(${style2.x}px, ${style2.y}px)`}} >drop2</View> <View className='drop3' >drop3</View> </View>
}
复制代码
注意点: 咱们没有用,left
,和top
来改变定位,css3
的transform
可以避免浏览器的重排和回流,性能优化上要强于直接改变定位的top,left值。 因为咱们模拟环境考虑到是h5移动端,因此用 webview
的 touchstart , touchmove ,ontouchend
事件来进行模拟。
核心代码-useDrapDrop
/* 移动端 -> 拖拽自定义效果(不使用定位) */
function useDrapDrop() {
/* 保存上次移动位置 */
const lastOffset = useRef({
x:0, /* 当前x 值 */
y:0, /* 当前y 值 */
X:0, /* 上一次保存X值 */
Y:0, /* 上一次保存Y值 */
})
/* 获取当前的元素实例 */
const currentDom = useRef(null)
/* 更新位置 */
const [, foceUpdate] = useState({})
/* 监听开始/移动事件 */
const [ ontouchstart ,ontouchmove ,ontouchend ] = useMemo(()=>{
/* 保存left right信息 */
const currentOffset = {}
/* 开始滑动 */
const touchstart = function (e) {
const targetTouche = e.targetTouches[0]
currentOffset.X = targetTouche.clientX
currentOffset.Y = targetTouche.clientY
}
/* 滑动中 */
const touchmove = function (e){
const targetT = e.targetTouches[0]
let x =lastOffset.current.X + targetT.clientX - currentOffset.X
let y =lastOffset.current.Y + targetT.clientY - currentOffset.Y
lastOffset.current.x = x
lastOffset.current.y = y
foceUpdate({
x,y
})
}
/* 监听滑动中止事件 */
const touchend = () => {
lastOffset.current.X = lastOffset.current.x
lastOffset.current.Y = lastOffset.current.y
}
return [ touchstart , touchmove ,touchend]
},[])
useLayoutEffect(()=>{
const dom = currentDom.current
dom.ontouchstart = ontouchstart
dom.ontouchmove = ontouchmove
dom.ontouchend = ontouchend
},[])
return [ { x:lastOffset.current.x,y:lastOffset.current.y } , currentDom]
}
复制代码
具体设计思路:
1 对于拖拽效果,咱们须要实时获取dom
元素的位置信息,因此咱们须要一个useRef
来抓取dom元素。
2 因为咱们用的是transfrom
改变位置,因此须要保存一下当前位置和上一次transform
的位置,因此咱们用一个useRef来缓存位置。
3 咱们经过useRef
改变x,y
值,可是须要渲染新的位置,因此咱们用一个useState
来专门产生组件更新。
4 初始化的时候咱们须要给当前的元素绑定事件,由于在初始化的时候咱们可能精确须要元素的位置信息,因此咱们用useLayoutEffect
钩子来绑定touchstart , touchmove ,ontouchend
等事件。
以上就是我在react自定义hooks上的总结,和一些实际的应用场景,咱们项目中,80%的表单列表场景,均可以用上述hooks来解决。
纸上得来终觉浅,绝知此事要躬行,真正玩好,玩转hooks,是一个日积月累的过程,怎么去设计一个符合业务场景的hooks,须要咱们不断的实战,不断的总结。
最后你们以为还不错的话,就 点赞 + 关注 一波,持续分享技术文章。
公众号:前端Sharing