useEffect 实践案例(一)

这波能反杀 发表于 1年以前  | 总阅读数:706 次

对于 useEffect 的掌握是 React hooks 学习的重中之重。因此我们还需要花一些篇幅继续围绕它讲解。

在上一篇文章中,我们使用两个案例分析了 useEffect 的理论知识。接下来,我们通过一些具体的实践案例来学习 useEffect 的运用

1 需求

现有一个简单的需求,要实现一个搜索框,输入内容之后,点击搜索按钮,然后得到一个列表。

当列表为空时,显示暂无数据

接口请求过程中,需要显示 Loading 状态

Loading 状态随便用的一个转圈图标来表示,和下面的图标有点重叠,以后有机会再调整一下 UI

接口请求成功之后,显示一个列表

再次搜索时,显示 Loading 状态

如果接口请求出错,显示错误页面

在实践中,这是针对一个请求所需要的常规状态处理,当然很多时候我们在学习的过程中简化了空数据/Loading/异常等状态,就导致了许多自学的朋友没有在工作中友好处理这些状态的习惯。

2 实现

我们一步一步来实现该需求

我们假设一个请求需要花费 600ms,在学习阶段,我们可以借助 Promise 与 setTimeout 来模拟一个接口请求

单独创建一个 api.ts 文件

在该文件中,我们声明一个名为 searchApi 的函数,该函数接收一个字符串作为参数

我计划设计该函数最终返回一个 Promise 对象。并将一个字符串数组 resolve 出来。该字符串由搜索条件的一个字符与Math.random 产生的随机数组成。

输出的列表长这样

该 api 函数具体代码如下:

// ./api.ts
export function searchApi(param: string) {
  return new Promise<string[]>((resolve, reject) => {
    const p = param.split('')
    const arr: string[] = []
    for(var i = 0; i < 10; i++) {
      const pindex = i % p.length
      arr.push(`${p[pindex] || '^ ^'} - ${Math.random()}`)
    }
    setTimeout(() => {
      if (Math.random() * 10 > 1) {
        resolve(arr)
      } else {
        reject('请求异常,请重新尝试!')
      }
    }, 600)
  })
}

在该函数中,我们使用泛型明确了 Promise 的输出类型,在后续的使用中就可以利用 TypeScript 的自动类型推导得到具体的返回类型

接下来我们要创建组件函数

// index.tsx
export default function DemoOneNormal() {
  // ...
}

然后我们根据 UI 的情况去分析应该在代码中设计哪些数据

首先有一个列表需要展示

const [list, setList] = useState<string[]>([])

然后有一个 Loading 的显示与隐藏需要控制

const [loading, setLoading] = useState(false)

还有一个错误信息需要显示

const [error, setError] = useState('')

还有一个稍微有一些特殊的,输入框中输入的内容。我们要注意准确分析内容:该内容的展示在已有的 UI 中,是根据键盘输入而展示内容,它不由数据来驱动

我们在该案例中,仅仅只是记录输入的内容,并传入 searchApi即可。因此我们可以使用 useRef 来存储该变量

const str = useRef('')

如果情况有变,有其他的 UI 需要该数据来驱动,那么我们就需要将其调整为使用 useState 来存储

接下来思考 JSX 代码的编写

首先是一个输入框 input 与按钮 button

<input 
  className={s.input} 
  placeholder="请输入您要搜索的内容" 
  onChange={(e) => str.current = e.target.value} 
/>
<Button 
  className={s.button} 
  onClick={onSure}
>
  搜索
</Button>

案例中的样式使用了 css module,因此 className 的语法会与前面介绍的有所不同,我们把 s.input 当成一个字符串来看待即可

代码中,借助 input 的 onChange 回调来记录当前输入的值

// const str = useRef('')
onChange={(e) => str.current = e.target.value} 

点击按钮时,修改对应的状态,并开始发送请求。此时 Loading 应该修改为 true

function onSure() {
  setLoading(true)
  searchApi(str.current).then(res => {
    setList(res)
    setLoading(false)
    setError('')
  }).catch(err => {
    setLoading(false)
    setError(err)
  })
}

请求成功之后,Loading 改回 false,list 得到新的数据。如果请求失败,Loading 依然需要改成 false,并记录错误信息

接下来我们要思考列表的 UI 代码。

首先,空数据、错误信息、正常列表的显示情况是互斥的,他们三个只能存在一个。Loading 状态是每个情况下都有可能发生的,与他们的关系是分别共存的

因此,当有错误信息时,这一块的内容应该为

if (error) {
  return (
    <div className={s.wrapper}>
      {loading && (
        <div className={s.loading_wrapper}>
          <Icon spin type='loading' style={{ fontSize: 40 }} />
        </div>
      )}
      <Icon type='event' color='red' style={{ fontSize: 32 }} />
      <div className={s.error}>{error}</div>
    </div>
  )
}

案例中出现的 Icon 组件是一个图标,该组件是我们这个项目自己封装好的基础组件

当是空列表时

if (list.length === 0) {
  return (
    <div className={s.wrapper}>
      {loading && (
        <div className={s.loading_wrapper}>
          <Icon spin type='loading' color='#2860Fa' style={{ fontSize: 38 }} />
        </div>
      )}
      <Icon type='event' color='#ccc' style={{ fontSize: 32 }} />
      <div className={s.nodata}>暂无数据</div>
    </div>
  )
}

正常列表有数据时

<div className={s.list}>
  {loading && (
    <div className={s.loading_wrapper}>
      <Icon spin type='loading' color='#2860Fa' style={{ fontSize: 38 }} />
    </div>
  )}

  {list.map(item => (
    <div key={item} className={s.item}>{item}</div>
  ))}
</div>

OK,此时所有的逻辑已经考虑完毕

3 优化封装

我们会发现,列表相关的逻辑实在是有点繁琐。如果每次遇到一个列表就要处理这么多,岂不是非常消耗时间?

因此我们这里考虑将这些逻辑统一封装到 List 组件里,下次要使用直接拿出来用就可以了

// ./List/index.tsx
export default function List(props) {}

在封装时,我们首先要考虑哪些属性需要作为 props 传入该 List 组件。关于封装的思考,和其他的逻辑封装是一样的,我们需要先考虑在不同的场景之下,他们的共性与差异分别是什么,差异的部分作为参数传入

三个数据,error,loading,list 都是差异部分,他们需要作为 props 传入

先定义一个类型声明如下

interface ListProps<T> {
  loading?: boolean,
  error?: string,
  list?: T[]
}

此时我们看到由于 list 的每一项具体数据内容,可能每一个列表都不一样,我们无法在这里确认他的类型,因此此处使用泛型来表示

不知道 list 的每一项具体数据是什么,也就意味着对应的 UI 我们也无法提前得知,只有在使用时才知道,因此还应该补上一个新的 props 属性

interface ListProps<T> {
  loading?: boolean,
  error?: string,
  list?: T[],
+ renderItem: (item: T) => ReactNode
}

然后我们只需要把差异部分与共同部分在组件逻辑中组合起来即可,List 组件完整代码如下

import Icon from 'components/Icon'
import { ReactNode } from 'react'
import s from './index.module.scss'

interface ListProps<T> {
  loading?: boolean,
  error?: string,
  list?: T[],
  renderItem: (item: T) => ReactNode
}

export default function List<T>(props: ListProps<T>) {
  const {list = [], loading, error, renderItem} = props

  if (error) {
    return (
      <div className={s.wrapper}>
        {loading && (
          <div className={s.loading_wrapper}>
            <Icon spin type='loading' style={{ fontSize: 40 }} />
          </div>
        )}
        <Icon type='event' color='red' style={{ fontSize: 32 }} />
        <div className={s.error}>{error}</div>
      </div>
    )
  }

  if (list.length === 0) {
    return (
      <div className={s.wrapper}>
        {loading && (
          <div className={s.loading_wrapper}>
            <Icon spin type='loading' color='#2860Fa' style={{ fontSize: 38 }} />
          </div>
        )}
        <Icon type='event' color='#ccc' style={{ fontSize: 32 }} />
        <div className={s.nodata}>暂无数据</div>
      </div>
    )
  }

  return (
    <div className={s.list}>
      {loading && (
        <div className={s.loading_wrapper}>
          <Icon spin type='loading' color='#2860Fa' style={{ fontSize: 38 }} />
        </div>
      )}

      {list.map(renderItem)}
    </div>
  )
}

封装好之后,使用起来就非常简单了,我们只需要把当前上下文中的数据传入进去即可。

<List 
  list={list} 
  loading={loading}  
  error={error}
  renderItem={(item) => (
    <div key={item} className={s.item}>{item}</div>
  )}
/>

该案例组件文件路径:src/pages/demos/effect/search/Normal.tsx

4 需求改进

在某些场景,初始化时我们并不需要展示空数组,而是需要请求一次接口,然后展示对应的列表,因此,在这种需求的情况下,代码需要进行一些调整

首先,Loading 的初始化状态需要从 false 改为 true,表示一开始就会立即请求数据

- const [loading, setLoading] = useState(false)
+ const [loading, setLoading] = useState(true)

然后初始化请求数据的操作,在 useEffect 中完成,传入空数组作为依赖项,表示只在组件首次渲染完成之后执行一次

... 

+ useEffect(() => {
+   searchApi(str.current).then(res => {
+     setList(res)
+     setLoading(false)
+     setError('')
+   }).catch(err => {
+     setLoading(false)
+     setError(err)
+   })
+ }, [])

function onSure() {
  setLoading(true)
  searchApi(str.current).then(res => {
    setList(res)
    setLoading(false)
    setError('')
  }).catch(err => {
    setLoading(false)
    setError(err)
  })
}

...

OK,这样需求就完整的被解决,不过此时我们发现,useEffect 的逻辑与 onSure 的逻辑高度重合,他们一个代表初始化逻辑,一个代表更新逻辑。

因此在代码上做一些简单的调整

function getList() {
    searchApi(str.current).then(res => {
      setList(res)
      setLoading(false)
      setError('')
    }).catch(err => {
      setLoading(false)
      setError(err)
    })
  }

  useEffect(() => {
    getList()
  }, [])

  function onSure() {
    setLoading(true)
    getList()
  }

这样调整了之后,我们发现一个有趣的事情,当点击搜索按钮触发 onSure 时,我们会执行一次把 loading 修改为 true 的操作

setLoading(true)

那如果这个时候,我们就可以把 loading 作为 useEffect 的依赖项传入,onSure 里就可以只保留这一行代码

useEffect(() => {
  loading && getList()
}, [loading])

function onSure() {
  setLoading(true)
}

这就是我们在本书唯一付费章节「React 哲学」中提到的开关思维。在日常生活中,如果我想要打开电视机,我们只需要关注开关按钮那一下操作,在这里也是一样,如果我想要重新请求列表搜索,我只需要关注如何操作 loading 这个开关即可

该案例组件文件路径:src/pages/demos/effect/search/Normal2.tsx

接下来我们将要学习自定义 hook,进一步感受开关思维的魅力。

本文由微信公众号这波能反杀原创,哈喽比特收录。
文章来源:https://mp.weixin.qq.com/s/nR9zczAZ5WZb1oG7w5cJDA

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
为Electron程序添加运行时日志 5年以前  |  20366次阅读
Node.js下通过配置host访问URL 5年以前  |  5894次阅读
用 esbuild 让你的构建压缩性能翻倍 4年以前  |  5792次阅读
 目录