You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

添加UID与搜索输入查询功能后,React Firebase待办应用运行缓慢

待办应用添加搜索功能后卡顿的修复方案

核心问题分析

  • 重复创建Firestore监听:你将搜索查询逻辑直接写在组件顶层,组件每次渲染(包括搜索输入变化、任务增删改、用户状态变更时)都会执行这段代码,反复创建新的onSnapshot监听且未清理旧监听,导致大量重复数据库订阅,占用资源引发卡顿。
  • useEffect依赖缺失:初始数据加载的useEffect依赖为空数组,用户状态(user)变化时(比如登录完成后)不会重新触发数据获取,可能导致数据加载异常。
  • 无搜索防抖:输入框每输入一个字符就触发一次数据库查询,频繁请求加重客户端与Firestore负担。
  • 空值风险:user?.uid为null时会触发无效查询,影响应用稳定性。

修复步骤

  1. 将查询逻辑移入useEffect并清理监听:所有onSnapshot监听都应在useEffect中创建,返回清理函数避免内存泄漏。
  2. 补充正确依赖项:让useEffect依赖user和防抖后的搜索值,确保值变化时自动更新查询。
  3. 添加搜索防抖:延迟搜索请求触发时机,减少不必要的数据库查询。
  4. 处理未登录状态:用户未登录时不执行查询,避免无效请求。
  5. 创建Firestore复合索引:搜索查询用到uid和name两个字段的条件,需在Firestore控制台创建对应复合索引,否则查询会失败。

修复后的完整代码

import React, { useEffect, useState, useCallback } from "react";
import { collection, doc, addDoc, updateDoc, deleteDoc, Timestamp, query, where, onSnapshot } from "firebase/firestore";
import { BsFillPencilFill, BsFillTrashFill } from "react-icons/bs";
import { IoMdAddCircle } from "react-icons/io";
import { useAuthState } from "react-firebase-hooks/auth";
import { auth, db, logout } from "./utils/firebase";

// 防抖函数
const debounce = (func, delay) => {
  let timeoutId;
  return (...args) => {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => func.apply(this, args), delay);
  };
};

export default function Todo() {
  const [name, setName] = useState("");
  const [tasks, setTasks] = useState([]);
  const [user, loading, error] = useAuthState(auth);
  const [searchInput, setSearchInput] = useState("");
  // 防抖后的搜索输入值
  const [debouncedSearch, setDebouncedSearch] = useState("");

  // 处理防抖搜索输入
  useEffect(() => {
    const debouncedSetSearch = debounce((value) => {
      setDebouncedSearch(value);
    }, 300); // 300ms延迟,可按需调整

    debouncedSetSearch(searchInput);
    return () => clearTimeout(debouncedSetSearch);
  }, [searchInput]);

  // 统一处理数据查询与监听
  useEffect(() => {
    if (!user) {
      setTasks([]);
      return;
    }

    let q;
    if (debouncedSearch.length > 0) {
      // 带搜索条件的查询
      q = query(
        collection(db, 'tasks'),
        where("uid", "==", user.uid),
        where("name", ">=", debouncedSearch),
        where("name", "<=", debouncedSearch + '\uf8ff')
      );
    } else {
      // 只按UID查询
      q = query(collection(db, 'tasks'), where("uid", "==", user.uid));
    }

    // 创建监听并返回清理函数
    const unsubscribe = onSnapshot(q, (querySnapshot) => {
      setTasks(querySnapshot.docs.map(doc => ({
        id: doc.id,
        data: doc.data()
      })));
    }, (err) => {
      console.error("监听错误:", err);
    });

    // 组件卸载或依赖变化时清理监听
    return unsubscribe;
  }, [user, debouncedSearch]);

  const handleChange = (e) => {
    e.preventDefault();
    setName(e.target.value);
  };

  const handleSearchChange = (e) => {
    e.preventDefault();
    setSearchInput(e.target.value);
  };

  const handleAdd = async (e) => {
    e.preventDefault();
    if (!name.trim()) {
      alert("请输入任务内容");
      clearInput();
      return;
    }
    if (!user) return;

    try {
      await addDoc(collection(db, 'tasks'), {
        name: name.trim(),
        completed: false,
        created: Timestamp.now(),
        uid: user.uid,
      });
      clearInput();
    } catch (err) {
      alert(err.message);
    }
  };

  const handleClick = (e) => {
    if (e.detail === 2) {
      e.currentTarget.classList.toggle('double-clicked');
    }
  };

  const handleUpdate = async (taskName, id) => {
    const newName = prompt("请输入新任务名称", taskName);
    if (newName === null) return;
    if (!newName.trim()) {
      alert("任务名称不能为空");
      return;
    }

    try {
      await updateDoc(doc(db, 'tasks', id), {
        name: newName.trim(),
      });
    } catch (err) {
      alert(err.message);
    }
  };

  const handleDelete = async (id) => {
    try {
      await deleteDoc(doc(db, 'tasks', id));
    } catch (err) {
      alert(err.message);
    }
  };

  const clearInput = useCallback(() => {
    const input = document.getElementById("input");
    if (input) {
      input.value = '';
      input.focus();
    }
    setName("");
  }, []);

  if (loading) {
    return <div>加载中...</div>;
  }

  if (error) {
    return <div>登录出错: {error.message}</div>;
  }

  if (!user) {
    return <div>请先登录</div>;
  }

  return (
    <div>
      <div>
        <div>
          <h1>待办清单</h1>
          <p>双击标记任务完成</p>
        </div>
        <input
          id="input"
          type="text"
          value={name}
          onChange={handleChange}
          autoFocus
          placeholder="输入新任务"
        />
        <button
          className="add-button"
          type="submit"
          onClick={handleAdd}
        >
          <IoMdAddCircle />
        </button>
      </div>

      <ol>
        {tasks.map((task) => (
          <li
            className="task-list-items"
            id={task.id}
            key={task.id}
            onClick={handleClick}
          >
            {task.data.name}
            <button
              className="edit-button"
              onClick={() => handleUpdate(task.data.name, task.id)}
            >
              <BsFillPencilFill />
            </button>
            <button
              className="delete-button"
              onClick={() => handleDelete(task.id)}
            >
              <BsFillTrashFill />
            </button>
          </li>
        ))}
      </ol>

      <div>
        <h5>按名称搜索任务</h5>
        <input
          id="search-bar"
          type="text"
          value={searchInput}
          onChange={handleSearchChange}
          placeholder="输入关键词搜索"
        />
      </div>
    </div>
  );
};

额外注意事项

  • 创建复合索引:首次运行搜索查询时,Firestore控制台会抛出错误,错误信息包含索引创建的直接链接,点击即可自动生成所需索引。
  • 优化渲染性能:可给任务列表项包裹React.memo,避免不必要的重渲染。
  • 错误处理:代码中添加了基础错误提示,可根据需求替换为更友好的UI组件。

内容的提问来源于stack exchange,提问作者Julian

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.06 08:20:48