preface
Since react16.8, react-hooks were born, I have been using hooks in my work. In the react projects I contacted for more than a year, I gradually used function stateless components instead of classs stateless components. I have also summarized some experiences during this period. Especially for the last three months of the project, a few custom hooks to deal with the repetitive logic in the company project, the overall feeling is good. Today I would like to share with you my experience on react-hooks and some design ideas of custom hooks in my work.
Custom hooks design
Back to the question? What are hooks?
After the react – hooks is react16.8, react the new hook API, the purpose is to increase the reusability of the code, logic, make up for the stateless component without life cycle, there is no data management status, the defects of the state. In the author’s opinion, the react-hooks’ idea and original intention is to form an independent rendering environment by integrating components, granulation and unitization to reduce rendering times and optimize performance.
How do you use react-hooks?
What are custom hooks
Custom hooks are an extension of react-hooks, which can be formulated according to business needs, with more emphasis on logical units. Depending on the business scenario, what do we need react-hooks to do? How to encapsulate a piece of logic and reuse it? This is the original intention of custom hooks.
How to design a custom hooks, design specification
Logic + Components
Hooks focus on logical reuse, which is our project, not just on component reuse. Hooks allow us to seal a section of common logic. It will be available right out of the box when we need it.
Custom hooks- driver conditions
Hooks are essentially a function. The execution of the function determines the execution context of the component itself with the stateless component. Each execution of a function (which is essentially an update to the component) executes a custom hooks execution, so that the component itself executes exactly as the hooks execute.
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Let us draw a picture to represent the above relation.
Custom hooks- generic pattern
We designed custom react-hooks to look like this.
const[ xxx , ... ] = useXXX(parameter A, parameter B...)Copy the code
When we write custom hooks, we need to focus on what is passed in and what is returned. What’s coming back is what we really need. More like a factory, the raw materials are processed and finally returned to us. As shown in the figure below
Custom hooks- conditional
If custom hooks are not well designed, such as returning a function that changes state but is not conditioned, they can cause unnecessary context executions, or even loop renderings of components.
For example, we write a very simple hooks to format an array from lowercase to uppercase.
import React , { useState } from 'react'
/* Custom hooks are used to format arrays from lowercase to uppercase */
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
Copy the code
As in the above problem, we format the list array passed by the parent component and change it from lowercase to uppercase, but when we click Add. Ideally, the array does not need to be reformatted, but the actual format is followed. There is a definite performance overhead.
So when we set custom hooks, make sure to include the conditional – performance overhead.
So let’s do it this way.
function useFormatList(list) {
return useMemo(() = > list.map(item= > {
console.log(1111)
return item.toUpperCase()
}), [])
}
Copy the code
The above problems have been brilliantly solved.
So, a good custom hooks should be used in conjunction with useMemo,useCallback, etc.
Custom hooks combat
Preparation: Set up the Demo style project
To link the actual business scenarios with the custom hooks, HERE I build a mobile React project using Taro – H5. Describes a scenario where you use custom hooks in real work.
Demo project address: custom hooks, Demo project
There will be more custom hooks to be updated, or if you are interested, you can keep an eye on this project, or you can maintain it together.
The project structure
The page folder contains the custom hooks to display the demo page. The hooks folder contains the custom hooks content.
Display effect
Each listItem records the effects of each completed custom hooks display, and others to follow. Let’s look at the hooks implementation.
Actual combat one: control scroll bar – top effect, gradient effect –useScroll
Background: A company H5 project, in the process of scrolling bar, need to control the gradient + height + top effect.
1 Effect
1 First of all, the red color block has the top effect. 2. Pink block, fixed on top but slightly offset, with gradual transparency effect.
2 the customuseScroll
Design ideas
Functions to be implemented:
1 Monitor the scroll bar. Calculate the threshold value, gradient value and transparency of the top. 3 Change the State render view.
Ok, let’s do this with a hooks.
page
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 save control transparency, top value, top switch and other variables */
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>
}
Copy the code
We save variables such as transparency, top value, top switch and so on by a scrollOptions, and then by returning a REF as a collector for DOM elements. The following are the hooks if implemented.
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
/* Control transparency */
const computerOpacty = 1 - scrollY / 160
/* Control the top effect */
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]
}
Copy the code
Specific design ideas
1 we use a useRef to get the required element. 2 We use useEffect to initialize the bind/unbind event. 3 We use useState to save the state to change and notify the component to render.
We can ignore the calculation process in the middle, and finally achieve the desired effect.
About performance optimization
Here is a performance optimization that has nothing to do with the hooks themselves. When changing the top value, we try to change the transform Y value instead of changing the top value
1 Transform is a CSS3 attribute that can be accelerated by the GPU. It is better than changing the top value directly in performance. 2 On the ios, if the top value is frequently changed, the splash screen compatibility is displayed.
Practice two: Control form state –useFormChange
Background: When we encounter scenarios such as list header search, form submission, etc., we need to change the value of each formItem one by one. It is difficult to bind events one by one, so we use a hooks to manage the form state.
1 Effect
The demo effect is as follows
Get the form
Reset the form
2 the customuseFormChange
Design ideas
Need to implement functionality
1 controls the value of each form. 2 has the form submission, obtain the whole form data function. 3 Click Reset to reset the form function.
page
import useFormChange from '.. /.. /hooks/useFormChange'
import './index.less'
const selector = ['hey'.'ha ha'.'xi xi']
function index() {
const [formData, setFormItem, reset] = useFormChange()
const {
name,
options,
select
} = formData
return <View className='formbox' >
<View className='des' >The text box</View>
<AtInput name='value1' title='name' type='text' placeholder='Please enter a name' value={name} onChange={(value)= > setFormItem('name', value)}
/>
<View className='des' >The radio</View>
<AtRadio
options={[
{ label:'Option one ',value: 'option1'}, {label:'Option two ',value: 'option2'}},]value={options}
onClick={(value)= > setFormItem('options', value)}
/>
<View className='des' >A drop-down box</View>
<Picker mode='selector' range={selector} onChange={(e)= > setFormItem('select',selector[e.detail.value])} >
<AtList>
<AtListItem
title='Current selection'
extraText={select}
/>
</AtList>
</Picker>
<View className='btns' >
<AtButton type='primary' onClick={()= >The console. The log (formData)} > submit</AtButton>
<AtButton className='reset' onClick={reset} >reset</AtButton>
</View>
</View>
}
Copy the code
useFormChange
/* form/header search */
function useFormChange() {
const formData = useRef({})
const [, forceUpdate] = useState(null)
const handerForm = useMemo(() = >{
/* Change the form cell entry */
const setFormItem = (keys, value) = > {
const form = formData.current
form[keys] = value
forceUpdate(value)
}
/* Reset the form */
const resetForm = () = > {
const current = formData.current
for (let name in current) {
current[name] = ' '
}
forceUpdate(' ')}return [ setFormItem ,resetForm ]
},[])
return [ formData.current ,...handerForm ]
}
Copy the code
Specific process analysis: 1 we use useRef to cache the entire form data. 2 Use the useState to update the data separately, without reading the useState status. ResetForm; set the change method of the form unit;
The question worth mentioning here is why useRef is used to cache formData data instead of useState directly.
If you use useMemo,useCallback, etc., you need to pass in the value of useState as deps. If you use useMemo,useCallback, etc., you need to pass in the value of useState as deps. But useRef is different, you can directly read/change the data cached in useRef.
Cause 2 Synchronize the useState. After useState is used to change the state value, we cannot obtain the latest state
function index(){
const [ number , setNumber ] = useState(0)
const changeState = () = >{
setNumber(number+1)
console.log(number) // Component update -> print number 0 -> did not get the latest value
}
return <View>
<Button onClick={changeState} >I'm gonna change State</Button>
</View>
}
Copy the code
We can use useRef and useState to synchronize
function index(){
const number = useRef(0)
const [ , forceUpdate ] = useState(0)
const changeState = () = >{
number.current++
forceUpdate(number.current)
console.log(number.current) // Print a value of 1, the component updates, and the value changes
}
return <View>
<Button onClick={changeState} >I'm gonna change State</Button>
</View>
}
Copy the code
Use useMemo to optimize the setFormItem and resetForm methods to avoid the performance overhead of repeated declarations.
Control sheets/Lists -useTableRequset
Background: When we need to control a table/list with paging and query criteria.
1 Effect
1 unified management of table data, including list, page number, total page number and other information 2 Achieve page number switch, update data.
2 the customuseTableRequset
Design ideas
We need state to hold list data, total page number, current page, etc. You need to expose a method to change the paging data and request the new data.
Let’s take a look at the specific implementation scheme.
page
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(){
/* Control form query conditions */
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' >Technical name</View>
<View className='col' >icon</View>
<View className='col' >Creation time</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>
}
Copy the code
useTableRequset
/* update table hooks */
export default function useTableRequset(query, api) {
/* Whether it is the first request */
const fisrtRequest = useRef(false)
/* Save the paging information */
const [pageOptions, setPageOptions] = useState({
page: 1.pageSize: 3
})
/* Save table data */
const [tableData, setTableData] = useState({
list: [].totalCount: 0.pageSize: 3.page:1,})/* Request data, data processing logic root back-end coordination */
const getList = useMemo(() = > {
return async payload => {
if(! api)return
const data = awaitapi(payload || {... query, ... pageOptions})if (data.code == 0) {
setTableData(data.data)
fisrtRequest.current = true}}}, [])/* Change the page and request data */ again
useEffect(() = >{ fisrtRequest.current && getList({ ... query, ... pageOptions }) }, [pageOptions])/* Change the query conditions. Request data */ again
useEffect(() = >{ getList({ ... query, ... pageOptions,page: 1
})
}, [query])
/* Handle paging logic */
const handerChange = useMemo(() = > (options) = >setPageOptions({... options }), [])return [tableData, handerChange, getList]
}
Copy the code
Specific design ideas:
Since this is a demo project, we made a data query interface with the local server in order to simulate the data request.
1 use a useRef to cache the first request.
2 Use useState to save the returned data and paging information.
3 use two separate useEffects to process, for the list query condition changes, or paging state changes, start the side effect hook, re-request data, here to distinguish between the two state change effects, you can actually use one effect to process.
4 exposes two methods, one for requesting data and one for handling paging logic.
Performance optimization
We use a useRef to cache whether it is the first time to render, so that, at initialization, both useEffect hooks are executed to avoid repeated requests for data.
2 For requesting data and processing paging logic to avoid repeated declarations, we use useMemo to optimize.
It is important to note that the request data postprocessing logic is packaged with custom hooks. In a real project, you would need to define your own hooks by looking at the format of the data returned by the backend convention.
Actual combat four: control drag effect –useDrapDrop
Background: Use transform and hooks to implement drag and drop effects without setting positioning.
1 Effect
Independent hooks bind independent DOM elements, making them drag-and-drop free.
2 useDrapDrop
Concrete implementation ideas
Functions to be implemented:
X,y = x,y = y = x,y = y = x,y = y = x,y = y = x,y = y = x,y = y
2. Hooks can grab the current DOM element container.
page
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>
}
Copy the code
Css3’s transform avoids browser reordering and backflow, and is better than changing the top,left values of the transform. Since our simulation environment is h5 mobile, we use the WebView’s TouchStart, TouchMove, and OnTouchEnd events to simulate.
Core code –useDrapDrop
/* Mobile -> Drag custom effects (no positioning) */
function useDrapDrop() {
/* Save the last move */
const lastOffset = useRef({
x:0./* Current x value */
y:0./* Current y value */
X:0./* Save the last X value */
Y:0./* Save the last Y value */
})
/* Get the current element instance */
const currentDom = useRef(null)
/* Update location */
const [, foceUpdate] = useState({})
/* Listen for start/move events */
const [ ontouchstart ,ontouchmove ,ontouchend ] = useMemo(() = >{
/* Save the left and right information */
const currentOffset = {}
/* Start sliding */
const touchstart = function (e) {
const targetTouche = e.targetTouches[0]
currentOffset.X = targetTouche.clientX
currentOffset.Y = targetTouche.clientY
}
/* Slide in */
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
})
}
/* Listen for slide stop events */
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]
}
Copy the code
Specific design ideas:
1 for drag-and-drop effects, we need to get the position information of DOM elements in real time, so we need a useRef to grab DOM elements.
2 Since we use transfrom to change the position, we need to save the current position and the position of the last transform, so we use a useRef to cache the position.
3 We change x and y values with useRef, but we need to render the new location, so we use a useState specifically to generate component updates.
We need to bind an event to the current element during initialization, because we may need the precise location of the element during initialization, so we use the useLayoutEffect hook to bind the event touchStart, TouchMove, onTouchEnd, etc.
conclusion
Above is the summary of my react custom hooks, and some practical application scenarios. In our project, 80% of the form list scenarios can be solved with the above hooks.
The paper comes zhongjue shallow, absolutely know this matter to practice, really play well, play with hooks, is a process of accumulation, how to design a business scene hooks, we need to continue the actual combat, continuous summary.
At the end of the day, if you think it’s ok, you can like it and follow it, and continue to share technical articles.
Official Account: front-end Sharing