🧑‍💻 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主要包括useStateuseEffectuseContext 以及useRef ,其中,useStateuseRef主要用于提供响应式变量,useEffect主要用于侦听变量做出响应式的变化,useContext用于提供统一的上下文,综上我们可以得出,Hook主要用于在React中管理状态和副作用。

什么是自定义Hook?

从结果上来看,只要我们涉及状态和副作用管理,并且需要调用以上的这些原生Hook,这样我们封装出来的一个函数就是一个自定义Hook

我们为什么需要自定义Hook?

就像我们经常需要将一些常用的功能封装成工具类一样,一些设计状态和副作用管理的逻辑,我们并不希望将其与UI部分混在一起,而是抽象出来,以便将逻辑和UI分离,并且方便我们复用。

如何实现一个自定义Hook?

需求

  1. 在信息流页面实现一个搜索框,这个页面里有一个输入框,下面是一个用于展示信息流的组件;
  1. 你需要实现基于用户名、内容以及tag的搜索;
  1. 信息流中有可以点击的tag,tag点击后的行为需要反应到输入框中,进行搜索;
  1. 搜索行为对输入具有响应性,用户在完成输入后不需要手动敲击回车,而是会自动进行搜索。

分析需求

实现一个自定义Hook,我们首先需要考虑,当前的页面需要什么数据和功能?这决定了我们在自定义Hook中需要对外暴露哪些state和setState,以及handler,搞清楚需要什么,写起代码来自然得心应手。
分析页面需要的数据和功能,我们可以得到以下结论:
  1. 我们不仅需要通过输入框拿到输入内容,同时还需要将tag的内容反应到输入框中,所以需要同时提供searchTexthandleSearchChange,即:
    1. <input
        type="text"
        placeholder="Search for a tag or username"
        value={searchText}
        onChange={handleSearchChange}
        required
        className="search_input peer"
      />
  1. 在输入框外面的form里,我们需要处理表单的提交行为,即敲击回车后,需要提交表单,即提供一个handleSubmit
    1. <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>
  1. 在下面的信息流组件中,我们显然需要拿到数据,即提供一个posts,且tag既然可以点击,我们也需要一个handleTagClick
    1. <PromptCardList data={posts} handleTagClick={handleTagClick} />
  1. 我们不光需要在hook内发送拿到feed数据的请求(主要是搜索),我们还需要考虑到,在页面挂载的时候,我们需要手动进行一次posts请求,拿到数据进行展示,所以需要提供一个fetchPosts
    1. useEffect(() => {
          fetchPosts()
        }, [])
  1. 我们需要对输入内容具有响应性,这实际上是一个“防抖”功能的实现

实现自定义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

© Musher 2019 - 2024

powered by nobelium