🧑💻 Promptopia 开发笔记(3)——实现一个自定义Hook
date
Jul 12, 2024
slug
Promptopia-dev-note-implement-custom-hooks
status
Published
tags
Next.js
React
Website
Hooks
JavaScript
summary
从实践经验出发,聊聊如何实现一个自定义hooks
type
Post
前言
在聊如何实现一个自定义Hook之前,我们先聊聊什么是Hook,什么是自定义Hook,以及我们什么时候需要自定义Hook?
什么是 Hook?
在React中,常见的Hook主要包括
useState
、useEffect
、useContext
以及useRef
,其中,useState
和useRef
主要用于提供响应式变量,useEffect
主要用于侦听变量做出响应式的变化,useContext
用于提供统一的上下文,综上我们可以得出,Hook主要用于在React中管理状态和副作用。什么是自定义Hook?
从结果上来看,只要我们涉及状态和副作用管理,并且需要调用以上的这些原生Hook,这样我们封装出来的一个函数就是一个自定义Hook
我们为什么需要自定义Hook?
就像我们经常需要将一些常用的功能封装成工具类一样,一些设计状态和副作用管理的逻辑,我们并不希望将其与UI部分混在一起,而是抽象出来,以便将逻辑和UI分离,并且方便我们复用。
如何实现一个自定义Hook?
需求
- 在信息流页面实现一个搜索框,这个页面里有一个输入框,下面是一个用于展示信息流的组件;
- 你需要实现基于用户名、内容以及tag的搜索;
- 信息流中有可以点击的tag,tag点击后的行为需要反应到输入框中,进行搜索;
- 搜索行为对输入具有响应性,用户在完成输入后不需要手动敲击回车,而是会自动进行搜索。
分析需求
实现一个自定义Hook,我们首先需要考虑,当前的页面需要什么数据和功能?这决定了我们在自定义Hook中需要对外暴露哪些state和setState,以及handler,搞清楚需要什么,写起代码来自然得心应手。
分析页面需要的数据和功能,我们可以得到以下结论:
- 我们不仅需要通过输入框拿到输入内容,同时还需要将tag的内容反应到输入框中,所以需要同时提供
searchText
和handleSearchChange
,即:
<input
type="text"
placeholder="Search for a tag or username"
value={searchText}
onChange={handleSearchChange}
required
className="search_input peer"
/>
- 在输入框外面的form里,我们需要处理表单的提交行为,即敲击回车后,需要提交表单,即提供一个
handleSubmit
<form className="relative w-full flex-center" onSubmit={handleSubmit}>
<input
type="text"
placeholder="Search for a tag or username"
value={searchText}
onChange={handleSearchChange}
required
className="search_input peer"
/>
</form>
- 在下面的信息流组件中,我们显然需要拿到数据,即提供一个
posts
,且tag既然可以点击,我们也需要一个handleTagClick
<PromptCardList data={posts} handleTagClick={handleTagClick} />
- 我们不光需要在hook内发送拿到feed数据的请求(主要是搜索),我们还需要考虑到,在页面挂载的时候,我们需要手动进行一次posts请求,拿到数据进行展示,所以需要提供一个
fetchPosts
。
useEffect(() => {
fetchPosts()
}, [])
- 我们需要对输入内容具有响应性,这实际上是一个“防抖”功能的实现
实现自定义Hook
当分析清楚了需求是什么之后,代码实现起来就没什么难度了。
写这段防抖和tag点击逻辑的时候有点小插曲,就是防抖需要对输入框生效,但不应该对tag的点击生效,毕竟没人希望点击tag后需要等1s才能有相应,这就涉及一些清楚定时器的操作,这其中遇到的一些异步逻辑的debug过程,已经在上一篇博文中介绍了,有兴趣可以查看这篇文章:‣
import { useState, useEffect, useRef } from 'react'
const useSearchPosts = (initialSearchText = '') => {
const [searchText, setSearchText] = useState(initialSearchText)
const [posts, setPosts] = useState([])
const timeoutId = useRef(null)
// 获取所有 prompt
const fetchPosts = async () => {
const response = await fetch('/api/prompt')
const data = await response.json()
setPosts(data)
}
// 根据搜索关键词获取 prompt
const fetchSearchPosts = async search => {
const response = await fetch('/api/prompt/search?searchText=' + search)
const data = await response.json()
setPosts(data)
}
useEffect(() => {
fetchPosts()
}, [])
// 防抖
useEffect(() => {
if (searchText.trim()) {
const newTimeout = setTimeout(() => {
fetchSearchPosts(searchText)
}, 1000)
timeoutId.current = newTimeout
console.log('set!', newTimeout)
return () => clearTimeout(newTimeout)
} else {
fetchPosts()
return () => clearTimeout(timeoutId.current)
}
}, [searchText])
const handleSearchChange = e => {
setSearchText(e.target.value)
}
const handleSubmit = e => {
e.preventDefault()
console.log('clear by submit', timeoutId.current)
clearTimeout(timeoutId.current)
fetchSearchPosts(searchText)
}
const handleTagClick = tag => {
setSearchText(tag)
// 需要等待searchText更新完毕,再清除副作用带来的定时器
setTimeout(() => {
console.log('clear by tag', timeoutId.current)
clearTimeout(timeoutId.current)
}, 0)
fetchSearchPosts(tag) // 立即执行搜索
}
return {
searchText,
posts,
fetchPosts,
handleSearchChange,
handleSubmit,
handleTagClick
}
}
export default useSearchPosts