Child render: 1
getChildData call: 1
{ type: 'GET_DATA' }
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
in Child (created by App)
75 | useEffect(() => {
76 | apis.getChildData().then((res) => {
> 77 | setData(res);
| ^
78 | });
79 | }, []);
80 | return { data };
{ type: 'GET_DATA_FULFILLED', payload: 'fake data' }
Child render: 2
getChildData call: 2
Child render: 3
在日常开发中,视图的一个经典展示逻辑是,API数据加载中时,展示"加载中..."提示,API返回正确数据时,展示依赖此数据的组件,API返回异常数据,展示"加载出错"提示。那么势必要根据一个视图状态模型来判断当前API的数据状态,很多人简单粗暴直接用
isLoading: boolean
来表示,那么来看下这样表示会出现什么问题。(代码以React视图框架为例)代码功能很简单,
apis.getData()
用来模拟后端接口服务。为了简化代码,不用redux
等状态管理库,使用useReducer
hook来演示,dataQueryReducer
函数来处理dispatch
的action
,state
的初始化状态为{ isLoading: false, data: null }
。当获取到API数据后渲染Child
组件,当数据还在加载中,渲染加载中视图<p>loading...</p>
。一切看起来都是这么自然,那么来看实际运行结果,我在几个关键处打印了日志,日志如下:可以看到,
Child
组件在获取数据之前就先渲染了一次,这不是我们期望的,严重的还会导致memory leak,后面会说到。有人说,那么把isLoading
的初始化状态改为true
就好了,嗯,3处代码关键改动:看下运行日志:
针对这个例子,问题是解决了,但又会出现几个问题:
isLoading
的初始化状态是true
的前提是组件在mount时调用了API,apis.getData()
是在useEffect(() => {}, [])
hook的effect函数中调用,你需要知道API的调用时机来决定isLoading
的初始化状态,耦合。isLoading
的初始化状态应该是false
,又会出现第一次出现的问题,即Child
组件在dispatch
GET_DATA
action之前渲染了一次。来看第2点响应用户事件调用API的代码:
当点击button按钮时,运行日志如下:
至此,主要的问题已经说明,解决方法,使用以下四种状态来表达视图状态模型的值,TS类型如下:
将
isLoading
字段改为status
,变量命名更加准确,上述四种状态分别表示:'idle'
:初始化状态'pending'
: API调用正在进行中,网络请求正在pending
状态'fulfilled'
: API调用已返回,系统正常,业务正常的值'rejected'
: API调用已返回,系统异常或业务异常的值来看改造后的代码
情景一:当组件mount时调用API
运行日志:
Child
组件尽在API返回正确数据时渲染,即data.status === 'fulfilled'
时,符合预期情景二:响应用户事件调用API
关键代码如下:
当点击button按钮时,运行日志:
同样符合预期。
再来看最开始提到的memory leak问题,使用boolean值作为视图状态模型的值,会导致
Child
组件它依赖的API调用之前就会被渲染一次,从而导致memory leak. 这次需要对Child
组件的代码进行改动:某天,
Child
组件不再是展示型组件(presentational component),或者说无状态组件(stateless component),它变成了容器型组件(container component),或者说有状态组件(stateful component)。从代码看到,Child
组件内部使用了自定义hook -useGetChildDataQuery
,用来封装组件生命周期及API调用逻辑。运行日志:
warning产生的原因:由于
data.isLoading
的初始化值是false
,Child
组件会被渲染,useGetChildDataQuery
hook会执行,apis.getChildData()
会被调用,在apis.getChildData()
返回之前,{ type: 'GET_DATA' }
action被dispatch,此时isLoading
在dataQueryReducer
被设置为true
,展示<p>loading...</p>
视图,而Child
组件实例被unmount,等到apis.getChildData()
返回,此时Child
已经被unmount,setData
无法更新被unmount组件的state
。除了warning,初始化时,由于
Child
组件被渲染,useGetChildDataQuery
hook中的apis.getChildData()
被调用,将多发送一次HTTP request。最终使用修正后的代码如下:
运行日志: