React使用useState/useEffect实现表单编辑保存CRUD功能求助
React CRUD场景下useState/useEffect正确实现指南

当前实现的基础逻辑:输入框默认禁用,通过placeholder展示历史字段值,基于useState/useEffect做状态更新,保存按钮默认加hidden类隐藏。
实现目标
- 给每个编辑按钮绑定点击事件:点击后隐藏当前编辑按钮、启用对应输入框、取消对应保存按钮的隐藏状态
- 给每个保存按钮绑定独立点击事件:点击后隐藏当前保存按钮、显示编辑按钮、将输入框恢复为禁用状态、将输入框placeholder更新为新输入值并提交数据(提交逻辑可复用现有Firebase对接代码)
原有问题代码
当前实现混用了原生Vanilla JS的DOM操作思路,这是问题的核心原因:React是声明式框架,UI完全由状态驱动,禁止手动查询DOM、绑定原生事件、修改DOM属性/class,这类操作会脱离React的渲染管控,很容易出现状态和UI不同步的问题。
原有JSX代码
<label className="col"> <div className="row"> <p className="bold nowrap inline-value-title" >Test Name: </p> <input id="display-name-change" type="text" className="form-reset inline-input" onChange={(e) => setDisplayName(e.target.value)} value={displayName} placeholder={user.displayName} disabled /> <button type="button" className="inline-button edit-val-btn"> <FontAwesomeIcon icon={faPenToSquare} /> </button> <button type="button" className="hidden inline-button save-val-btn">. <FontAwesomeIcon icon={faCircleCheck} /> </button> </div> </label>
原有DOM操作风格逻辑代码
const editValueBtns = document.querySelectorAll('.edit-val-btn'); const saveValueBtns = document.querySelectorAll('.save-val-btn'); useEffect(() => { editValueBtns.forEach((button) => { button.addEventListener('click', (e) => { button.classList.add('hidden') button.nextSibling.classList.remove('hidden') button.parentElement.children[1].removeAttr("disabled") ; }) }) saveValueBtns.forEach((button) => { button.addEventListener('click', (e) => { button.classList.add('hidden') button.previousSibling.classList.remove('hidden') button.parentElement.children[1].addAttr("disabled") ; }) }) }, []);
原有Firebase/Firestore提交代码
const handleSubmit = async (e) => { e.preventDefault(); let selectedFile = document.querySelector('#thumbnailInput') // displayname if(displayName.length == 0){console.log('No change of name')} else { console.log('change to displayname') updateProfile(user, { displayName }) setDoc(doc(db, 'users', user.uid), { displayName }, { merge: true }) } // phone Number if (phoneNo.length == 0){console.log('No change of phone no')} else { console.log('change to phone') updateProfile(user, { phoneNo }) setDoc(doc(db, 'users', user.uid), { phoneNo }, { merge: true }) } // title if (title.length == 0){console.log('No change of title')} else { console.log('change to title') updateProfile(user, { title }) setDoc(doc(db, 'users', user.uid), { title }, { merge: true }) } // avatar thumbnail if(selectedFile.files[0] == undefined){ console.log('no change to thumbnail') } else { console.log('change to thumbnail') // 传入路径创建StorageReference const storageRef = ref(storage,`thumbnails/${user.uid}/${displayName}`) // 上传图片,file参数为blob格式 await uploadBytes(storageRef, thumbnail); const downloadUrl = await getDownloadURL(storageRef); // 等待profile更新完成 await updateProfile(user, { photoURL: downloadUrl }) setDoc(doc(db, 'users', user.uid), { photoURL: downloadUrl, }, { merge: true }) } // 清空所有表单输入 const inputs = e.target.querySelectorAll('.form-reset') inputs.forEach((input) => { input.value="" }) }
正确实现方式
核心原则:所有UI变化都由状态控制,不做任何手动DOM操作
第一步:补充状态定义
因为存在多个可编辑字段(姓名、手机号、职位、头像),单独定义一个状态对象统一管理每个字段的编辑模式即可,不需要额外的DOM查询:
// 假设你已经定义了存储输入值的状态:displayName/phoneNo/title/thumbnail // 新增编辑状态,key对应字段名,value为布尔值代表是否处于编辑中 const [editMode, setEditMode] = useState({ displayName: false, phoneNo: false, title: false })
第二步:改写JSX,所有属性直接绑定状态
所有的disabled、className、点击事件都直接和状态绑定,删掉原有手动绑定事件的useEffect代码:
<label className="col"> <div className="row"> <p className="bold nowrap inline-value-title" >Test Name: </p> <input id="display-name-change" type="text" className="form-reset inline-input" onChange={(e) => setDisplayName(e.target.value)} value={displayName} placeholder={user.displayName} {/* 编辑模式下才取消禁用 */} disabled={!editMode.displayName} /> {/* 编辑模式下隐藏编辑按钮 */} <button type="button" className={`inline-button edit-val-btn ${editMode.displayName ? 'hidden' : ''}`} onClick={() => setEditMode(prev => ({...prev, displayName: true}))} > <FontAwesomeIcon icon={faPenToSquare} /> </button> {/* 非编辑模式下隐藏保存按钮 */} <button type="button" className={`inline-button save-val-btn ${editMode.displayName ? '' : 'hidden'}`} onClick={async () => { // 先执行提交逻辑 if(displayName.trim().length > 0) { await updateProfile(user, { displayName }) await setDoc(doc(db, 'users', user.uid), { displayName }, { merge: true }) // 提交成功后更新user对象里的displayName,placeholder会自动更新为新值 setUser(prev => ({...prev, displayName})) // 清空输入框 setDisplayName('') } // 退出编辑模式 setEditMode(prev => ({...prev, displayName: false})) }} > <FontAwesomeIcon icon={faCircleCheck} /> </button> </div> </label>
第三步:优化原有提交逻辑
删掉handleSubmit里所有document.querySelector类的DOM操作代码:
- 不需要手动找DOM设置input的disabled属性,状态变更后React会自动更新
- 不需要手动找DOM清空input值,直接调用
setDisplayName('')这类状态更新方法,因为input的value已经绑定了状态,状态清空后输入框自动清空 - 不需要手动增删元素的hidden类,className已经和editMode状态绑定,状态变化后class自动更新
其他字段(手机号、职位)的实现逻辑和上面的displayName完全一致,只要在editMode里加对应的字段,复制结构改下对应状态名和提交逻辑即可。
内容的提问来源于stack exchange,提问作者Michael Martell
相关产品推荐
相关产品推荐

