React中useEffect首次加载后不再调用Firebase读取API的问题
问题根源
你当前的问题是:组件首次渲染能正常从Firebase获取数据,但执行更新、删除操作后不会自动重新渲染,必须刷新页面才能看到变化;如果把todos加入useEffect依赖,虽然能实现自动更新,但会触发大量不必要的API调用。
这是因为你用getDocs做单次数据拉取,Firebase数据变更后不会主动通知前端;而把todos加进依赖会导致每次setTodos都触发重新拉取,形成无限循环。
最优解决方案:使用Firebase实时监听(
onSnapshot) Firestore提供onSnapshot方法,能建立和数据库的实时连接,当集合内数据新增、修改、删除时,自动触发回调更新本地状态,既不用手动重复拉取数据,也能保证组件实时渲染最新内容。
核心代码修改
替换原来的useEffect部分为以下代码:
// 实时监听todo集合的变化 useEffect(() => { // 订阅集合变化,返回取消订阅的函数 const unsubscribe = onSnapshot(collection(db, "react-todo"), (snapshot) => { // 把快照数据转换成数组 const todoArray = snapshot.docs.map(doc => ({ ...doc.data(), id: doc.id })); setTodos(todoArray); }); // 组件卸载时取消订阅,防止内存泄漏 return () => unsubscribe(); }, []);
其他细节优化
- 替换列表key为todo.id:用index做key会导致列表项顺序变化时渲染异常,改用Firebase生成的唯一id更可靠
{todos.map((todo) => ( <Todo key={todo.id} // 替换原来的index todo={todo} toggleComplete={toggleComplete} deleteTodo={deleteTodo} /> ))}
- 修正checkbox的checked属性:
checked应该是布尔值,不用转成字符串
<input onChange={() => toggleComplete(todo)} type="checkbox" checked={todo.complete} // 直接用布尔值 />
- 简化事件处理逻辑:
e.preventDefault()不需要传参数,用onChange替代onChangeCapture更符合常规用法
// createTodo函数修正 const createTodo = async (e) => { e.preventDefault(); if (input === "") { alert("请输入待办内容"); return; } await addDoc(collection(db, "react-todo"), { text: input, complete: false, }); setInput(""); }; // input的onChange修正 <input type="text" placeholder="Add Todo" className={style.input} value={input} onChange={(e) => setInput(e.target.value)} />
完整修改后的代码
App.jsx
function App() { const [todos, setTodos] = useState([]); const [input, setInput] = useState(""); // Create todo const createTodo = async (e) => { e.preventDefault(); if (input === "") { alert("请输入待办内容"); return; } await addDoc(collection(db, "react-todo"), { text: input, complete: false, }); setInput(""); }; // 实时监听todo集合变化 useEffect(() => { const unsubscribe = onSnapshot(collection(db, "react-todo"), (snapshot) => { const todoArray = snapshot.docs.map(doc => ({ ...doc.data(), id: doc.id })); setTodos(todoArray); }); return () => unsubscribe(); }, []); // update todo in firebase const toggleComplete = async (todo) => { await updateDoc(doc(db, "react-todo", todo.id), { complete: !todo.complete, }); }; // delete todo const deleteTodo = async (todo) => { await deleteDoc(doc(db, "react-todo", todo.id)); }; return ( <div className={style.bg}> <div className={style.container}> <h3 className={style.heading}>Todo App</h3> <form onSubmit={createTodo} className={style.form}> <input type="text" placeholder="Add Todo" className={style.input} value={input} onChange={(e) => setInput(e.target.value)} /> <button className={style.button}> <AiOutlinePlus size={30} /> </button> </form> <ul> {todos.map((todo) => ( <Todo key={todo.id} todo={todo} toggleComplete={toggleComplete} deleteTodo={deleteTodo} /> ))} </ul> <p className={style.count}>{`你有 ${todos.length} 条待办事项`}</p> </div> </div> ); }
Todo.jsx
function Todo({ todo, toggleComplete, deleteTodo }) { return ( <li className={todo.complete ? style.liComplete : style.li}> <div className={style.row}> <input onChange={() => toggleComplete(todo)} type="checkbox" checked={todo.complete} /> <p onClick={() => toggleComplete(todo)} className={todo.complete ? style.textComplete : style.text} > {todo.text} </p> </div> <button onClick={() => deleteTodo(todo)}>{<FaRegTrashAlt />}</button> </li> ); }
原理说明
onSnapshot会在组件挂载时建立与Firestore的实时连接,当集合数据发生任何变化,立即触发回调更新本地todos状态,从而触发组件重新渲染。返回的unsubscribe函数用于在组件卸载时关闭监听,避免内存泄漏。这种方式既不需要依赖todos,也不会产生多余API调用,完美实现数据实时同步。
内容的提问来源于stack exchange,提问作者Abhishek Ajmera
相关产品推荐
相关产品推荐

