函数组件子组件Props引用过时状态的原因解析(Next.js场景)
动态添加子组件的函数组件状态异常问题
我实际使用的是Next.js,但原理与React一致。正在开发一个可动态添加字段(子组件)的表单,默认显示1个字段,且始终保持至少1个字段。
父组件代码(Parent.js)
import {useState, useEffect} from 'react' import Rooms from './Rooms' export default function TourOverview(props) { const [roomList, setRoomList] = useState([]) useEffect(()=>{ // 默认添加一个不可删除的房间 setRoomList([<Rooms addRoomCount={addRoomCount} id={1} key={1} />]) }, []) const addRoomCount=()=> { let tempArr = roomList // 浅拷贝 tempArr.push(<Rooms removeRoomCount={removeRoomCount} id={roomList.length+1} key={roomList.length+1} />) setRoomList([...tempArr]) } const removeRoomCount=(id)=>{ // 暂不实现,非问题相关 } return ( <> {roomList} </> ) }
子组件代码(Rooms.js)
import {useState, useEffect} from 'react' export default function Rooms(props, ref) { return ( <> Room {props.id} {props.addRoomCount != undefined ? <button type="button" className="btn btn-link" onClick={props.addRoomCount}> + Add Room </button> : <button type="button" className="btn btn-link" onClick={()=>props.removeRoomCount(props.id)}> - Remove </button> } </> ) }
问题现象
从代码逻辑看,父组件首次加载会自动添加一个带“+ Add Room”按钮的子组件,后续添加的子组件仅显示“- Remove”按钮。但实际运行时,点击第一个子组件的添加按钮,roomList状态会重置为初始的空状态——本质是第一个子组件Props中的addRoomCount函数引用的是初始加载时的空roomList状态。类组件版本功能正常,但函数组件出现此问题。
原因分析
这是函数组件的闭包陷阱导致的:
- 父组件初始化时,依赖为空数组的
useEffect执行,此时创建的addRoomCount函数是基于初始空roomList的闭包,它内部访问的roomList永远是初始的空数组。 - 这个旧的
addRoomCount被传给了第一个Rooms子组件,当后续父组件重新渲染(比如添加新房间后),会生成新的addRoomCount函数(基于更新后的roomList),但第一个子组件是直接存储在roomList状态里的React元素,它的props.addRoomCount还是旧的闭包函数。 - 点击第一个子组件的添加按钮时,旧的
addRoomCount执行,会把tempArr设为空数组,push新元素后调用setRoomList,直接覆盖了之前已添加的房间,看起来就像状态被重置了。
解决方案
方案1:使用函数式更新获取最新状态
修改addRoomCount,利用setRoomList的函数式更新特性,直接获取最新的状态值,避免闭包带来的旧值问题:
const addRoomCount = () => { setRoomList(prevRoomList => { const newRoomId = prevRoomList.length + 1; return [...prevRoomList, <Rooms removeRoomCount={removeRoomCount} id={newRoomId} key={newRoomId} />]; }); };
方案2:状态存储数据而非React元素(推荐)
更规范的做法是,状态里只存储房间的数据源(比如id数组),渲染时再根据数据生成组件。这样能从根本上避免闭包和React元素引用过期的问题:
// 修改后的Parent.js代码 import { useState, useEffect } from 'react'; import Rooms from './Rooms'; export default function TourOverview(props) { // 状态只存房间id数组,不存React元素 const [roomIds, setRoomIds] = useState([]); useEffect(() => { // 默认添加第一个房间 setRoomIds([1]); }, []); const addRoomCount = () => { setRoomIds(prevIds => [...prevIds, prevIds.length + 1]); }; const removeRoomCount = (id) => { // 确保至少保留一个房间 if (roomIds.length > 1) { setRoomIds(prevIds => prevIds.filter(item => item !== id)); } }; return ( <> {roomIds.map(id => ( <Rooms key={id} id={id} // 只有第一个房间显示添加按钮 addRoomCount={id === 1 ? addRoomCount : undefined} removeRoomCount={removeRoomCount} /> ))} </> ); }
这种方式下,每次父组件渲染都会重新生成Rooms组件,子组件拿到的props都是最新的函数引用,彻底解决闭包问题。
内容的提问来源于stack exchange,提问作者GnomeMage
相关产品推荐
相关产品推荐

