全栈开发

nextjs表单提交防抖

use-debounce.ts

import { useEffect, useState } from "react"

export const useDebounce = (value: string, delay: number) => {
    const [debouncedValue, setDebouncedValue] = useState(value);

    useEffect(() => {
        const timer = setTimeout(() => {
            setDebouncedValue(value);
        }, delay);

        return () => clearTimeout(timer);
    }, [delay, value]);

    return debouncedValue;
};

应用:

"use client";
import { useDocumentsStore } from '@/app/(protected)/dashboard/documents/_libs/use-document-store'
import { documentFiltersDefaultValues, documentFiltersSchema, DocumentFiltersSchema } from '@/app/(protected)/dashboard/documents/_types/document-filter-schema'
import { zodResolver } from '@hookform/resolvers/zod'
import { useEffect } from 'react'
import { FormProvider, useForm, useWatch } from 'react-hook-form'
import { useDebounce } from '@/lib/use-debounce'
import { ControlledInput } from '@/components/controlled/controlled-input'

function DocumentFiltersDrawer() {
    const form = useForm<DocumentFiltersSchema>({
        defaultValues: documentFiltersDefaultValues,
        resolver: zodResolver(documentFiltersSchema)
    })

    const {
        updateDocumentFiltersSearchTerm,
        updateDocumentFiltersPage
    } = useDocumentsStore();


    const searchTerm = useWatch({ control: form.control, name: "searchTerm" });
    const debouncedSearchTerm = useDebounce(searchTerm, 400);

    useEffect(() => {
        updateDocumentFiltersSearchTerm(debouncedSearchTerm);
        updateDocumentFiltersPage(1);
    }, [debouncedSearchTerm, updateDocumentFiltersSearchTerm, updateDocumentFiltersPage]);


    return (
        <FormProvider {...form}>
            <div className="flex gap-2 mb-3">
                <ControlledInput<DocumentFiltersSchema>
                    name="searchTerm"
                    placeholder="Quick Search"
                />
            </div>
        </FormProvider>
    )
}

export default DocumentFiltersDrawer

以上效果,在搜索框发生变动的时候 间隔400ms触发检索,而不是每次变动都触发。

核心代码分析

const searchTerm = useWatch({ control: form.control, name: "searchTerm" });
const debouncedSearchTerm = useDebounce(searchTerm, 400);

useEffect(() => {
    updateDocumentFiltersSearchTerm(debouncedSearchTerm);
    updateDocumentFiltersPage(1);
}, [debouncedSearchTerm, updateDocumentFiltersSearchTerm, updateDocumentFiltersPage]);

useWatch:监听表单 searchTerm 字段的实时变化

useDebounce:对 searchTerm 做防抖处理

  • 每次 searchTerm 变化时,设置一个 400ms 的延迟
  • 在延迟期间如果 searchTerm 再次变化,上一次的定时器会被清掉
  • 只有 用户停止输入超过 400msdebouncedSearchTerm 才会更新

useEffect:监听 debouncedSearchTerm

  • 当防抖后的值更新时,执行搜索相关动作(更新搜索条件、重置页码)

防抖逻辑总结

  1. 用户每输入一个字符 → searchTerm 变化 → useDebounce 启动计时器
  2. 在 400ms 内继续输入 → 上一个计时器清除,重新计时
  3. 用户停止输入 ≥ 400ms → debouncedSearchTerm 更新 → useEffect 执行
  4. 后续没有输入 → 不再触发,直到下一次输入

时间轴示意图

假设输入 abc,每个字符输入间隔 < 400ms,最后停下来:

时间(ms)  →  0     100     200     300     800
输入       a       b       c      (停)  

useDebounce 定时器:
          ─────┐
                └───> 被清除
                 ─────┐
                       └───> 被清除
                        ─────┐
                              └──> 400ms 后触发 debouncedSearchTerm = "abc"

useEffect 执行:
                              ┌────────────> updateDocumentFiltersSearchTerm("abc")
                              └────────────> updateDocumentFiltersPage(1)

关键点

  • 防抖期间不触发搜索 → 避免频繁请求
  • 停顿超过 400ms 才触发一次搜索
  • 只触发一次,直到下一次输入