原文地址
我们在实际的业务场景下,遇到一个需求:
对于一些加载比较慢的资源,组件最初展示标准的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 ==> useStateconst [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,而不是loadingPropNameconst isDelayed = useDelayHint(props.isLoading, 2000);return <PureDisplay {
...props} isDelayed={
isDelayed} />;
};
可以看到,原本用于HOC的PureDisplay组件在此处还能继续用,这让HOC迁移到Hook的成本非常的小。