React父组件触发重置后二级子组件未更新值的问题求助
更新:
我认为问题在于父组件触发handleReset()时,二级子组件的props未变化导致不重新渲染,但如何让它们重新渲染?我尝试用useEffect但无效。
我卡这个问题好几个小时了:父组件把useState里的值传给一级子组件CustomerData,再往下传给二级子组件AddressForm和CustomerForm。现在的问题是,父组件调用handleReset()后,二级子组件里的值没更新,我搞不清原因。我觉得重置时二级子组件没重新渲染,得找到正确的状态传递方式。
下面是我的组件代码:
父组件
function WidgetCustomerData(props): JSX.Element { const customerDataTest = { customerFirstName: 'firstNameTest', customerLastName: 'lastNameTest', customerPhone: '1313212', customerEmail: 'test@test.com', customerAddress: 'Musterstraße', customerAddressNo: '12', customerZip: '80335', customerCity: 'Berlin', deviceAddress: 'Straße', deviceAddressNo: '152', deviceZip: '32214', deviceCity: 'Hamburg', }; const [customerData, setCustomerData] = useState({ addressValue: customerDataTest.customerAddress, addressValueNr: customerDataTest.customerAddressNo, addressValueZip: customerDataTest.customerZip, addressValueCity: customerDataTest.customerCity, customerFirstName: customerDataTest.customerFirstName, customerLastName: customerDataTest.customerLastName, customerPhone: customerDataTest.customerPhone, customerEmail: customerDataTest.customerEmail, deviceAddress: customerDataTest.deviceAddress, deviceAddressNo: customerDataTest.deviceAddressNo, deviceZip: customerDataTest.deviceZip, deviceCity: customerDataTest.deviceCity }) const handleClose = () => { props.handleClose(); } const handleSubmit = async (e) => { e && e.preventDefault(); const formUrl = 'https://fetch.mock/widget-customer-data/addCustomerData'; const formData = new FormData(e.target); for (const [name, value] of formData) { console.log(`${name}: ${value}`); } const response = await fetch(formUrl.toString(), { method: 'POST', body: formData, credentials: 'same-origin', }); const data = await response.json(); }; const handleReset = () => { console.log('handleReset ----'); setCustomerData({ addressValue: customerDataTest.customerAddress, addressValueNr: customerDataTest.customerAddressNo, addressValueZip: customerDataTest.customerZip, addressValueCity: customerDataTest.customerCity, customerFirstName: customerDataTest.customerFirstName, customerLastName: customerDataTest.customerLastName, customerPhone: customerDataTest.customerPhone, customerEmail: customerDataTest.customerEmail, deviceAddress: customerDataTest.deviceAddress, deviceAddressNo: customerDataTest.deviceAddressNo, deviceZip: customerDataTest.deviceZip, deviceCity: customerDataTest.deviceCity }) }; return ( <div className="customer-data"> <Dialog onClose={handleClose} open={props.open} aria-labelledby="customized-dialog-title" PaperProps={{ style: { borderRadius: 20, minWidth: '80%', maxHeight: 'fit-content' }, }}> <DialogTitle id="customized-dialog-title" onClose={handleClose} /> <Typography style={{ marginTop: 20, paddingLeft: 48, fontSize: 32, fontWeight: 600 }}>Kundendaten bearbeiten</Typography> {/* <DialogContent> */} <form onSubmit={handleSubmit}> <div style={{ paddingLeft: 48, paddingRight:48 }}> <CustomerData customerData={customerData} /> </div> <DialogActions> <ResetButton onClick={handleReset} variant="contained" color="primary"> Reset </ResetButton> <SaveButton type="submit" variant="contained" color="primary"> Save </SaveButton> </DialogActions> </form> {/* </DialogContent> */} </Dialog> </div> ); } export default WidgetCustomerData;
一级子组件
export default ({nextStepAction, customerData }: MoleculeSystemManagementCustomerDataProps): JSX.Element => { return ( <div className='c-data'> <CustomerForm customerData={customerData} /> <AddressForm formId='customer' customerData={customerData} /> </div> ); }
二级子组件(AddressForm)
function AddressForm({formId, customerData }): JSX.Element { const classes = useStyles(); const { addressValue, addressValueNr, addressValueZip, addressValueCity, deviceAddress, deviceAddressNo, deviceZip, deviceCity } = customerData; return ( <Grid container className={classes.borderedRow}> <Grid item xs={12} sm={6} md={3} className={classes.formColumn}> <div> <InputField type='label' id='address' name={`${formId}-address`} value={formId === 'customer' ? addressValue : deviceAddress} label={'Street'} /> </div> </Grid> <Grid item xs={12} sm={6} md={3} className={classes.formColumn}> <InputField type='label' id='city' name={`${formId}-city`} value={formId === 'customer' ? addressValueNr : deviceAddressNo} label={'Number'} /> </Grid> <Grid item xs={12} sm={6} md={3} className={classes.formColumn}> <InputField type='label' id='state' name={`${formId}-state`} value={formId === 'customer' ? addressValueZip : deviceZip} label={'ZIP'} /> </Grid> <Grid item xs={12} sm={6} md={3} className={classes.formColumn}> <InputField type='label' id='zip' name={`${formId}-zip`} value={formId === 'customer' ? addressValueCity : deviceCity} label={'ORT'} /> </Grid> </Grid> ); } export default AddressForm;
问题根源
核心问题大概率是**InputField组件没有正确响应props的value变化**:如果InputField内部维护了自己的state(非受控组件),外部传入的value只会在初始化时生效,后续props更新不会同步到输入框。另外你重置的状态和初始值完全一致,不过你每次都创建了新对象,React浅比较会识别到变化,所以不是这个原因。
修复步骤
把
InputField改成标准受控组件
去掉InputField内部的本地状态,完全依赖外部传入的value和onChange:function InputField({ value, onChange, ...rest }) { return <input value={value} onChange={onChange} {...rest} />; }如果必须保留内部状态,要监听外部
value的变化同步更新:function InputField({ value, onChange, ...rest }) { const [inputValue, setInputValue] = useState(value); useEffect(() => { setInputValue(value); }, [value]); // 外部value变化时同步内部状态 const handleChange = (e) => { setInputValue(e.target.value); onChange?.(e); }; return <input value={inputValue} onChange={handleChange} {...rest} />; }优化父组件重置逻辑(可选)
把初始状态抽成独立函数,避免重复代码,同时保证每次重置都是新对象:const getInitialCustomerData = () => ({ addressValue: customerDataTest.customerAddress, addressValueNr: customerDataTest.customerAddressNo, addressValueZip: customerDataTest.customerZip, addressValueCity: customerDataTest.customerCity, customerFirstName: customerDataTest.customerFirstName, customerLastName: customerDataTest.customerLastName, customerPhone: customerDataTest.customerPhone, customerEmail: customerDataTest.customerEmail, deviceAddress: customerDataTest.deviceAddress, deviceAddressNo: customerDataTest.deviceAddressNo, deviceZip: customerDataTest.deviceZip, deviceCity: customerDataTest.deviceCity }); // 初始化state const [customerData, setCustomerData] = useState(getInitialCustomerData()); // 重置函数 const handleReset = () => { console.log('handleReset ----'); setCustomerData(getInitialCustomerData()); };强制子组件重渲染(兜底方案)
如果上面的方法无效,可以给二级子组件添加动态key,强制React销毁旧组件并创建新实例:// 在一级子组件中修改 <CustomerForm key={`customer-form-${JSON.stringify(customerData)}`} customerData={customerData} /> <AddressForm key={`address-form-${JSON.stringify(customerData)}`} formId='customer' customerData={customerData} />注意:
JSON.stringify只适合小对象,大对象会影响性能,优先用前两种方法。
内容的提问来源于stack exchange,提问作者Christian

