React Hook 不完全指南

前言

本文内容大部分参考了
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 来实现对应的功能,可以节省大部分的代码

《React 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. 使用规则

  1. 只在最顶层使用 Hook:不要在循环,条件或嵌套函数中调用 Hook, 确保总是在你的 React 函数的最顶层调用他们。
  2. 不要在普通的 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 (
    <div>
      <h3>Counter with Hook</h3>
      <p>You click {count} times</p>
      <button onClick={e => setCount(count => count + 1)}>Click me</button>
      <p>
        <input placeholder="输入姓名" onChange={e => setName(e.target.value)} />
        <br />
        your name is {name}
      </p>
    </div>
  )
}

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 (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
      <button onClick={handleAlertClick}>Show alert</button>
    </div>
  )
}

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 的方式来实现表格的分页,数据请求操作,

《React Hook 不完全指南》

跑马灯中奖

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

《React Hook 不完全指南》

参考资料

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

    原文作者:离尘不理人
    原文地址: https://segmentfault.com/a/1190000019223106
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞