Играйте с реактивными хуками, пользовательскими шаблонами дизайна хуков и их реальной борьбой

React.js
Играйте с реактивными хуками, пользовательскими шаблонами дизайна хуков и их реальной борьбой

предисловие

Поскольку реакция 16.8,react-hooksС момента своего рождения он использовался в работеhooks, Более года, проект реакции, с которым я связался, постепенно использовалfunctionВместо этого компоненты без состоянияclasssКомпонент состояния состояния также обобщает некоторые события в течение периода. Специально для недавнего трехмесячного проекта я использовал небольшие кастомные хуки, чтобы справиться с повторяющейся логикой в ​​проектах компании, и общее ощущение неплохое. Сегодня я расскажу вам о своей работеreact-hooksОпыт, и некоторые дизайнерские идеи нестандартных крючков, и поделиться опытом работы со всеми.

Индивидуальный дизайн крючков

Снова вернуться к этому вопросу? Что такое крючки.

react-hooksПосле реагирования 16.8 новая API крюка реактивных направлена ​​на увеличение повторного использования и логики кода, а для того, чтобы составить дефекты, которые не имеют жизненного цикла, не имеют жизненного цикла и отсутствие состояния управления данными. Автор думает,react-hooksИдея и первоначальное намерение заключаются в том, чтобы детализировать и объединить компоненты для формирования независимой среды рендеринга, уменьшить количество рендерингов и оптимизировать производительность.

Еще одна статья, которую могут прочитать партнеры, не разбирающиеся в реактивных хуках:Как используются реактивные хуки?

Что такое нестандартные хуки

Пользовательские крючки вreact-hooksВ качестве расширения основы можно сформулировать крючки для удовлетворения потребностей бизнеса в соответствии с потребностями бизнеса, при этом больше внимания уделяется логическим единицам. Согласно различным бизнес-сценариям, что нам нужноreact-hooksЧто делать, как инкапсулировать часть логики и использовать ее повторно — такова изначальная цель кастомных хуков.

Как разработать индивидуальный крючок, спецификация дизайна

Логика + Компоненты

крючки сосредоточиться налогическое мультиплексирование, это наш проект не только на уровне повторного использования компонентов. Хуки позволяют нам инкапсулировать часть общей логики. Он будет доступен из коробки, когда он нам понадобится.

Пользовательские условия, управляемые хуками

hooksПо сути функция. Выполнение функции определяет контекст выполнения самого компонента без состояния. Каждый раз, когда функция выполняется (по сути, обновление компонента), выполняется настройкаhooksВидно, что выполнение самого компонента точно такое же, как выполнение хуков.

ТакpropМодификации,useState,useReducerИспользование — это условие обновления компонента без сохранения состояния, тогда это условие управляет выполнением хуков. Мы используем картинку, чтобы представить вышеуказанные отношения.

Пользовательские крючки - общий шаблон

На заказ разработан намиreact-hooksДолжен быть таким длинным.

const [ xxx , ... ] = useXXX(参数A,参数B...)

Когда мы пишем пользовательские хуки, нам нужноособенный ~ особенный ~ особенныйОсновное внимание уделяетсяпройти в чем,что вернуть. Возвращается то, что нам действительно нужно. Больше похоже на фабрику, сырье обрабатывается и, наконец, возвращается к нам. как показано ниже

Пользовательские хуки - условные

Если пользовательские хуки спроектированы неправильно, например возвращают функцию, которая изменяет состояние, но не добавляет условий, это может привести к ненужному выполнению контекста и даже к циклическому выполнению рендеринга компонентов.

Например: мы пишем очень простой хук дляОтформатируйте массив для преобразования нижнего регистра в верхний..


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. Несомненно, увеличение производительности.

Поэтому, когда мы настраиваем пользовательские хуки, мы должны добавить условную производительность.

Итак, давайте разберемся с этим так.

function useFormatList(list) {
    return useMemo(() => list.map(item => {
        console.log(1111)
        return item.toUpperCase()
    }), [])
}

Шикарное решение вышеуказанной проблемы.

Таким образом, полезные пользовательские хуки должны быть сопоставленыuseMemo ,useCallbackЖдатьapiиспользовать вместе.

Пользовательские хуки в действии

Подготовка: создание демо-проекта

Для включения реальных бизнес-сценариев и пользовательскихhooksСоединенные вместе, я использую здесьtaro-h5построил мобильныйreactпроект. Используется для описания обычая, используемого в реальной работе.hooksместо действия.

адрес демо-проекта:Пользовательские хуки, демонстрационные проекты

В будущем будет обновлено больше пользовательских крючков, или заинтересованные студенты могут обратить внимание на этот проект или также могут поддерживать этот проект вместе.

Структура проекта

pageПапка включает отображение пользовательских хуковdemoстраница,hooksВнутри папки находится содержимое пользовательских хуков.

Отображение результатов

каждыйlistItemЗапишите эффект отображения каждого выполненного пользовательского хука, а также других один за другим.hooks.我们接下来看看hooks具体实现。

Фактический бой 1: Управляйте полосой прокрутки — эффект потолка, эффект градиента —useScroll

Предыстория: Один из проектов компании h5 должен управлять эффектом градиента + высоты + потолка во время процесса прокрутки полосы прокрутки.

1 для достижения эффекта

1 Во-первых, красный блок создает эффект потолка. 2 Блок розового цвета фиксируется сверху, но имеет небольшое смещение, а также эффект постепенного перехода в прозрачность.

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сохранить прозрачность,topvalue , потолочные выключатели и другие переменные, а затем вернутьrefтак какdomСборщик элементов. Следующий шаг — если хуки реализованы.

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 чтобы сохранить состояние для изменения и уведомить компонент для рендеринга.

Мы можем игнорировать процесс вычисления в середине и, наконец, добиться ожидаемого эффекта.

Об оптимизации производительности

Вот пункт оптимизации производительности, который не имеет никакого отношения к самим хукам, мы меняемtopзначение, попробуйте использовать изменениеtransformВместо прямого изменения верхнего значения значение Y по следующим причинам

1 transformGPU-ускорениеCSS3свойства, с точки зрения удобства выполнения лучше, чем прямое изменениеtopстоимость. 2 дюймаiosконец, фиксированное позиционирование часто меняетсяtopзначение, происходит совместимость экрана-заставки.

Фактический бой 2: Состояние формы управления-useFormChange

Предыстория: Но мы сталкиваемся с такими сценариями, как поиск в заголовке списка, отправка формы и т. д., и нам нужно изменить каждый из них один за другим.formItemизvalueСвязывать события одно за другим сложнее, поэтому в обычной разработке мы используем хук для унифицированного управления состоянием формы.

1 Добиться эффекта

Демонстрационный эффект выглядит следующим образом

получить форму

Сброс форма

2 Настроить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, следующее демо

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метод, чтобы избежать повторных объявлений, что приводит к снижению производительности.

Фактический бой третий: контрольная таблица/список - useTableRequset

Предыстория: Когда нам нужно управлять таблицей/списком с разбиением на страницы и условиями запроса.

1 Добиться эффекта

1 Унифицированное управление табличными данными, включая список, номер страницы, общее количество страниц и другую информацию 2 Реализуйте переключение номеров страниц и обновите данные.

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

useRef

useState

3 использовать дваuseEffectОбработайте их отдельно.Для изменения условия запроса списка или изменения состояния подкачки запустите хук побочного эффекта и повторно запросите данные.Чтобы различить эффект двух изменений состояния, на самом деле можно использовать одно из них.effectобрабатывать.

4 Предоставьте два метода, а именно запрос данных и логику обработки пейджинга.

оптимизация производительности

1 Мы используемuseRefЧтобы кэшировать, является ли это первым рендерингом, цель состоит в том, чтобы при инициализации дваuseEffectХуки будут выполняться, чтобы избежать повторных запросов данных.

2 Для запроса данных и обработки логики пейджинга, чтобы избежать повторных объявлений, мы используемuseMemoбыть оптимизированным.

Стоит отметить, что логика постобработки данных запроса инкапсулируется вместе с кастомными хуками, в реальных проектах нужно формулировать свои хуки по согласованному с бэкендом формату возврата данных.

Фактический бой четыре: контролировать эффект перетаскивания-useDrapDrop

фон: сtransformа такжеhooksЭффект перетаскивания достигается без настройки позиционирования.

1 Добиться эффекта

Независимостьhooksсвязать независимымdomэлемент, чтобы он мог достичь эффекта свободного перетаскивания.

2 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Можно браузер, чтобы избежать перегруппировки и с обратным холодильником, изменение ориентации сильнее, чем оптимизация прямой производительности сверху, левое значение. Как мы рассмотрим, что среда симуляции - это мобильный терминал 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и т.д. события.

Суммировать

Выше приведено мое резюме о пользовательских хуках реагирования и некоторых практических сценариях применения.В нашем проекте 80% сценариев списка форм могут быть решены с помощью вышеуказанных хуков.

Это поверхностно на бумаге, и я ничего не знаю об этом., Действительно хорошо играть, играть крючки, это процесс, который накапливается с течением времени, как разработать крючок, который соответствует бизнес-сценарию, требует от нас постоянной борьбы и подведения итогов.

В конце концов, если вы считаете, что это неплохо, ставьте лайк + следите за волной и продолжайте делиться техническими статьями.

Общедоступный номер: Front-end Sharing