React中DataTable二次初始化失败,切换不同行数数据报错
React中DataTable动态更新行数时的异常问题及修复方案
问题描述
在React项目中使用jQuery DataTable实现日期选择后动态填充表格数据时,出现以下异常:
- 首次选择行数相同的日期(如5月3日和5月2日)切换无问题
- 切换到行数更少的日期(如5月1日)时抛出错误;若首次选择行数少的日期再切换到行数多的日期,页面直接崩溃,行数变化后表格无法正常更新
已尝试的无效修复方案
- 日期变更时销毁并清空表格
- 添加代码手动移除表头和表体:
$("#example thead").remove(); $('#example tbody').remove();
报错信息
NotFoundError: Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node.
问题根源分析
- DOM选择器错误:代码中使用
$("#example")操作表格,但实际表格id为investorTable,导致无效的DOM操作 - 手动DOM操作与React虚拟DOM冲突:直接通过jQuery移除thead/tbody,破坏了React对DOM的管理,引发节点移除异常
- 初始化时机不可靠:使用
setTimeout延迟初始化DataTable,无法确保与React的渲染周期同步 - 行key不唯一:使用数组index作为表格行的key,数据更新时可能导致React复用错误的DOM节点
- 空数据处理缺失:未处理无数据时的DataTable销毁逻辑
修复方案
1. 修正DOM选择器
确保所有表格操作使用正确的id investorTable
2. 让React完全控制表格DOM
移除手动操作thead/tbody的代码,由React通过state自动渲染表格内容
3. 同步DataTable生命周期与React渲染
在useEffect中,确保在React完成表格渲染后再初始化DataTable,移除不可靠的setTimeout,并在数据清空时销毁表格
4. 使用唯一行key
将表格行的key从index改为数据中唯一标识(如结合name和group_name)
5. 完善空数据处理
当investorData为空时,销毁已存在的DataTable实例
修复后的完整代码
import React, { useState, useEffect } from 'react'; import { Row, Col, Card, Container, CardHeader, CardBody, FormGroup, Label, Input, Form, Table } from 'reactstrap'; import 'datatables.net-dt/css/dataTables.dataTables.css'; import $ from 'jquery'; import 'datatables.net'; const StockSummary = () => { const [stockDates, setStockDates] = useState([]); const [selectedDate, setSelectedDate] = useState(''); const [investorData, setInvestorData] = useState([]); // 模拟加载日期选项 useEffect(() => { const mockDates = [ { stock_date: '2024-05-01' }, { stock_date: '2024-05-02' }, { stock_date: '2024-05-03' }, ]; setStockDates(mockDates); }, []); // 日期变更时模拟获取数据 const handleDateChange = (e) => { const date = e.target.value; setSelectedDate(date); if (date === '2024-05-01') { setInvestorData([ { name: 'Alice', number_of_shares: 100, category_name: 'Retail', group_name: 'A' }, { name: 'Bob', number_of_shares: 150, category_name: 'Retail', group_name: 'A' }, { name: 'Charlie', number_of_shares: 200, category_name: 'Institutional', group_name: 'B' }, { name: 'David', number_of_shares: 250, category_name: 'Retail', group_name: 'C' }, { name: 'Eva', number_of_shares: 300, category_name: 'Institutional', group_name: 'C' }, ]); } else if (date === '2024-05-02') { setInvestorData([ { name: 'Frank', number_of_shares: 120, category_name: 'Retail', group_name: 'A' }, { name: 'Grace', number_of_shares: 130, category_name: 'Retail', group_name: 'B' }, { name: 'Hank', number_of_shares: 140, category_name: 'Institutional', group_name: 'C' }, { name: 'Ivy', number_of_shares: 150, category_name: 'Retail', group_name: 'D' }, { name: 'Jack', number_of_shares: 160, category_name: 'Retail', group_name: 'E' }, { name: 'Karen', number_of_shares: 170, category_name: 'Retail', group_name: 'F' }, { name: 'Leo', number_of_shares: 180, category_name: 'Institutional', group_name: 'G' }, ]); } else if (date === '2024-05-03') { setInvestorData([ { name: 'Frank 1', number_of_shares: 1200, category_name: 'Retail', group_name: 'A' }, { name: 'Grace', number_of_shares: 130, category_name: 'Retail', group_name: 'B' }, { name: 'Hank', number_of_shares: 320, category_name: 'Institutional', group_name: 'C' }, { name: 'Ivy', number_of_shares: 10, category_name: 'Retail', group_name: 'D' }, { name: 'Jack', number_of_shares: 100, category_name: 'Retail', group_name: 'E' }, { name: 'Karen', number_of_shares: 170, category_name: 'Retail', group_name: 'F' }, { name: 'Leo', number_of_shares: 180, category_name: 'Institutional', group_name: 'G' }, ]); } else { setInvestorData([]); } }; // 同步DataTable初始化与销毁 useEffect(() => { // 先销毁已存在的DataTable实例 if ($.fn.DataTable.isDataTable('#investorTable')) { $('#investorTable').DataTable().destroy(true); // true参数会还原表格到初始状态 } // 数据存在时初始化DataTable if (investorData.length > 0) { // 使用requestAnimationFrame确保React已完成DOM渲染 requestAnimationFrame(() => { $('#investorTable').DataTable({ lengthChange: true, autoWidth: true, scrollCollapse: true, }); }); } // 清理函数:组件卸载时销毁表格 return () => { if ($.fn.DataTable.isDataTable('#investorTable')) { $('#investorTable').DataTable().destroy(true); } }; }, [investorData]); return ( <Container fluid> <Row className="m-1"> <Col xs={12}> <h4 className="main-title">Stock Summary</h4> </Col> </Row> <Row> <Col xs={12}> <Card> <CardHeader> <h5>Select Stock Date</h5> </CardHeader> <CardBody> <Form> <FormGroup> <Label>Select Date</Label> <Input type="select" name="stockDate" id="stockDate" value={selectedDate} onChange={handleDateChange} > <option value="">-- Select Stock Date --</option> {stockDates.map((date) => ( <option key={date.stock_date} value={date.stock_date}> {date.stock_date} </option> ))} </Input> </FormGroup> </Form> </CardBody> </Card> </Col> </Row> <Row> <Col xs={12}> <Card> <CardHeader> <h5>Investor Data</h5> </CardHeader> <CardBody> <Table id="investorTable" className="w-100 display row-border-table align-middle"> <thead> <tr> <th>Investor Name</th> <th>Share Count</th> <th>Category</th> <th>Group</th> </tr> </thead> <tbody> {investorData.map((investor) => ( <tr key={`${investor.name}-${investor.group_name}`}> <td>{investor.name}</td> <td>{investor.number_of_shares}</td> <td>{investor.category_name}</td> <td>{investor.group_name}</td> </tr> ))} </tbody> </Table> </CardBody> </Card> </Col> </Row> </Container> ); }; export default StockSummary;
内容的提问来源于stack exchange,提问作者Chuah Sze Wei
相关产品推荐
相关产品推荐

