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 customuseScrollDesign 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 customuseFormChangeDesign 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 customuseTableRequsetDesign 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 useDrapDropConcrete 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