React中无法将拖拽排序数据存入localStorage并在刷新后复用
问题分析
你的代码存在几个核心问题导致拖拽排序无法持久化到localStorage:
- 初始数据加载冲突:两个
useEffect在组件挂载时同时执行,Firebase数据和localStorage数据会互相覆盖,最终状态取决于哪个请求完成得晚。 - 数据结构不匹配:Firebase返回键值对对象,但拖拽逻辑将其转为数组后存回状态,后续渲染用
Object.keys处理数组,导致数据结构混乱。 - 丢失Firebase键:拖拽时直接操作数据值,丢失原始Firebase键,影响删除等依赖键的功能。
- localStorage被Firebase更新覆盖:每次Firebase数据变化都会触发localStorage保存,覆盖手动排序结果。
解决方案
将原始数据和排序顺序拆分为两个独立状态:originalData存储Firebase原始键值对,sortedKeys存储排序后的键数组(持久化到localStorage)。既保留Firebase数据完整性,又能独立维护排序状态。
修正后的代码
import React, { useState, useEffect, useRef } from "react"; import "./BootTable.css"; import "./Appa.css"; import "../node_modules/bootstrap/dist/css/bootstrap.min.css"; import firebaseDB from "./firebased"; import { ToastContainer, toast } from "react-toastify"; import "react-toastify/dist/ReactToastify.css"; function App() { const dragItem = useRef(); const dragOverItem = useRef(); // 存储Firebase原始数据(键值对对象) const [originalData, setOriginalData] = useState({}); // 存储排序后的键数组,控制渲染顺序 const [sortedKeys, setSortedKeys] = useState([]); // 从Firebase加载原始数据 useEffect(() => { const unsubscribe = firebaseDB.child("register").on("value", (snapshot) => { const data = snapshot.val() || {}; setOriginalData(data); // 初始化排序:优先用localStorage记录,否则用Firebase默认顺序 if (sortedKeys.length === 0) { const savedKeys = window.localStorage.getItem("SORTED_KEYS"); if (savedKeys) { setSortedKeys(JSON.parse(savedKeys)); } else { setSortedKeys(Object.keys(data)); } } else { // Firebase新增数据时,将新键追加到排序数组末尾 const currentKeys = new Set(sortedKeys); const newKeys = Object.keys(data).filter(key => !currentKeys.has(key)); if (newKeys.length > 0) { setSortedKeys(prev => [...prev, ...newKeys]); } } }); // 清理Firebase订阅 return () => unsubscribe(); }, [sortedKeys.length]); // 初始加载localStorage中的排序顺序 useEffect(() => { const savedKeys = window.localStorage.getItem("SORTED_KEYS"); if (savedKeys && sortedKeys.length === 0) { setSortedKeys(JSON.parse(savedKeys)); } }, []); // 将排序顺序持久化到localStorage useEffect(() => { if (sortedKeys.length > 0) { window.localStorage.setItem("SORTED_KEYS", JSON.stringify(sortedKeys)); } }, [sortedKeys]); const deleteHandler = (key) => { firebaseDB.child(`register/${key}`).remove(() => { toast.error("Deleted!", { position: "top-right" }); // 删除后同步更新排序数组 setSortedKeys(prev => prev.filter(k => k !== key)); }); }; const dragStart = (e, position) => { dragItem.current = position; }; const dragEnter = (e, position) => { dragOverItem.current = position; }; const drop = (e) => { if (dragItem.current === dragOverItem.current) return; // 复制排序数组并重新排序 const newSortedKeys = [...sortedKeys]; const draggedKey = newSortedKeys[dragItem.current]; newSortedKeys.splice(dragItem.current, 1); newSortedKeys.splice(dragOverItem.current, 0, draggedKey); setSortedKeys(newSortedKeys); dragItem.current = null; dragOverItem.current = null; }; return ( <div className="full_Container"> <center> <h6 className="h6_heading">Winners</h6> </center> <table className="table " id="data-table"> <thead className="table-hero"> <tr> <th scope="col">S.no</th> <th className="put_pointer" scope="col">Name ▲▼</th> <th className="" scope="col">Company Name</th> <th className="put_pointer" scope="col">LTP</th> <th className="put_pointer" scope="col">%</th> <th className="put_pointer" scope="col">52 W.H</th> <th className="" scope="col">52 W.L</th> <th scope="col">M.B</th> <th className="" scope="col">Mkt.Cap</th> <th className="" scope="col">Volume</th> <th className="" scope="col">Zauba</th> <th className="" scope="col">M.C</th> <th className="" scope="col">N</th> <th className="" scope="col">C.I</th> <th className="" scope="col">Del</th> </tr> </thead> <tbody> {sortedKeys.map((key, index) => { const item = originalData[key]; if (!item) return null; // 处理已删除但排序数组未同步的情况 return ( <tr style={{ cursor: "move" }} onDragStart={(e) => dragStart(e, index)} onDragEnter={(e) => dragEnter(e, index)} onDragEnd={drop} key={key} // 使用Firebase键作为唯一标识,避免index导致的渲染问题 draggable > <td>{index + 1}</td> <td> <a target="_blank" style={{ color: "#212529" }} href={item.url}> {item.name} </a> </td> <td> <a target="_blank" style={{ color: "#212529" }} href={item.tradingview}> {item.companyname} </a> </td> <td>{item.ltp}</td> <td>{item.percent}</td> <td className="wk_low">{item.weekhigh}</td> <td className="wk_low">{item.weeklow}</td> <td className="wk_high"> {(item.weekhigh / item.weeklow).toFixed(2)} </td> <td>{item.mcap}</td> <td>{item.volume}</td> <td> <a target="_blank" href={item.zauba}> <button style={{ color: "#D39B16", padding: "3px", fontWeight: 1000 }} className="btn btn-sm" > Z </button> </a> </td> <td> <a target="_blank" href={item.mc}> <button style={{ color: "#FF60AB", padding: "3px", fontWeight: 1000 }} className="btn btn-sm" > M </button> </a> </td> <td> <a target="_blank" href={item.mc}> <button style={{ color: "#19be7b", padding: "3px", fontWeight: 1000 }} className="btn btn-sm" > N </button> </a> </td> <td> <a target="_blank" href={item.ci}> <button style={{ color: "#19be7b", padding: "3px", fontWeight: 1000 }} className="btn btn-sm" > C.I </button> </a> </td> <td> <button style={{ color: "#fff", padding: "3px", fontWeight: 1000 }} className="btn btn-sm btn-danger" onClick={() => deleteHandler(key)} > Del </button> </td> </tr> ); })} </tbody> </table> <ToastContainer theme="colored" /> </div> ); } export default App;
关键修改点说明
- 拆分状态:
originalData保留Firebase原始键值对,确保数据完整性;sortedKeys存储排序后的键数组,专门控制渲染顺序。
- 初始加载逻辑:
- 优先加载localStorage中的排序记录,不存在则用Firebase默认顺序;Firebase新增数据时自动追加到排序数组末尾。
- 拖拽排序:
- 仅对
sortedKeys数组重排,不修改原始数据,保留Firebase键;重排后自动持久化到localStorage。
- 仅对
- 渲染优化:
- 使用Firebase键作为
tr的唯一标识,避免index变化导致的不必要重渲染;增加空值判断处理边界情况。
- 使用Firebase键作为
- 删除同步:
- 删除数据时同步移除
sortedKeys中的对应键,确保排序状态与数据一致。
- 删除数据时同步移除
内容的提问来源于stack exchange,提问作者Konidoni Ravirr
相关产品推荐
相关产品推荐

