如何解决React点击时元素重复生成、函数执行两次的问题
问题根因
- 异步查询和DOM生成逻辑错误放在render函数中:React的render函数会在每次状态更新时重新执行,你在render内调用
waitForData触发Firestore查询,每次render都会触发一次数据拉取和后续DOM生成逻辑,直接导致重复执行。 - 违背React设计逻辑使用原生DOM操作:你没有通过React状态管理渲染列表,而是直接用原生API创建、插入DOM,React无法感知这部分DOM的存在,状态更新重渲染时会重复生成节点。同时循环生成的元素都使用相同id
writeto,违反id唯一性规则,导致DOM查询和赋值逻辑异常,新增的节点没有被赋值出现空白。 - 新增节点前未清空容器:每次调用
getfiles都直接向.filtered-docs追加节点,没有清空之前的内容,点击次数越多生成的冗余节点越多。 - 新增数据后触发的逻辑链重复:点击按钮触发
createFileToDb写入数据后,触发组件状态更新重渲染,render内的查询逻辑又会执行一次,重复生成节点。
修复步骤
- 把所有异步查询逻辑从render函数中移出,放到组件生命周期或事件回调中:组件挂载时(
componentDidMount)执行一次初始数据拉取,新增文档成功后在createFileToDb的then回调中主动拉取一次数据即可,不要在render中做任何副作用操作。 - 用React状态管理列表渲染,完全去掉原生DOM操作:在state中新增
documentList字段存储从Firestore拿到的文档数据,查询到数据后调用setState更新状态,render函数中直接遍历documentList生成对应的DOM节点,不要手动调用createElement、appendChild等API。 - 去掉重复id,循环生成的元素改用唯一key标识,或者用class替代id做标识。
- 表单取值改用React受控组件实现,不要用
getElementById从DOM中拿值,避免直接操作DOM的各种异常。
如果暂时不想改造为React状态渲染的方式,每次调用getfiles生成节点前,先清空容器的原有内容,新增代码document.querySelector('.filtered-docs').innerHTML = '';即可解决重复追加的问题,但还是推荐使用React官方的渲染方式避免后续更多异常。
改造示例代码
class DocumentComponent extends React.Component { // 初始化state,存储表单值和文档列表 state = { documentList: [], docName: '', specifiedWidth: '', specifiedHeight: '', colorType: '' } componentDidMount() { // 组件挂载后拉取初始文档列表 this.loadDocumentList(); } // 封装统一的文档拉取方法 loadDocumentList = async () => { const list = await this.getDataFromFirebase(); this.setState({ documentList: list }); } // Firebase拉取数据逻辑 getDataFromFirebase = () => { return firebase.firestore() .collection("documents") .doc(firebase.auth().currentUser.uid) .collection("userDocs") .orderBy("creation", "asc") .get() .then(snapshot => { return snapshot.docs.map(doc => ({ id: doc.id, data: doc.data() })); }).catch(err => console.error('拉取数据失败:', err)); } // 写入Firebase逻辑 createFileToDb = () => { const { docName, specifiedWidth, specifiedHeight, colorType } = this.state; firebase.firestore().collection("documents") .doc(firebase.auth().currentUser.uid) .collection("userDocs") .add({ documentName: docName, width: Number(specifiedWidth), height: Number(specifiedHeight), docColorType: colorType, creation: firebase.firestore.FieldValue.serverTimestamp() }).then(() => { console.log("文件写入数据库成功"); // 写入成功后重新拉取列表刷新页面 this.loadDocumentList(); }).catch(() => { console.log("文件写入失败"); }) } // 表单输入绑定state,实现受控组件 handleInputChange = (e) => { this.setState({ [e.target.id]: e.target.value }); } render() { const { documentList } = this.state; return ( <div> {/* 表单区域 */} <input id="docName" value={this.state.docName} onChange={this.handleInputChange} placeholder="自定义标题" /> <input id="specifiedWidth" value={this.state.specifiedWidth} onChange={this.handleInputChange} placeholder="文档宽度" /> <input id="specifiedHeight" value={this.state.specifiedHeight} onChange={this.handleInputChange} placeholder="文档高度" /> <select id="colorType" value={this.state.colorType} onChange={this.handleInputChange} > <option value="light">浅色</option> <option value="dark">深色</option> </select> <button onClick={this.createFileToDb}>创建文档</button> {/* 文档列表渲染区域,完全通过state生成,不需要手动操作DOM */} <div className="filtered-docs"> {documentList.map(docItem => ( <div key={docItem.id} className="result"> <p className="writeto">{docItem.data.documentName}</p> </div> ))} </div> </div> ) } }
内容的提问来源于stack exchange,提问作者user14223729
相关产品推荐
相关产品推荐

