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

React中无法将拖拽排序数据存入localStorage并在刷新后复用

问题分析

你的代码存在几个核心问题导致拖拽排序无法持久化到localStorage:

  1. 初始数据加载冲突:两个useEffect在组件挂载时同时执行,Firebase数据和localStorage数据会互相覆盖,最终状态取决于哪个请求完成得晚。
  2. 数据结构不匹配:Firebase返回键值对对象,但拖拽逻辑将其转为数组后存回状态,后续渲染用Object.keys处理数组,导致数据结构混乱。
  3. 丢失Firebase键:拖拽时直接操作数据值,丢失原始Firebase键,影响删除等依赖键的功能。
  4. 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;

关键修改点说明

  1. 拆分状态:
    • originalData保留Firebase原始键值对,确保数据完整性;sortedKeys存储排序后的键数组,专门控制渲染顺序。
  2. 初始加载逻辑:
    • 优先加载localStorage中的排序记录,不存在则用Firebase默认顺序;Firebase新增数据时自动追加到排序数组末尾。
  3. 拖拽排序:
    • 仅对sortedKeys数组重排,不修改原始数据,保留Firebase键;重排后自动持久化到localStorage。
  4. 渲染优化:
    • 使用Firebase键作为tr的唯一标识,避免index变化导致的不必要重渲染;增加空值判断处理边界情况。
  5. 删除同步:
    • 删除数据时同步移除sortedKeys中的对应键,确保排序状态与数据一致。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.19 22:05:26