React Hook渲染数量不一致错误,请求排查原因
问题背景
遇到React的「more hooks than previous render」错误,已排除条件语句中初始化state的问题,定位到表格生成函数内的state(位于map循环调用的GenerateFamilyRow组件中),同时怀疑FileDirectoryBody中的async useEffect是否影响。
错误信息
Previous render Next render ------------------------------------------------------ 1. useState useState 2. useEffect useEffect 3. useEffect useEffect 4. undefined useState
相关代码
FileDirectory(顶层组件)
export default function FileDirectory(){ return( <div> <Header/> <FileDirectoryBody/> </div> ) }
FileDirectoryBody组件
export default function FileDirectoryBody(){ const[files,setFiles] = useState([]) useEffect(()=>{ // Testing console.log('files updated') console.log(files) },[files]) useEffect(async()=>{ setFiles(sortByFamily(await getAllFiles())) },[]) return( <div class='family-file-container'> <FamilyTable sortedFiles={files}/> </div> ); }
FamilyTable组件
export default function FamilyTable(props){ const[sortedFiles,setSortedFiles] = useState([]) useEffect(()=>{ setSortedFiles(props.sortedFiles) },[props.sortedFiles]) useEffect(()=>{ // Testing console.log('sorted Files updated') console.log(sortedFiles) },[sortedFiles]) const generateRows = () =>{ // Maps rows of files and their meta data return( sortedFiles.map((family,index)=>{ return( generateFamilyRow(family) ) }) ) } return( <div> <div class='file-directory-header'> <h1 class='header-text'>File Directory</h1> </div> <div class='column-row'> <div class='header-column-row-text'>Family Name</div> <div class='header-column-row-text'>Cell Count</div> <div class='header-column-row-text'>Expand Family</div> </div> <Line color={'black'}/> <div class='directory-body'> {generateRows()} </div> </div> ); }
GenerateFamilyRow组件(问题所在)
export default function GenerateFamilyRow(family){ const[open,setOpen] = useState([]) // State that is causing the issue const toggle = () =>{setOpen(!open)} return( <div> <div class='family-row'> <div class='family-row-item'>{family[0].family_name}</div> <div class='family-row-item'>{family.length}</div> <div class='family-row-item'><button onClick={()=>{toggle()}}>{open ? '^' : '⌄'}</button></div> <Line color={'black'}/> </div> {open && <table class='family-table' id={`family-table-${family.family_name}`}> <tr> <th>File Name</th> <th>Cell ID</th> <th>Cell Powder ID</th> <th>Cell Family Name</th> <th>Cell Last Uploaded</th> </tr> </table>} </div> ); }
问题原因及解决方案
1. 组件渲染方式错误(核心问题)
在FamilyTable的generateRows里直接调用generateFamilyRow(family)是普通函数调用,不是React组件渲染。React组件必须用<Component />语法渲染,且组件名需大写开头。
直接调用函数时,内部的useState会在父组件每次render时执行,而sortedFiles长度会随异步请求结果变化(从空数组变为有数据的数组),导致每次render时执行的useState数量改变,触发hooks数量不一致的错误。
修复方案:
在map中改用组件渲染语法,同时添加唯一key:
const generateRows = () => { return sortedFiles.map((family, index) => { return <GenerateFamilyRow key={family[0].family_name} family={family} /> }) }
2. async useEffect的错误用法
useEffect的回调不能是async函数,因为async函数返回Promise,而React期望回调返回清理函数或undefined,这会导致副作用处理异常。
修复方案:在useEffect内部定义async函数并调用:
useEffect(() => { const fetchFiles = async () => { setFiles(sortByFamily(await getAllFiles())) } fetchFiles() }, [])
3. 冗余的state同步
FamilyTable中无需额外维护sortedFiles的state,直接使用props.sortedFiles即可,避免多一层state带来的同步问题:
export default function FamilyTable(props){ // 移除冗余的state和同步逻辑 // const[sortedFiles,setSortedFiles] = useState([]) // useEffect(()=>{setSortedFiles(props.sortedFiles)},[props.sortedFiles]) const generateRows = () => { return props.sortedFiles.map((family, index) => { return <GenerateFamilyRow key={family[0].family_name} family={family} /> }) } useEffect(()=>{ // Testing console.log('sorted Files updated') console.log(props.sortedFiles) },[props.sortedFiles]) // 其余return代码保持不变 }
4. 初始state逻辑不合理
GenerateFamilyRow中open的初始值是[],空数组在JS中属于真值,第一次点击后会变成false,后续切换逻辑混乱。直接把初始值设为false更符合需求:
const[open,setOpen] = useState(false)
内容的提问来源于stack exchange,提问作者boredProjects

