useEffect 实践案例之一

开发 前端
案例中的样式使用了CSS Module,因此 ClassName 的语法会与前面介绍的有所不同,我们把 S.input 当成一个字符串来看待即可。

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

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

一、需求

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

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

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

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

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

再次搜索时,显示 Loading 状态。

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

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

二、实现

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

我们假设一个请求需要花费 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="请输入您要搜索的内容" 
  notallow={(e) => str.current = e.target.value} 
/>
<Button 
  className={s.button} 
  onClick={onSure}
>
  搜索
</Button>

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

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

// const str = useRef('')
notallow={(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,此时所有的逻辑已经考虑完毕。

三、优化封装

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

因此我们这里考虑将这些逻辑统一封装到 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

四、需求改进

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

首先,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,进一步感受开关思维的魅力。

责任编辑:姜华 来源: 这波能反杀
相关推荐

2023-12-21 09:00:21

函数React 组件useEffect

2022-05-07 15:44:45

eTS 开发鸿蒙

2024-01-02 07:56:13

ReactuseEffect数据驱动 UI

2023-12-12 09:43:17

桌面开发Net消息机制

2023-12-22 08:46:15

useEffectVueMobx

2019-08-09 10:58:48

2011-01-25 10:51:54

系统架构设计师

2019-07-17 15:05:35

应用服务器Tomcat监控

2021-06-03 19:55:55

MySQ查询优化

2022-02-16 15:39:30

ACTS应用XTS子系统鸿蒙

2012-06-25 14:01:10

云计算案例

2021-02-04 15:08:37

Vue渐进式框架

2021-05-06 05:39:30

Inotify监听系统

2018-05-21 10:40:46

Redis集群实践

2011-04-25 11:18:39

Ajax

2021-04-21 21:09:36

缓存系统高可用

2021-11-10 10:48:36

C++函数指针

2021-02-18 22:21:20

ASM服务组件化

2021-11-30 07:02:10

虚拟化Linux 中断

2021-10-09 19:05:06

channelGo原理
点赞
收藏

51CTO技术栈公众号