10分钟教你手写8个经常使用的自定义hooks

前言

Hook 是 React 16.8 的新增特性。它可让你在不编写 class 的状况下使用 state 以及其余的 React 特性。本文是一篇以实战为主的文章,主要讲解实际项目中如何使用hooks以及一些最佳实践,不会一步步再介绍一遍react hooks的由来和基本使用,由于写hooks的文章不少,并且官网对于react hooks的介绍也很详细,因此你们不熟悉的能够看一遍官网javascript

你将收获

  • react hooks核心API使用以及注意事项
  • 实现一个小型redux
  • 实现自定义的useState
  • 实现自定义的useDebounce
  • 实现自定义的useThrottle
  • 实现自定义useTitle
  • 实现自定义的useUpdate
  • 实现自定义的useScroll
  • 实现自定义的useMouse
  • 实现自定义的createBreakpoint

正文

1. react hooks核心API使用注意事项

笔者在项目中经常使用的hooks主要有useState, useEffect,useCallback,useMemo,useRef。固然像useReducer, useContext, createContext这些钩子在H5游戏中也会使用,由于不须要维护错综复杂的状态,因此咱们彻底能够由上述三个api构建一个本身的小型redux(后面会介绍如何实现小型的redux)来处理全局状态,可是对于企业复杂项目来讲,咱们使用redux及其生态会更加高效一些。css

咱们在使用hooks和函数组件编写咱们的组件时,第一个要考虑的就是渲染性能,咱们知道若是在不作任何处理时,咱们在函数组件中使用setState都会致使组件内部从新渲染,一个比较典型的场景: html

当咱们在容器组件手动更新了任何state时,容器内部的各个子组件都会从新渲染,为了不这种状况出现,咱们通常都会使用memo将函数组件包裹,来达到class组件的pureComponent的效果:

import React, { memo, useState, useEffect } from 'react'
const A = (props) => {
  console.log('A1')
  useEffect(() => {
    console.log('A2')
  })
  return <div>A</div>
}

const B = memo((props) => {
  console.log('B1')
  useEffect(() => {
    console.log('B2')
  })
  return <div>B</div>
})

const Home = (props) => {
  const [a, setA] = useState(0)
  useEffect(() => {
    console.log('start')
    setA(1)
  }, [])
  return <div><A n={a} /><B /></div> } 复制代码

当咱们将B用memo包裹后,状态a的更新将不会致使B组件从新渲染。其实仅仅优化这一点还远远不够的,好比说咱们子组件用到了容器组件的某个变量或者函数,那么当容器内部的state更新以后,这些变量和函数都会从新赋值,这样就会致使即便子组件使用了memo包裹也仍是会从新渲染,那么这个时候咱们就须要使用useMemouseCallback了。前端

useMemo能够帮咱们将变量缓存起来,useCallback能够缓存回调函数,它们的第二个参数和useEffect同样,是一个依赖项数组,经过配置依赖项数组来决定是否更新。vue

import React, { memo, useState, useEffect, useMemo } from 'react'
const Home = (props) => {
  const [a, setA] = useState(0)
  const [b, setB] = useState(0)
  useEffect(() => {
    setA(1)
  }, [])

  const add = useCallback(() => {
    console.log('b', b)
  }, [b])

  const name = useMemo(() => {
    return b + 'xuxi'
  }, [b])
  return <div><A n={a} /><B add={add} name={name} /></div>
}
复制代码

此时a更新后B组件不会再从新渲染。以上几个优化步骤主要是用来优化组件的渲染性能,咱们平时还会涉及到获取组件dom和使用内部闭包变量的情景,这个时候咱们就可使用useRefjava

useRef返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变。node

function AutoFocusIpt() {
  const inputEl = useRef(null);
  const useEffect(() => {
    // `current` 指向已挂载到 DOM 上的文本输入元素
    inputEl.current.focus();
  }, []);
  return (
    <>
      <input ref={inputEl} type="text" />
    </>
  );
}
复制代码

除了以上应用场景外,咱们还能够利用它来实现class组件的setState的功能,具体实现后面会有介绍。react

2. 实现一个小型redux

实现redux咱们会利用以前说的useReducer, useContext, createContext这三个api,至于如何实现redux,其实网上也有不少实现方式,这里笔者写一个demo供你们参考:webpack

// actionType.js
const actionType = {
  INSREMENT: 'INSREMENT',
  DECREMENT: 'DECREMENT',
  RESET: 'RESET'
}
export default actionType

// actions.js
import actionType from './actionType'
const add = (num) => ({
    type: actionType.INSREMENT,
    payload: num
})

const dec = (num) => ({
    type: actionType.DECREMENT,
    payload: num
})

const getList = (data) => ({
    type: actionType.GETLIST,
    payload: data
})
export {
    add,
    dec,
    getList
}

// reducer.js
function init(initialCount) {
  return {
    count: initialCount,
    total: 10,
    user: {},
    article: []
  }
}

function reducer(state, action) {
  switch (action.type) {
    case actionType.INSREMENT:
      return {count: state.count + action.payload};
    case actionType.DECREMENT:
      return {count: state.count - action.payload};
    case actionType.RESET:
      return init(action.payload);
    default:
      throw new Error();
  }
}

export { init, reducer }

// redux.js
import React, { useReducer, useContext, createContext } from 'react'
import { init, reducer } from './reducer'

const Context = createContext()
const Provider = (props) => {
  const [state, dispatch] = useReducer(reducer, props.initialState || 0, init);
    return (
      <Context.Provider value={{state, dispatch}}> { props.children } </Context.Provider> ) } export { Context, Provider } 复制代码

其实还有更优雅的方式实现,笔者以前也写了几套redux模版,欢迎一块儿讨论哈。接下来咱们进入正文,来带你们实现几个经常使用的自定义hooks。css3

3. 实现自定义的useState,支持相似class组件setState方法

熟悉react的朋友都知道,咱们使用class组件更新状态时,setState会支持两个参数,一个是更新后的state或者回调式更新的state,另外一个参数是更新后的回调函数,以下面的用法:

this.setState({num: 1}, () => {
    console.log('updated')
})
复制代码

可是hooks函数的useState第二个参数回调支持相似class组件的setState的第一个参数的用法,并不支持第二个参数回调,可是不少业务场景中咱们又但愿hooks组件能支持更新后的回调这一方法,那该怎么办呢?其实问题也很简单,咱们只要对hooks原理和api很是清楚的话,就能够经过自定义hooks来实现,这里咱们借助上面提到的useRef和useEffect配合useState来实现这一功能。

注:react hooks的useState必定要放到函数组件的最顶层,不能写在ifelse等条件语句当中,来确保hooks的执行顺序一致,由于useState底层采用链表结构实现,有严格的顺序之分。

咱们先来看看实现的代码:

import { useEffect, useRef, useState } from 'react'

const useXState = (initState) => {
    const [state, setState] = useState(initState)
    let isUpdate = useRef()
    const setXState = (state, cb) => {
      setState(prev => {
        isUpdate.current = cb
        return typeof state === 'function' ? state(prev) : state
      })
    }
    useEffect(() => {
      if(isUpdate.current) {
        isUpdate.current()
      }
    })
  
    return [state, setXState]
  }

export default useXState
复制代码

笔者利用useRef的特性来做为标识区分是挂载仍是更新,当执行setXstate时,会传入和setState如出一辙的参数,而且将回调赋值给useRef的current属性,这样在更新完成时,咱们手动调用current便可实现更新后的回调这一功能,是否是很巧妙呢?

4. 实现自定义的useDebounce

节流函数和防抖函数想必你们也不陌生,为了让咱们在开发中更优雅的使用节流和防抖函数,咱们每每须要让某个state也具备节流防抖的功能,或者某个函数的调用,为了不频繁调用,咱们每每也会采起节截流防抖这一思想,原生的节流防抖函数可能如一下代码所示:

// 节流
function throttle(func, ms) {
    let previous = 0;
    return function() {
        let now = Date.now();
        let context = this;
        let args = arguments;
        if (now - previous > ms) {
            func.apply(context, args);
            previous = now;
        }
    }
}

// 防抖
function debounce(func, ms) {
    let timeout;
    return function () {
        let context = this;
        let args = arguments;

        if (timeout) clearTimeout(timeout);
        
        timeout = setTimeout(() => {
            func.apply(context, args)
        }, ms);
    }
}
复制代码

那么咱们首先来实现一下防抖的hooks,代码以下:

import { useEffect, useRef } from 'react'

const useDebounce = (fn, ms = 30, deps = []) => {
    let timeout = useRef()
    useEffect(() => {
        if (timeout.current) clearTimeout(timeout.current)
        timeout.current = setTimeout(() => {
            fn()
        }, ms)
    }, deps)

    const cancel = () => {
        clearTimeout(timeout.current)
        timeout = null
    }
  
    return [cancel]
  }

export default useDebounce
复制代码

由代码能够知道,useDebounce接受三个参数,分别为回调函数,时间间隔以及依赖项数组,它暴露了cancel API,主要是用来控制什么时候中止防抖函数用的。具体使用以下:

// ...
import { useDebounce } from 'hooks'
const Home = (props) => {
  const [a, setA] = useState(0)
  const [b, setB] = useState(0)
  const [cancel] = useDebounce(() => {
    setB(a)
  }, 2000, [a])

  const changeIpt = (e) => {
    setA(e.target.value)
  }
  return <div> <input type="text" onChange={changeIpt} /> { b } { a } </div> } 复制代码

以上代码就实现了state的debounce的功能,具体效果以下图所示:

5. 实现自定义的useThrottle

同理,咱们继续来实现节流的hooks函数。直接上代码:

import { useEffect, useRef, useState } from 'react'

const useThrottle = (fn, ms = 30, deps = []) => {
    let previous = useRef(0)
    let [time, setTime] = useState(ms)
    useEffect(() => {
        let now = Date.now();
        if (now - previous.current > time) {
            fn();
            previous.current = now;
        }
    }, deps)

    const cancel = () => {
        setTime(0)
    }
  
    return [cancel]
  }

export default useThrottle
复制代码

代码和自定义useDebounce相似,但须要注意一点就是为了实现cancel功能,咱们使用了内部state来处理,经过控制时间间隔来取消节流效果,固然还有不少其余方法能够实现这个hooks API。具体效果以下:

6. 实现自定义useTitle

自定义的useTitle hooks其实使用场景也不少,由于咱们目前大部分项目都是采用SPA或者混合SPA的方式开发,对于不一样的路由咱们一样但愿想多页应用同样能切换到对应的标题,这样可让用户更好的知道页面的主题和内容。这个hooks的实现也很简单,咱们直接上代码:

import { useEffect } from 'react'

const useTitle = (title) => {
    useEffect(() => {
      document.title = title
    }, [])
  
    return
  }

export default useTitle
复制代码

以上代码能够看出咱们只须要在useEffect中设置document的title属性就行了,咱们不须要return任何值。其实还有更优雅和复杂的实现方法,这里就不一一举例了。具体使用以下:

const Home = () => {
    // ...
    useTitle('趣谈前端')
    
    return <div>home</div>
}
复制代码

7. 实现自定义的useUpdate

咱们都知道若是想让组件从新渲染,咱们不得不更新state,可是有时候业务须要的state是不必更新的,咱们不能仅仅为了让组件会从新渲染而强制让一个state作无心义的更新,因此这个时候咱们就能够自定义一个更新的hooks来优雅的实现组件的强制更新,实现代码以下:

import { useState } from 'react'

const useUpdate = () => {
    const [, setFlag] = useState()
    const update = () => {
        setFlag(Date.now())
    }
  
    return update
  }

export default useUpdate
复制代码

以上代码能够发现,咱们useUpdate钩子返回了一个函数,该函数就是用来强制更新用的。使用方法以下:

const Home = (props) => {
  // ...
  const update = useUpdate()
  return <div> {Date.now()} <div><button onClick={update}>update</button></div> </div>
}
复制代码

效果以下:

8. 实现自定义的useScroll

自定义的useScroll也是高频出现的问题之一,咱们每每会监听一个元素滚动位置的变化来决定展示那些内容,这个应用场景在H5游戏开发中应用十分普遍,接下来咱们来看看实现代码:

import { useState, useEffect } from 'react'

const useScroll = (scrollRef) => {
  const [pos, setPos] = useState([0,0])

  useEffect(() => {
    function handleScroll(e){
      setPos([scrollRef.current.scrollLeft, scrollRef.current.scrollTop])
    }
    scrollRef.current.addEventListener('scroll', handleScroll, false)
    return () => {
      scrollRef.current.removeEventListener('scroll', handleScroll, false)
    }
  }, [])
  
  return pos
}

export default useScroll
复制代码

由以上代码可知,咱们在钩子函数里须要传入一个元素的引用,这个咱们能够在函数组件中采用ref和useRef来获取到,钩子返回了滚动的x,y值,即滚动的左位移和顶部位移,具体使用以下:

import React, { useRef } from 'react'

 import { useScroll } from 'hooks'
const Home = (props) => {
  const scrollRef = useRef(null)
  const [x, y] = useScroll(scrollRef)

  return <div> <div ref={scrollRef}> <div className="innerBox"></div> </div> <div>{ x }, { y }</div> </div>
}
复制代码

经过使用useScroll,钩子将会帮咱们自动监听容器滚动条的变化从而实时获取滚动的位置。具体效果以下:

9. 实现自定义的useMouse和实现自定义的createBreakpoint

自定义的useMouse和createBreakpoint的实现方法和useScroll相似,都是监听窗口或者dom的事件来自动更新咱们须要的值,这里我就不一一实现了,若是不懂的能够和我交流。经过这些自定义钩子能大大提升咱们代码的开发效率,并将重复代码进行有效复用,因此你们在工做中能够多尝试。

当咱们写了不少自定钩子时,一个好的开发经验就是统一管理和分发这些钩子,笔者建议能够在项目中单独建一个hooks的目录专门存放这些可复用的钩子,方便管理和维护。以下:

最后

若是想获取本次项目完整的源码, 或者想学习更多H5游戏, webpacknodegulpcss3javascriptnodeJScanvas数据可视化等前端知识和实战,欢迎在公号《趣谈前端》加入咱们的技术群一块儿学习讨论,共同探索前端的边界。

更多推荐

相关文章
相关标签/搜索