自從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