原文地址
我们在实际的业务场景下,遇到一个需求:
对于一些加载比较慢的资源,组件最初展示标准的Loading效果,但在一定时间(比如2秒)后,变为“资源较大,正在积极加载,请稍候”这样的友好提示,资源加载完毕后再展示具体内容。
对于一个展示的组件来说,我们希望的逻辑就是这样的:
const PureDisplay = ({isLoading, isDelayed, data}) => {
if (isDelayed) {
return <div>'Please wait a little more...'</div>;
}
if (isLoading) {
return <div>'Loading...'</div>;
}
return <div>{data}</div>;
};
通过isDelayed和isLoading这2个属性来表达3种状态(初始加载中、加载用时过长、已经加载完毕),使用条件分支展示不同的内容。
在以往,我们很容易判断出来isDelayed的获取可以通过HOC来实现,因此我们也判断它可以用Hook来实现复用。但是在面对如何实现这个Hook的时候,出现了不小的疑惑,甚至无从下手。
最后在一翻讨论后,我们发现一种方法,即先实现一个HOC版本,再“翻译”成对应的Hook,能快速完成对应的代码。
HOC版
假设我们需要实现一个withDelayHint的HOC来实现这一逻辑,简单整理了一下它的功能:
- 知道组件当前是否在loading状态,如果不在的话就不用开定时器了。
- 如果处在loading状态,则打开一个定时器,指定时间后将isDelayed由false改为true。
- 如果loading状态发生了变化,则需要停掉定时器,并回到第1步重新判断是不是要开新的定时,用于组件状态更新的场合。
基于上面的整理,HOC需要至少2个参数:
- 如何获取loading状态。最简单地方法是提供一个属性的名称,直接从props[loadingPropName]拿,函数化一些可以提供一个函数来通过getLoading(props)获取。
- 定时器的延迟时长,以毫秒为单位。
因此,它的实现还是相对简单的:
import React from 'react'
const pureDisplay=({isLoading,isDelady,data})=>{
if(isDelady){
return <div>'Please wait a little more ...'</div>
}
if(isLoading){
return <div>'Loading'</div>
}
return <div>数据:{data}</div>
}
const withDelayHint=(loadingName,delay)=>ComponentIn=>{
const ComponentOut=class extends React.Component{
state={
timer:null,
isDelady:false
}
tryStartTimer=()=>{
this.setState({isDelady:false})
//处于loading状态时 则开启定时器
if(this.props[loadingName]){
const timer=setTimeout(()=>{
this.setState({isDelady:true})
},delay)
this.setState({timer})
}
}
componentDidMount(){
this.tryStartTimer()
}
componentDidUpdate(prevProps){
//如果数据回来后 则取消定时器
if(prevProps[loadingName]!==this.props[loadingName]){
clearTimeout(this.state.timer);
this.tryStartTimer()
}
}
componentWillUnmount(){
clearTimeout(this.state.timer);
}
render(){
const {isDelady}=this.state;
console.log('props',this.props);
return <ComponentIn {...this.props} isDelady={isDelady}></ComponentIn>
}
}
return ComponentOut;
}
//在使用上,将组件用HOC包装一次,即可以拿到isDelayed属性:
const DisplayWithDelay=withDelayHint('isLoading',2000)(pureDisplay);
export default DisplayWithDelay
//使用
<DisplayWithDelay isLoading={true} />
Hook版
在React官方提供的hook中,与组件中各种逻辑都有对应的版本,比如:
- setState对应useState。
- 生命周期对应useEffect。
因此,我们把上面的代码一一通过映射来实现。需要注意的是,因为hook本身并不是组件的实现,所以是获取不到props的,因此hook不会有“从props中获取isLoading”这个逻辑,而是直接接收isLoading的值就行:
import React, { useRef, useEffect, useState } from 'react'
const PureDisplay = ({ isLoading, isDelady, data }) => {
if (isDelady) {
return <div>'Please wait a little more ...'</div>
}
if (isLoading) {
return <div>'Loading'</div>
}
return <div>数据:{data}</div>
}
const useDelayHint = (loading, delay) => {
//和 render无关的属性可以用 useRef 保存
const timer = useRef(null);
// setState ==> useState
const [delayed, setDelayed] = useState(false);
//生命周期
useEffect(
() => {
const timer=loading?setTimeout(()=>setDelayed(true),delay):setDelayed(false);
//清除逻辑
return ()=>clearTimeout(timer);
},
//componentDidUpdate
[loading]
);
return delayed;
}
使用
const HookDisplay = props => {
// 这里直接给isLoading,而不是loadingPropName
const isDelayed = useDelayHint(props.isLoading, 2000);
return <PureDisplay {...props} isDelayed={isDelayed} />;
};
可以看到,原本用于HOC的PureDisplay组件在此处还能继续用,这让HOC迁移到Hook的成本非常的小。