对于使用 react 的同学来说,hook 一定不陌生,但是如何封装 hook 以及在业务中怎么使用封装的 hook,很多同学并没有一个很好的实践,这篇文章就通过10个常用的 hook 让大家学会封装 hook,能够在自己的业务中使用,提高复用率,减少开发成本

前沿

hook 到底有什么作用呢?它可以让你对一些功能组件重复逻辑进行封装,分离组件并将其功能细化,让组件逻辑变的简单明了,逻辑共享变的更容易,减少了代码的重复性,维护和更新变的更简单易懂

hook 的本质就是让我们的组件不再使用 class 组件,所以,如果你的项目还在用 react 的 class 组件的方式,是不能使用 hook 的

react 也内置了一些对应的 hook,比如我们常用的 useState、useEffect 等等,这里就不多说了

让我们开始封装自己的一个 hook 库吧

useToggle

import { useState } from "react"

export default function useToggle(defaultValue) {
  const [value, setValue] = useState(defaultValue)

  function toggleValue(value) {
    setValue(currentValue =>
      typeof value === "boolean" ? value : !currentValue
    )
  }

  return [value, toggleValue]
}

通过代码能够看出这个 hook 的作用,本质就是进行状态的切换,你可以将其理解成一个 react 组件,也可以只是将其理解成一个函数,这个函数接受一个初始值,用 useState 进行状态的存储,通过函数 toggleValue 进行状态的切换,然后函数返回两个内容,一个是 当前的 value,一个是 toggleValue 函数,进行状态的切换,只不过组件返回的是一段 jsx 代码,这里返回的是一个数组

在使用方面就变的很简单了

export default function ToggleComponent() {
  const [value, toggleValue] = useToggle(false)

  return (
    <div>
      <div>{value.toString()}</div>
      <button onClick={toggleValue}>Toggle</button>
      <button onClick={() => toggleValue(true)}>Make True</button>
      <button onClick={() => toggleValue(false)}>Make False</button>
    </div>
  )
}

useStorage

前端的数据存储离不开 localStorage 和 sessionStorage,那如何根据这个内容写一个自定义 hook 呢?

import { useCallback, useState, useEffect } from "react"

export function useLocalStorage(key, defaultValue) {
  return useStorage(key, defaultValue, window.localStorage)
}

export function useSessionStorage(key, defaultValue) {
  return useStorage(key, defaultValue, window.sessionStorage)
}

function useStorage(key, defaultValue, storageObject) {
  const [value, setValue] = useState(() => {
    const jsonValue = storageObject.getItem(key)
    if (jsonValue != null) return JSON.parse(jsonValue)

    if (typeof defaultValue === "function") {
      return defaultValue()
    } else {
      return defaultValue
    }
  })

  useEffect(() => {
    if (value === undefined) return storageObject.removeItem(key)
    storageObject.setItem(key, JSON.stringify(value))
  }, [key, value, storageObject])

  const remove = useCallback(() => {
    setValue(undefined)
  }, [])

  return [value, setValue, remove]
}

这两个 hook 功能差不多,接收两个参数,key 和 defaultValue,当然你还可以扩展过期时间相关内容

useEffect 监听 key 或者 value 是否变化做出一系列操作,通过 JSON.stringify 格式化成字符串,并通过 value 是否是 undefined 进行删除操作

使用也比较简单

export default function StorageComponent() {
  const [age, setAge, removeAge] = useLocalStorage("age", 26)

  return (
    <div>
      <div>
        {name} - {age}
      </div>
      <button onClick={() => setAge(40)}>Set Age</button>
      <button onClick={removeAge}>Remove Age</button>
    </div>
  )
}

useAsync

import { useCallback, useEffect, useState } from "react"

export default function useAsync(callback, dependencies = []) {
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState()
  const [value, setValue] = useState()

  const callbackMemoized = useCallback(() => {
    setLoading(true)
    setError(undefined)
    setValue(undefined)
    callback()
      .then(setValue)
      .catch(setError)
      .finally(() => setLoading(false))
  }, dependencies)

  useEffect(() => {
    callbackMemoized()
  }, [callbackMemoized])

  return { loading, error, value }
}

主要内容还是针对 useState 和 useEffect 的封装,相互结合组成了 useAsync 的封装,callback 传入的是一个 Promise 函数,将 loading、error、value 统一处理,并针对 useEffect 的执行时机添加了 dependencies 参数

const { loading, error, value } = useAsync(() => {
  return new Promise((resolve, reject) => {
    const success = false
    setTimeout(() => {
      success ? resolve("Hi") : reject("Error")
    }, 1000)
  })
})

useFetch

根据我们封装的 useAsync,通过进一步处理,我们还能够得到更好用的 useFetch,之后在项目中再使用就不需要用自己封装的 fetch.js 了,毕竟其中没有 loading 或者 value 绑定在 state 的操作,可以用更好用的 useFetch

const DEFAULT_OPTIONS = {
  headers: { "Content-Type": "application/json" },
}

export default function useFetch(url, options = {}, dependencies = []) {
  return useAsync(() => {
    return fetch(url, { ...DEFAULT_OPTIONS, ...options }).then(res => {
      if (res.status === 200) return res.data
      return Promise.reject(res)
    })
  }, dependencies)
}

使用方式

const { loading, error, value } = useFetch(
    url,
    {
      method: 'post'
    }
  )

useEffectOnce

这个实现起来比较简单

import { useEffect } from "react"

export default function useEffectOnce(cb) {
  useEffect(cb, [])
}

使用同样

useEffectOnce(() => alert("Hi"))

useRenderCount

查看某个页面渲染了多少次

import { useEffect, useRef } from "react"

export default function useRenderCount() {
  const count = useRef(1)
  useEffect(() => count.current++)
  return count.current
}

使用

const renderCount = useRenderCount()

useTimeout

import { useCallback, useEffect, useRef } from "react"

export default function useTimeout(callback, delay) {
  const callbackRef = useRef(callback)
  const timeoutRef = useRef()

  useEffect(() => {
    callbackRef.current = callback
  }, [callback])

  const set = useCallback(() => {
    timeoutRef.current = setTimeout(() => callbackRef.current(), delay)
  }, [delay])

  const clear = useCallback(() => {
    timeoutRef.current && clearTimeout(timeoutRef.current)
  }, [])

  useEffect(() => {
    set()
    return clear
  }, [delay, set, clear])

  const reset = useCallback(() => {
    clear()
    set()
  }, [clear, set])

  return { reset, clear }
}

这个 hook 本质就是延迟多长时间执行 callback 函数,对外暴露了两个方法,分别是重置 reset 和 clear 清除定时器,可以更方便进行定时器操作,使用 ref 保存定时器和回调函数

使用方式

const { clear, reset } = useTimeout(() => setCount(0), 1000)

通过按钮点击或者函数调用来对定时器进行操作

useDebounce

同样的,对 useTimeout 进一步进行封装,可以实现 debounce 的操作,主要目的是为了解决某个方法在指定时间内重复调用,用 hook 的方式可以很方便的解决这种问题

export default function useDebounce(callback, delay, dependencies) {
  const { reset, clear } = useTimeout(callback, delay)
  useEffect(reset, [...dependencies, reset])
  useEffect(clear, [])
}

其中通过 dependencies 的变化可以控制 reset,控制执行的频率

const [count, setCount] = useState(10)
useDebounce(() => alert(count), 1000, [count])

count 在 1s 之内变化频繁的话,是不会触发 alert 的,当然也可以通过一个是否立即执行的参数进行一些相应的控制,这里就不提了,有兴趣的同学可以自主完善一下

总结

总体来看,封装 hook 还是挺简单的,你可以理解为就是把一些常用的原生的 hook 或者一些函数的再次封装,结合 state 或者 effect 将一些通用的逻辑提取,让页面变化更简单,更专注于页面本身自己的逻辑

同时也需要注意 hook 的一些使用规则,本质它就是一个 js 函数

  1. 只能在函数最外层调用 hook,不要在循环、条件判断或者子函数中调用
  2. 只能在 React 的函数组件中调用 hook 不要在其他 JavaScript 函数中调用,当然你也可以在自定义函数中调用自定义 hook,比如我们实现的 useFetch 就是基于 useAsync

FE情报局
30 声望5 粉丝