useEffect 实践案例(一)

简介: useEffect 实践案例(一)


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


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


1.需求


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


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

image.png

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

image.png

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


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

image.png

再次搜索时,显示 Loading 状态

image.png

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

image.png

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


2.实现


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


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


单独创建一个 api.ts 文件


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


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


输出的列表长这样

image.png

该 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 的自动类型推导得到具体的返回类型

image.png

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

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


5.推荐阅读


React 哲学

相关文章
|
1月前
|
JavaScript 前端开发
Vue开发必备:$nextTick方法的理解与实战场景
Vue开发必备:$nextTick方法的理解与实战场景
124 1
|
2月前
|
开发框架 JavaScript 前端开发
手把手教你剖析vue响应式原理,监听数据不再迷茫
该文章深入剖析了Vue.js的响应式原理,特别是如何利用`Object.defineProperty()`来实现数据变化的监听,并探讨了其在异步接口数据处理中的应用。
|
6月前
|
前端开发 JavaScript
重点来了,useEffect
重点来了,useEffect
|
6月前
|
前端开发 API 数据处理
useEffect 实践案例(2):自定义 hook
useEffect 实践案例(2):自定义 hook
|
6月前
|
前端开发 JavaScript 定位技术
蜕变之始,useEffect 最后一种用法
蜕变之始,useEffect 最后一种用法
|
6月前
|
前端开发 JavaScript 索引
【源码&库】Vue3 中的 nextTick 魔法背后的原理
【源码&库】Vue3 中的 nextTick 魔法背后的原理
152 0
|
6月前
【Vue2.0学习】—Todolist案例自定义事件(六十)
【Vue2.0学习】—Todolist案例自定义事件(六十)
|
前端开发
前端学习案例5-react中的生命周期
前端学习案例5-react中的生命周期
73 0
前端学习案例5-react中的生命周期
|
JavaScript 前端开发
nextTick在项目中的实践
在项目中经常需要在视图层立即显示数据,而有时候由于异步数据传递的原因,在页面上并不会立即显示页面,这时候就需要使用Vue提供的nextTick这个方法,本篇介绍nextTick的原理解析
89 0
|
前端开发 JavaScript
react的基础学习和案例
react的基础学习和案例
121 0