You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React JS:ViewModel引发无限渲染循环的问题排查与解决

React中MVVM模式的无限循环问题解决及正确实现

问题原因分析

你当前的实现存在两个核心问题:

  1. 每次组件ViewContract渲染时,都会调用ViewContractViewModel()创建全新的实例——包括内部的useState状态和componentDidMount函数。
  2. 当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的状态同步。

关键总结

  1. 不要在组件渲染函数内直接创建ViewModel实例,否则每次渲染都会生成新实例,引发状态和函数引用变化。
  2. React中实现MVVM的最优方式是用自定义Hook封装ViewModel逻辑,天然契合React的函数式模型。
  3. 所有需要仅执行一次的初始化逻辑,必须放在useEffect中并设置空依赖数组。

内容的提问来源于stack exchange,提问作者Yotam

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.01 20:20:55