对于使用 react 的同学来说,hook 一定不陌生,但是如何封装 hook 以及在业务中怎么使用封装的 hook,很多同学并没有一个很好的实践,这篇文章就通过10个常用的 hook 让大家学会封装 hook,能够在自己的业务中使用,提高复用率,减少开发成本
前沿
hook 到底有什么作用呢?它可以让你对一些功能组件重复逻辑进行封装,分离组件并将其功能细化,让组件逻辑变的简单明了,逻辑共享变的更容易,减少了代码的重复性,维护和更新变的更简单易懂
hook 的本质就是让我们的组件不再使用 class 组件,所以,如果你的项目还在用 react 的 class 组件的方式,是不能使用 hook 的
react 也内置了一些对应的 hook,比如我们常用的 useState、useEffect 等等,这里就不多说了
让我们开始封装自己的一个 hook 库吧
useToggle
代码语言:javascript复制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 代码,这里返回的是一个数组
在使用方面就变的很简单了
代码语言:javascript复制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 呢?
代码语言:javascript复制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 进行删除操作
使用也比较简单
代码语言:javascript复制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
代码语言:javascript复制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 参数
代码语言:javascript复制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
代码语言:javascript复制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)
}
使用方式
代码语言:javascript复制const { loading, error, value } = useFetch(
url,
{
method: 'post'
}
)
useEffectOnce
这个实现起来比较简单
代码语言:javascript复制import { useEffect } from "react"
export default function useEffectOnce(cb) {
useEffect(cb, [])
}
使用同样
代码语言:javascript复制useEffectOnce(() => alert("Hi"))
useRenderCount
查看某个页面渲染了多少次
代码语言:javascript复制import { useEffect, useRef } from "react"
export default function useRenderCount() {
const count = useRef(1)
useEffect(() => count.current )
return count.current
}
使用
代码语言:javascript复制const renderCount = useRenderCount()
useTimeout
代码语言:javascript复制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 保存定时器和回调函数
使用方式
代码语言:javascript复制const { clear, reset } = useTimeout(() => setCount(0), 1000)
通过按钮点击或者函数调用来对定时器进行操作
useDebounce
同样的,对 useTimeout 进一步进行封装,可以实现 debounce 的操作,主要目的是为了解决某个方法在指定时间内重复调用,用 hook 的方式可以很方便的解决这种问题
代码语言:javascript复制export default function useDebounce(callback, delay, dependencies) {
const { reset, clear } = useTimeout(callback, delay)
useEffect(reset, [...dependencies, reset])
useEffect(clear, [])
}
其中通过 dependencies 的变化可以控制 reset,控制执行的频率
代码语言:javascript复制const [count, setCount] = useState(10)
useDebounce(() => alert(count), 1000, [count])
count 在 1s 之内变化频繁的话,是不会触发 alert 的,当然也可以通过一个是否立即执行的参数进行一些相应的控制,这里就不提了,有兴趣的同学可以自主完善一下
总结
总体来看,封装 hook 还是挺简单的,你可以理解为就是把一些常用的原生的 hook 或者一些函数的再次封装,结合 state 或者 effect 将一些通用的逻辑提取,让页面变化更简单,更专注于页面本身自己的逻辑
同时也需要注意 hook 的一些使用规则,本质它就是一个 js 函数
- 只能在函数最外层调用 hook,不要在循环、条件判断或者子函数中调用
- 只能在 React 的函数组件中调用 hook 不要在其他 JavaScript 函数中调用,当然你也可以在自定义函数中调用自定义 hook,比如我们实现的 useFetch 就是基于 useAsync