资讯专栏INFORMATION COLUMN

React Hook 不完全指南

Lin_R / 2724人阅读

摘要:使用完成副作用操作,赋值给的函数会在组件渲染到屏幕之后。如此很容易产生,并且导致逻辑不一致。同时,这也是很多人将与状态管理库结合使用的原因之一。当我们通过的第二个数组类型参数,指明当前的依赖,就能避免不相关的执行开销了。

前言
本文内容大部分参考了 overreacted.io 博客一文,同时结合 React Hook 官方 文章,整理并归纳一些笔记和输出个人的一些理解
什么是 Hook ?
官方介绍:Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
React 中内置的 Hook API

基础 Hook

useState

// 传入初始值,作为 state
const [state, setState] = useState(initialState)

//  `惰性初始 state`;传入函数,由函数计算出的值作为 state
// 此函数只在初始渲染时被调用
const [state, setState] = useState(() => {
  const initialState = someExpensiveComputation(props)
  return initialState
})

useEffect

该 Hook 接收一个包含命令式、且可能有副作用代码的函数.

在函数组件主体内(这里指在 React 渲染阶段)改变 DOM、添加订阅、设置定时器、记录日志以及执行其他包含副作用的操作都是不被允许的,因为这可能会产生莫名其妙的 bug 并破坏 UI 的一致性。

使用 useEffect 完成副作用操作,赋值给 useEffect 的函数会在组件渲染到屏幕之后。你可以把 effect 看作从 React 的纯函数式世界通往命令式世界的逃生通道。

默认情况下,effect 将在每轮渲染结束后执行,但你可以选择让它 在只有某些值改变的时候才执行。详情见后面。

清除 effect
通常,组件卸载时需要清除 effect 创建的诸如订阅或计时器 ID 等资源。要实现这一点,useEffect 函数需返回一个清除函数。以下就是一个创建订阅的例子:

useEffect(() => {
  const subscription = props.source.subscribe()
  return () => {
    // 清除订阅
    subscription.unsubscribe()
  }
}, [依赖])

useContext

额外的 Hook

useReducer

useCallback

useMemo

useRef

useImperativeHandle

useLayoutEffect

useDebugValue

我们为什么选择使用 Hook ? 1. 在组件之间复用状态逻辑很难
React 没有提供将可复用性行为“附加”到组件的途径(例如,把组件连接到 store)。如果你使用过 React 一段时间,你也许会熟悉一些解决此类问题的方案,比如 render props 和 高阶组件。但是这类方案需要重新组织你的组件结构,这可能会很麻烦,使你的代码难以理解。如果你在 React DevTools 中观察过 React 应用,你会发现由 providers,consumers,高阶组件,render props 等其他抽象层组成的组件会形成“嵌套地狱”。尽管我们可以在 DevTools 过滤掉它们,但这说明了一个更深层次的问题:React 需要为共享状态逻辑提供更好的原生途径。

你可以使用 Hook 从组件中提取状态逻辑,使得这些逻辑可以多带带测试并复用。Hook 使你在无需修改组件结构的情况下复用状态逻辑。 这使得在组件间或社区内共享 Hook 变得更便捷。

2. 复杂组件变得难以理解
我们经常维护一些组件,组件起初很简单,但是逐渐会被状态逻辑和副作用充斥。每个生命周期常常包含一些不相关的逻辑。例如,组件常常在 componentDidMount 和 componentDidUpdate 中获取数据。但是,同一个 componentDidMount 中可能也包含很多其它的逻辑,如设置事件监听,而之后需在 componentWillUnmount 中清除。相互关联且需要对照修改的代码被进行了拆分,而完全不相关的代码却在同一个方法中组合在一起。如此很容易产生 bug,并且导致逻辑不一致。

在多数情况下,不可能将组件拆分为更小的粒度,因为状态逻辑无处不在。这也给测试带来了一定挑战。同时,这也是很多人将 React 与状态管理库结合使用的原因之一。但是,这往往会引入了很多抽象概念,需要你在不同的文件之间来回切换,使得复用变得更加困难。

为了解决这个问题,Hook 将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据),而并非强制按照生命周期划分。你还可以使用 reducer 来管理组件的内部状态,使其更加可预测。

3. 用更少的代码,实现同样的效果

下面的代码可以直观的体现出来,在某些场景下,使用 hook 来实现对应的功能,可以节省大部分的代码

3.1 清除副作用更加紧凑

对比 Class 组件来说,清除副作用要简单的多,如下代码,在 useEffect hook 里面返回一个函数,当我们的函数组件卸载的时候,就会自动执行这个函数,从而来清除副作用。想想我们在 Class 组件里面需要在 componentWillUnmount 生命周期里面去编写对应的代码。

对比两者我们发现,使用 useEffect 的方式,能够将挂载和卸载的逻辑更加紧密的耦合在一起,从而减少 BUG 的发生

useEffect(() => {
  const id = setInterval(() => {
    setCount(count => count + 1)
  }, 1000)
  return () => clearInterval(id)
}, [])

// 比如给 windows 挂载监听函数
useEffect(() => {
  window.addEventListener("reszie", handleRezie)

  return () => {
    window.removeEventListener("resize", handleRezie)
  }
}, [])
如何正确的使用 Hook ? 1. 使用规则

只在最顶层使用 Hook:不要在循环,条件或嵌套函数中调用 Hook, 确保总是在你的 React 函数的最顶层调用他们。

不要在普通的 JavaScript 函数中调用 Hook。你可以

[x] 在 React 的函数组件中调用 Hook

[x] 在自定义 Hook 中调用其他 Hook

2. 只有在自己依赖更新时才执行 effect

使用 useEffect 完成副作用操作,赋值给 useEffect 的函数会在组件渲染到屏幕之后;牢记这句话。

仔细观察如下代码,当函数组件里面,有多个 effect 的时候,默认的 effect 将在每次 UI render 之后被调用。当我们通过 useEffect 的第二个数组类型参数,指明当前 effect 的依赖,就能避免不相关的执行开销了。

通过启用 eslint-plugin-react-hooks 插件,来强制提醒我们在使用 effect 的时候,申明所需要的依赖

{
  "plugins": [
    // ...
    "react-hooks"
  ],
  "rules": {
    // ...
    "react-hooks/rules-of-hooks": "error",
    "react-hooks/exhaustive-deps": "warn"
  }
}
const CounterHook = () => {
  const [count, setCount] = useState(0)
  const [name, setName] = useState("heaven")

  useEffect(() => {
    document.title = `counterWithHook ${count}`
  }, [count])

  useEffect(() => {
    console.log("you name is", name)
  }, [name])

  return (
    

Counter with Hook

You click {count} times

setName(e.target.value)} />
your name is {name}

) }
2.1 不要忘记函数依赖

对于 useEffect 内部方法,一旦引用外部的函数,那么这个时候需要注意了:
需要把 useEffect 内部引用到的方式,声明为当前 effect 的依赖
在下图的代码中,我们可以看到,在 effect 函数内部,引入外部的函数,我们的 eslint-plugin-react-hooks 插件会自动提示我们需要把对应的函数作为依赖添加进去

不规范示例:这里在安装了插件的情况下,会自动提示我们将 fetchData 函数移入 effect 内部

const getFetchUrl = () => {
  return `https://hn.algolia.com/api/v1/search?query=${query}`
}

const fetchData = async () => {
  return axios.get(getFetchUrl())
}

useEffect(() => {
  fetchData().then(resp => {
    console.log(resp)
    setData(resp.data)
  })
}, [])

正确的写法:

useEffect(() => {
  const getFetchUrl = () => {
    return `https://hn.algolia.com/api/v1/search?query=${query}`
  }

  const fetchData = async () => {
    return axios.get(getFetchUrl())
  }

  fetchData().then(resp => {
    console.log(resp)
    setData(resp.data)
  })
}, [query])
3、理解每一次的 Rendering
每一次渲染都有它自己的 Props and State
每一次渲染都有它自己的事件处理函数
每次渲染都有它自己的 Effects

运行如下代码之后,在我们点击 Show alert 按钮之后,然后点击 Click me 按钮,alert 输出的永远是在点击的那个时刻的 count;

换句话来说;在 hooks 组件里面,每一次的渲染,都相当于记录当前次的『快照』

import React, { useEffect, useState } from "react"
const Counter = () => {
  const [count, setCount] = useState(0)

  const handleAlertClick = () => {
    setTimeout(() => {
      alert(`Yout clicked me: ${count}`)
    }, 3000)
  }

  useEffect(() => {
    setTimeout(() => {
      console.log(`Yout clicked ${count} times`)
    }, 3000)
  })

  return (
    

You clicked {count} times

) } export default Counter
使用自定义 Hook
通过自定义 Hook,可以将组件逻辑提取到可重用的函数中。

当我们想在两个函数之间共享逻辑时,我们会把它提取到第三个函数中。而组件和 Hook 都是函数,所以也同样适用这种方式。

自定义 Hook 是一个函数,其名称以 “use” 开头,函数内部可以调用其他的 Hook。

自定义 useService hook

useService.js 自定义的一个 server hook,该 hook 封装了 ajax 请求中的 { loading, error, response } 三个基础逻辑;有了这个 hook 我们就能很轻松的在每次网络请求里面去处理各种异常逻辑了;详细用法看文章最后的 Table 分页操作实例

import { useEffect, useRef, useState, useCallback } from "react"
import { isEqual } from "lodash"

const useService = (service, params) => {
  const prevParams = useRef(null)
  const [callback, { loading, error, response }] = useServiceCallback(service)

  useEffect(() => {
    if (!isEqual(prevParams.current, params)) {
      prevParams.current = params
      callback(params)
    }
  })

  return { loading, error, response }
}

const useServiceCallback = service => {
  const [loading, setLoading] = useState(false)
  const [error, setError] = useState(null)
  const [response, setResponse] = useState(null)

  // 使用 useCallback,来判断 service 是否改变
  const callback = useCallback(
    params => {
      setLoading(true)
      setError(null)
      service(params)
        .then(response => {
          console.log(response)
          setLoading(false)
          setResponse(response)
        })
        .catch(error => {
          setLoading(false)
          setError(error)
        })
    },
    [service]
  )

  return [callback, { loading, error, response }]
}
实例剖析 Table 分页操作

如下代码,使用 hook 的方式来实现表格的分页,数据请求操作,

跑马灯中奖

使用 hook 实现一个简易版的跑马灯抽奖逻辑

参考资料

官方 Hook 介绍
Hook 规则
Hook API 索引
如何在 Hook 中发起请求
useEffect 详解

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/109624.html

相关文章

  • React Hook起飞指南

    摘要:起飞指南作者元潇方凳雅集出品目前放出来了个内置,但仅仅基于以下两个,就能做很多事情。行代码实现一个全局元潇根组件挂上即可子组件调用随时随地实现一个局部元潇的本质是的一个语法糖,感兴趣可以阅读一下的类型定义和实现。 React Hook起飞指南 作者:元潇 方凳雅集出品 16.8目前放出来了10个内置hook,但仅仅基于以下两个API,就能做很多事情。所以这篇文章不会讲很多API,...

    姘搁『 评论0 收藏0
  • 理解 React Hooks 的 Capture Value 特性

    摘要:在读了一些文章后,大致是找到自己总是掉坑的原因了没理解中的特性。通过这个示例,相信会比较容易地理解特性,并如何使用来暂时绕过它。在知道并理解这个特性后,有助于进一步熟悉了的运行机制,减少掉坑的次数。 由于刚使用 React hooks 不久,对它的脾气还拿捏不准,掉了很多次坑;这里的 坑 的意思并不是说 React hooks 的设计有问题,而是我在使用的时候,因为还没有跟上它的理念导...

    curlyCheng 评论0 收藏0
  • React教程:组件,Hooks和性能

    摘要:顾名思义,受控组件的值由控制,能为与用户交互的元素提供值,而不受控制的元素不获取值属性。另外我发现受控组件更容易理解和于使用。只是一种把组件作为参数的函数,并且与没有包装器的组件相比,能够返回具有扩展功能的新组件。其中三个基本的是,和。 翻译:疯狂的技术宅原文:https://www.toptal.com/react/... 本文首发微信公众号:jingchengyideng欢迎关...

    edagarli 评论0 收藏0
  • react hook实用的用法和技巧分析

    摘要:以我个人的观点,要不要使用呢建议用的的人项目版本已经是了新建的项目一直对新技术保持关注,跃跃欲试的人对函数式编程爱好的人对的,,厌烦,甚至因为重新渲染整天在头疼的人不建议用的人对极其厌恶,对生命周期编程是非爱好的人。 react hook发布也已经有几个月了,相信有部分人已经开始使用了,还有些人在犹豫要不要用,可能更多人安于现状,没有要用的打算,甚至还有很多公司的react版本是15或...

    CntChen 评论0 收藏0
  • 10分钟了解react新特性hooks

    摘要:简介是的新增特性。我们统一把这些操作称为副作用,或者简称为作用。由于副作用函数是在组件内声明的,所以它们可以访问到组件的和。副作用函数还可以通过返回一个函数来指定如何清除副作用。目前为止,有两种主流方案来解决这个问题高阶组件和。 Hook 简介 Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。 us...

    hlcfan 评论0 收藏0

发表评论

0条评论

Lin_R

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<