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

仅合法API状态类型模式下如何解决React组件状态重置问题

问题:保留合法API状态类型同时避免React组件状态重置

定义的API状态TypeScript类型

interface ApiInit {
  kind: 'init';
}
interface ApiSuccess<T> {
  kind: 'success';
  data: T;
}
interface ApiFail {
  kind: 'fail';
  errors: string[];
}
interface ApiLoading {
  kind: 'loading';
}

type ApiReturn<T> = ApiSuccess<T> | ApiFail | ApiLoading | ApiInit

问题场景及示例代码

当前使用上述类型时,每次API进入success状态,React会把返回的data视为全新数组,加上中间的loading状态切换,会导致嵌套的Org组件先卸载再重新挂载,内部状态被重置。以下是完整示例代码:

import React, { useState, useEffect } from 'react';
import { render } from 'react-dom';

interface ApiInit {
  kind: 'init';
}
interface ApiSuccess<T> {
  kind: 'success';
  data: T;
}
interface ApiFail {
  kind: 'fail';
  errors: string[];
}
interface ApiLoading {
  kind: 'loading';
}

type ApiReturn<T> = ApiSuccess<T> | ApiFail | ApiLoading | ApiInit

type IOrg = { name: string };

const api = {
  getOrgs: async () => ({
    data: [{ name: 'org1' }, { name: 'org2' }],
    errors: [],
  }),
};

const useApi = <T,>(apiFunc) => {
  const [state, setState] = useState<ApiReturn<T>>({kind: 'init'});

  const request = async () => {
    setState({ kind: 'loading' });
    const { data, errors } = await apiFunc();
    if (errors.length) {
      setState({ kind: 'fail', errors });
    } else {
      setState({ kind: 'success', data });
    }
  };
  return { state, request };
};

const Org = ({org}: {org: IOrg}) => {
  const [state, setState] = useState('not updated')
  return (
    <div>
      <div>{org.name}</div>
      <div>{state}</div>
      <button onClick={() => setState('updated')}>Update</button>
    </div>
  )
}

const Orgs = ({orgs}: {orgs: IOrg[]}) => {
  return (
    <div>
      {orgs.map(org => <Org key={org.name} org={org} />)}
    </div>
  )
}

const App = () => {
  const orgsApi = useApi<IOrg[]>(api.getOrgs);

  useEffect(() => {
    orgsApi.request()
  }, [])

  return (
    <div>
      <button onClick={orgsApi.request}>Get orgs</button>
      {orgsApi.state.kind === 'success' && <Orgs orgs={orgsApi.state.data} />}
    </div>
  )
};

render(<App />, document.getElementById('root'));

复现步骤

  • 运行代码后,点击若干Update按钮,此时Org组件的内部状态变为updated
  • 点击Get orgs按钮,会发现所有Org组件的内部状态被重置为not updated

需求

能否保留当前的仅合法API状态类型模式,同时让React正确调和组件而非重新创建?最好不引入辅助状态,仅使用useApi钩子返回的state。


解决方案

核心问题有两个:一是每次请求返回的data是全新引用,触发组件不必要重渲染;二是loading状态切换会导致Orgs组件卸载再挂载,直接重置内部状态。我们可以通过优化useApi钩子解决这两个问题,完全保留原有的类型模式。

优化后的useApi钩子

import React, { useState, useRef, useCallback } from 'react';

// 保留原有的Api接口定义
interface ApiInit {
  kind: 'init';
}
interface ApiSuccess<T> {
  kind: 'success';
  data: T;
}
interface ApiFail {
  kind: 'fail';
  errors: string[];
}
interface ApiLoading<T = unknown> {
  kind: 'loading';
  data?: T;
}
type ApiReturn<T> = ApiSuccess<T> | ApiFail | ApiLoading<T> | ApiInit

const useApi = <T,>(apiFunc: () => Promise<{ data: T; errors: string[] }>) => {
  const [state, setState] = useState<ApiReturn<T>>({ kind: 'init' });
  const prevSuccessDataRef = useRef<T | undefined>(undefined);

  // 根据业务选择比较方式,这里用JSON.stringify做简单深比较,复杂场景可替换为lodash.isEqual
  const isDataEqual = (a: T, b: T): boolean => {
    return JSON.stringify(a) === JSON.stringify(b);
  };

  const request = useCallback(async () => {
    // 更新loading状态时,保留之前的success数据,避免Orgs组件卸载
    setState(prev => ({
      kind: 'loading',
      ...(prev.kind === 'success' ? { data: prev.data } : {})
    } as ApiReturn<T>));

    try {
      const { data, errors } = await apiFunc();
      if (errors.length) {
        setState({ kind: 'fail', errors });
      } else {
        // 仅当数据内容真正变化时,才更新data引用
        if (!prevSuccessDataRef.current || !isDataEqual(data, prevSuccessDataRef.current)) {
          prevSuccessDataRef.current = data;
          setState({ kind: 'success', data });
        } else {
          // 数据无变化时,仅更新状态类型,保留原有data引用
          setState(prev => ({ ...prev, kind: 'success' } as ApiReturn<T>));
        }
      }
    } catch (err) {
      setState({ kind: 'fail', errors: [err instanceof Error ? err.message : 'Unknown error'] });
    }
  }, [apiFunc]);

  return { state, request };
};

// 保留原有的Org、Orgs、App组件代码不变

关键优化点

  1. 保留loading状态下的success数据:修改ApiLoading接口允许携带data属性,更新loading状态时继承之前的success数据,确保Orgs组件不会被卸载,从根源避免内部状态重置。
  2. 稳定data引用:用useRef缓存上一次的success数据,只有当新数据内容真正变化时才更新引用,避免不必要的组件重渲染。
  3. 完全兼容原有类型模式:仅扩展了ApiLoading的可选属性,ApiReturn类型的合法状态逻辑完全保留,无需修改业务组件的类型判断逻辑。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.21 14:36:53