React JS:ViewModel引发无限渲染循环的问题排查与解决
React中MVVM模式的无限循环问题解决及正确实现
问题原因分析
你当前的实现存在两个核心问题:
- 每次组件
ViewContract渲染时,都会调用ViewContractViewModel()创建全新的实例——包括内部的useState状态和componentDidMount函数。 - 当ViewModel里调用
setisLoading或setValue触发组件重渲染后,新的ViewModel实例会生成新的componentDidMount函数,导致useEffect的依赖项变化,进而触发useEffect再次执行,最终形成无限循环。
解决思路
要解决这个问题,核心是确保ViewModel的状态和逻辑在组件多次渲染中保持稳定,同时控制初始化逻辑仅执行一次:
- 利用React Hook的特性(如自定义Hook、
useRef、useMemo)保持ViewModel实例或逻辑的唯一性。 - 将初始化逻辑放在
useEffect中,并通过空依赖数组限制其仅在组件挂载时执行一次。
正确实现示例
方式一:自定义Hook封装ViewModel(React推荐方案)
这种方式最贴合React的函数式编程模型,用自定义Hook封装ViewModel的状态和业务逻辑:
// 自定义Hook作为ViewModel,封装状态与业务逻辑 const useViewContractViewModel = () => { const [value, setValue] = useState<string | null>(null); const [isLoading, setIsLoading] = useState(false); // 仅在组件挂载时执行一次初始化请求 useEffect(() => { const fetchData = async () => { setIsLoading(true); const fetchedValue = await fetchValueFromServer(); setValue(fetchedValue); setIsLoading(false); }; fetchData(); }, []); // 空依赖数组确保只执行一次 // 可添加更多业务方法,比如更新值的逻辑 const updateValue = async (newValue: string) => { setIsLoading(true); await updateValueOnServer(newValue); // 假设存在更新服务器的逻辑 setValue(newValue); setIsLoading(false); }; return { value, isLoading, updateValue }; }; export default function ViewContract() { const { value, isLoading } = useViewContractViewModel(); return ( <div className='App-header'> {isLoading ? 'Loading' : value ? value : 'View Contract'} </div> ); }
说明:
- 自定义Hook内部的
useState会在组件多次渲染时保留之前的状态,不会重新初始化。 - 初始化逻辑放在Hook内部的
useEffect中,空依赖数组保证仅在组件挂载时执行一次,彻底避免循环。
方式二:类式ViewModel(贴近iOS MVVM结构)
如果习惯iOS中类式的ViewModel写法,可以用useRef保存ViewModel实例,确保其在组件生命周期内唯一:
// 类式ViewModel,封装业务逻辑 class ViewContractViewModel { value: string | null = null; isLoading = false; private setValue: React.Dispatch<React.SetStateAction<string | null>>; private setIsLoading: React.Dispatch<React.SetStateAction<boolean>>; constructor(setValue: React.Dispatch<React.SetStateAction<string | null>>, setIsLoading: React.Dispatch<React.SetStateAction<boolean>>) { this.setValue = setValue; this.setIsLoading = setIsLoading; } async fetchData() { this.isLoading = true; this.setIsLoading(true); const fetchedValue = await fetchValueFromServer(); this.value = fetchedValue; this.setValue(fetchedValue); this.isLoading = false; this.setIsLoading(false); } } export default function ViewContract() { const [value, setValue] = useState<string | null>(null); const [isLoading, setIsLoading] = useState(false); // 用useRef保存ViewModel实例,确保多次渲染复用同一实例 const viewModelRef = useRef<ViewContractViewModel | null>(null); useEffect(() => { // 仅在组件挂载时创建一次ViewModel if (!viewModelRef.current) { viewModelRef.current = new ViewContractViewModel(setValue, setIsLoading); } // 执行初始化数据请求 viewModelRef.current.fetchData(); }, []); // 空依赖数组限制仅执行一次 return ( <div className='App-header'> {isLoading ? 'Loading' : value ? value : 'View Contract'} </div> ); }
说明:
useRef保存的ViewModel实例在组件多次渲染时不会被重新创建,保持逻辑的一致性。- 类内部通过接收React的状态更新函数来触发组件刷新,实现ViewModel到View的状态同步。
关键总结
- 不要在组件渲染函数内直接创建ViewModel实例,否则每次渲染都会生成新实例,引发状态和函数引用变化。
- React中实现MVVM的最优方式是用自定义Hook封装ViewModel逻辑,天然契合React的函数式模型。
- 所有需要仅执行一次的初始化逻辑,必须放在
useEffect中并设置空依赖数组。
内容的提问来源于stack exchange,提问作者Yotam
相关产品推荐
相关产品推荐

