仅合法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组件代码不变
关键优化点
- 保留loading状态下的success数据:修改
ApiLoading接口允许携带data属性,更新loading状态时继承之前的success数据,确保Orgs组件不会被卸载,从根源避免内部状态重置。 - 稳定data引用:用
useRef缓存上一次的success数据,只有当新数据内容真正变化时才更新引用,避免不必要的组件重渲染。 - 完全兼容原有类型模式:仅扩展了
ApiLoading的可选属性,ApiReturn类型的合法状态逻辑完全保留,无需修改业务组件的类型判断逻辑。
内容的提问来源于stack exchange,提问作者barciewicz
相关产品推荐
相关产品推荐

