React中调用两个useState的set函数为何引发单选按钮切换异常?
React单选按钮切换异常:点击Description需两次生效的原因及解决方案
问题代码及现象
function App() { const guidance_descriptions = { "text":"dummy text A", "description": "dummy text B" }; const [guidanceText,setGuidanceText] = useState(guidance_descriptions['text']); const ModeButtons = () => { const [mode,setMode] = useState("text"); function changeSelectedMode(e) { setMode(e.target.value); setGuidanceText(guidance_descriptions[e.target.value]); } return ( <div> <input type="radio" value="text" name="selectMode" checked={mode==="text"} onChange={changeSelectedMode}/> Text <input type="radio" value="description" name="selectMode" checked={mode==="description"} onChange={changeSelectedMode}/> Description </div> ) } return ( <div className="App"> <header className="App-header"> <ModeButtons/> </header> </div> ); } export default App;
现象:切换到「Description」单选按钮需要点击两次才生效,切换到「Text」仅需一次;移除setGuidanceText(guidance_descriptions[e.target.value]);后功能恢复正常。
原因分析
核心问题是**ModeButtons组件被定义在App组件内部**,当App组件的状态guidanceText更新时,App会触发重新渲染,此时内部定义的ModeButtons会被重新创建(相当于一个全新的组件实例),其内部的mode状态会被重置为初始值"text"。
具体流程拆解:
- 第一次点击Description:
- 触发
changeSelectedMode,执行setMode("description")更新ModeButtons内部状态,同时执行setGuidanceText更新App的guidanceText为"dummy text B" App因状态更新重新渲染,ModeButtons被重新创建,内部mode重置为"text",导致单选按钮的checked属性回到text的选中状态,看起来像是点击没生效
- 触发
- 第二次点击Description:
- 再次执行
setGuidanceText时,设置的值还是"dummy text B",和当前guidanceText一致,App不会触发重新渲染 ModeButtons不会被重置,setMode("description")能正常更新内部状态,checked属性正确显示选中状态,所以点击生效
- 再次执行
- 点击Text的情况:
- 执行
setGuidanceText时设置的是"dummy text A",和App初始的guidanceText值一致,App不会重新渲染 ModeButtons不会被重置,setMode("text")直接生效,一次点击就显示选中状态
- 执行
解决方案
方案一:将ModeButtons移到App组件外部并改为受控组件
把状态mode提升到App组件中,让ModeButtons成为受控组件,避免内部状态重置的问题:
import { useState } from 'react'; // 组件移到App外部,避免每次App渲染都重新创建 const ModeButtons = ({ currentMode, onModeChange }) => { const changeSelectedMode = (e) => { onModeChange(e.target.value); }; return ( <div> <input type="radio" value="text" name="selectMode" checked={currentMode === "text"} onChange={changeSelectedMode} /> Text <input type="radio" value="description" name="selectMode" checked={currentMode === "description"} onChange={changeSelectedMode} /> Description </div> ); }; function App() { const guidance_descriptions = { "text":"dummy text A", "description": "dummy text B" }; const [guidanceText, setGuidanceText] = useState(guidance_descriptions['text']); const [mode, setMode] = useState("text"); // 将mode状态提升到App const handleModeChange = (newMode) => { setMode(newMode); setGuidanceText(guidance_descriptions[newMode]); }; return ( <div className="App"> <header className="App-header"> <ModeButtons currentMode={mode} onModeChange={handleModeChange} /> </header> <p>{guidanceText}</p> </div> ); } export default App;
方案二:用React.memo和useCallback优化内部组件
如果必须把ModeButtons留在App内部,可以通过缓存组件和函数,避免不必要的重渲染:
import { useState, useCallback, memo } from 'react'; function App() { const guidance_descriptions = { "text":"dummy text A", "description": "dummy text B" }; const [guidanceText, setGuidanceText] = useState(guidance_descriptions['text']); // 用useCallback缓存更新函数,避免每次App渲染都创建新函数 const updateGuidanceText = useCallback((value) => { setGuidanceText(value); }, []); // 用memo包裹ModeButtons,只有props变化时才重新渲染 const ModeButtons = memo(() => { const [mode, setMode] = useState("text"); const changeSelectedMode = (e) => { setMode(e.target.value); updateGuidanceText(guidance_descriptions[e.target.value]); }; return ( <div> <input type="radio" value="text" name="selectMode" checked={mode==="text"} onChange={changeSelectedMode}/> Text <input type="radio" value="description" name="selectMode" checked={mode==="description"} onChange={changeSelectedMode}/> Description </div> ); }); return ( <div className="App"> <header className="App-header"> <ModeButtons/> </header> <p>{guidanceText}</p> </div> ); } export default App;
内容的提问来源于stack exchange,提问作者Oliver Liu
相关产品推荐
相关产品推荐

