React Hooks 食用指南,包会,通俗易懂!

前言

自 React 16.8 版本问世以来,我们便可以在 React 中书写函数式组件,引入 Hook 来渐进式地代替之前的类式组件。如今,React 已发布至 18.2 版本,已经全面拥抱 Hook,你可以放心的去写 Hook,而不用去考虑复杂的类式组件。Hook 的出现显然降低了我们使用 React 开发 Web 应用的心智负担。

本文只列举我们在开发时常用的一些 Hook,更多 Hook 的用法请参考 React 官方文档进行查看和学习。

Hook 简介

Hook 是什么

Hook 是 React 16.8 的新增特性,是一个特殊的函数,它可以让你钩入 React 的特性,让你在不编写 class 组件的情况下使用 state 以及其他的 React 特性。

Hook 不包含任何破坏性改动,100% 向后兼容,Hook是完全可选的,你无需重写任何已有代码就可以在一些组件中尝试 Hook。Hook 不会影响你对 React 概念的理解。恰恰相反,Hook 为已知的 React 概念提供了更直接的 API,如:props, state,context,refs 以及生命周期。

React 官方表明,没有计划从 React 中移除 class,因为在 Facebook(Meta),有成千上万的组件用 class 书写,完全没有重写它们的计划。因此你还可以在 React 中书写类式组件,而采用渐进式的方式书写Hook。

为什么需要 Hook

class 组件的优势

我们在 React 中书写 class 组件可以定义自己的 state,用来保存组件自己内部的状态,而函数式组件不可以,因为函数每次调用都会产生新的临时变量。

此外 class 组件有自己的生命周期,我们可以在对应的生命周期中完成自己的逻辑,比如在 componentDidMount 生命周期中发送网络请求,并且该生命周期函数只会执行一次,在学习 hooks 之前,如果在函数式组件中发送网络请求,意味着每次重新渲染都会重新发送一次网络请求。

值得强调的是,class 组件可以在状态改变时只重新执行 render 函数,以及我们希望重新调用的生命周期函数componentDidUpdate 等,但是函数式组件在重新渲染时,整个函数都会被执行,似乎没有什么地方可以只让它们调用一次。

class 组件的劣势

1. 在组件之间复用状态逻辑很难

因为 React 没有提供将可复用性行为"附加"到组件的途径,导致我们有时需要书写高阶组件来实现一些状态的复用,像 redux 中 connect 或者 react-router 中的 withRouter,这些高阶组件就是为了状态的复用而设计的。又比如我们平时使用 Provider、 Consumer 来共享一些状态,由 providers,consumers,高阶组件,render props 等其他抽象层组成的组件会形成嵌套地狱,这些代码让我们无论是从编写、阅读还是从设计上来说,都变得非常困难。

2. 复杂组件变得难以理解

我们最初在编写一个 class 组件时,往往逻辑比较简单,并不会非常复杂。但是随着业务的增多,我们的 class 组件会变得越来越复杂,每个生命周期常常包含一些不相关的逻辑,比如在 componentDidMount 生命周期中,可能就会包含很多其他的逻辑代码,如网络请求、一些事件的监听,之后还需要在 componentWillUnmount 中消除事件。对于这样的 class 组件来说实际上非常难以拆分,因为它们的逻辑往往混在一起, 强行拆分反而会造成过度设计, 增加代码的复杂度。

在多数情况下,我们不可能把组件拆分为更小的粒度,因为状态逻辑无处不在,这也是导致我们在开发时往往将 React 与状态管理库结合在一起使用的原因。Hook 将组件中相互关联的部分拆分成更小的函数,从而可以更灵活的使用,而并非强制按照生命周期去划分。

3. 难以理解的 class

在类式组件中,class 是学习 React 的一大障碍。因为你必须去理解 JavaScript 中 this 的工作方式,必须去搞清楚 this 的指向到底是谁,所以需要花很多的精力去学习 this,虽然前端开发人员必须掌握 this,但是依然处理起来非常麻烦,因此书写 class 组件的成本是比较高的和难以理解的。

PS: 如果你想搞清楚如何正确使用 this,如何判断出 this 指向,你可以查阅这篇文章《JavaScript 高级:this 指向终极指南》

以上便是 class 组件存在的一些主要问题,同时 class 组件并不能很好的压缩,会使热重载出现不稳定的情况,这些问题的出现便促使 React 团队提供一个使代码更易于优化的 API。

而 Hook 的出现 ,是开创性的,它可以让我们在不编写 class 的情况下使用 state 以及其他的 React 特性,Hook 的出现基本可以代替我们之前所有使用 class 组件的地方,但是如果是一个旧的项目,你并不需要直接将所有的代码重构为 Hooks,因为它完全向下兼容,你可以渐进式的来使用它,Hook 只能在函数组件中使用,不能在类组件, 或者函数组件之外的地方使用。

Hook 是指类似于 useState、useEffect 这样的函数,Hooks是对这类函数的统称。

常用的 Hooks

useState

useState 是允许你在 React 函数组件中添加 state 的 Hook,useState 是一种新方法 。它与 class 里面的 this.state 提供的功能完全相同。

一般来说,在函数退出后变量就会"消失",而 state 中的变量会被 React 所保留。

示例:

jsx 复制代码
import { memo, useState } from "react"

function useStateHook(props) {
  // 声明一个叫 count 的 state 变量
  const [count, setCount] = useState(0)

  return (
    <div>
      <h1>当前计数: {count}</h1>
      <button onClick={e => setCount(count+1)}>+1</button>
      <button onClick={e => setCount(count-1)}>-1</button>
    </div>
  )
}

export default memo(useStateHook)

从上述示例中,我们可以分析到,使用 useState Hook 时,useState 传入参数值 0(即初始化 state,声明了一个叫 count 的 state 变量,然后把它设为 0),可以从其返回值中解构出两个元素,第一个元素为当前状态的值,即初始化值 count,第二个元素为设置状态值的函数 setCount,React 会在重复渲染时记住它当前的值,并且提供最新的值给我们的函数,当点击 button 按钮的时候,传递一个新的值给 setCount,React 会重新渲染组件,并把最新的 count 值传给它。

值得注意的是,返回的这两个元素的名称是自定义的,因为返回的第二个参数是一个函数,所以在定义元素名称的时候一般都会在第一个元素名称的基础上加上 set,在本示例中,返回的第一个元素设置为 count,第二个元素设置为 setCount。

state 只在组件首次渲染的时候被创建。在下一次重新渲染时, useState 返回给我们当前的 state。

useEffect

概念

Effect Hook 可以让你在函数式组件中执行副作用的操作,来完成一些类似于 class 组件中生命周期的功能,事实上像网络请求、手动更新DOM、一些事件的监听,都是 React 更新 DOM 的一些副作用(Side Effects),所以对于完成这些功能的 Hook 被称之为 Effect Hook。

通过 useEffect 的 Hook,可以告诉 React 需要在渲染后执行某些操作,useEffect 要求我们传入一个回调函数,在 React 执行完更新 DOM 操作之后,就会回调这个函数,默认情况下, 无论是第一次渲染之后,还是每次更新之后,都会执行这个回调函数。总的来说就是 useEffect 会在回调函数中执行一些副作用,如网络请求/DOM操作(修改标题)/事件监听等。

如果你熟悉 React class 的生命周期函数,你可以把 useEffect Hook 看做 componentDidMount、componentDidUpdate 和 componentWillUnmount 这三个函数的组合。

示例:

jsx 复制代码
import React, { memo } from 'react'
import { useState, useEffect } from 'react'

const useEffectHook = memo(() => {
  const [count, setCount] = useState(0)

  useEffect(() => {
    document.title = count
  })

  return (
    <div>
      <h1>当前计数: {count}</h1>
      <button onClick={e => setCount(count+1)}>+1</button>
    </div>
  )
})

export default useEffectHook

从上述示例中,我们可以分析到,useEffect 传入的回调函数会在组件被第一渲染完成后和每次更新之后自动执行,类似于 componentDidMount 和 componentDidUpdate 生命周期。组件挂载时,会把标题设置为 0,当点击 1次 button 按钮的时候,执行更新操作,count 值变为 1,同时标题也会被改为 1。

清除 Effect

有时候,我们只想在 React 更新 DOM 之后运行一些额外的代码。比如发送网络请求,手动变更 DOM,记录日志等,这些都是常见的无需清除的操作。但是我们不可避免地也会在 componentDidMount 生命周期中设置事件订阅,之后还需要在 componentWillUnmount 生命周期中清除它。此时,我们可以在 useEffect 回调函数中返回一个新函数,在这个新函数中,我们可以执行一些清除操作。 该行为类似于 componentWillUnmount 生命周期。

示例:

jsx 复制代码
import React, { memo, useEffect } from 'react'
import { useState } from 'react'

const useEffectHook = memo(() => {
  const [count, setCount] = useState(0)

  // 在执行完当前组件渲染之后要执行的副作用代码
  useEffect(() => {
    // 监听事件
    eventBus.on("myEvent", foo)
    // 组件被重新渲染或者组件卸载的时候执行
    return () => {
      // 在这里执行 组件卸载的相关逻辑
      console.log("取消监听 eventBus 中的 myEvent 事件")
    }
  })

  return (
    <div>
      <h1>当前计数:{count}</h1>
      <button onClick={e => setCount(count+1)}>+1</button>
    </div>
  )
})

export default useEffectHook

从上述示例代码中,我们可以分析到,当组件被挂载时,会注册 myEvent 事件,当组件被重新渲染或者卸载的时候,就会执行回调函数中的 return 函数,执行其中的清除逻辑

值得注意的是,useEffect 回调函数中 return 的函数(此函数会执行一些清除的逻辑),它会在调用一个新的 effect 之前对前一个 effect 进行清理,这也说明了为什么在每次更新的时候都会执行 effect 的清除操作,如果有 return 一个函数的话。

为什么要在 effect 中返回一个函数? 这是 effect 可选的清除机制。 每个 effect 都可以返回一个清除函数,如此可以将添加和移除订阅的逻辑放在一起,它们都属于 effect 的一部分。React 会在组件更新和卸载的时候执行清除操作,正如之前学到的, effect 在每次渲染的时候都会执行。

使用多个 Effect

使用 Hook 的其中一个目的就是解决 class 中生命周期经常将很多的逻辑放在一起的问题,比如网络请求、事件监听、手动修改DOM,这些往往都会放在 componentDidMount 中。

使用 Effect Hook,我们可以将多个不相关的逻辑分离到不同的 useEffect 中,Hook 允许我们按照代码的用途分离它们, 而不是像生命周期函数那样。

示例:

jsx 复制代码
import React, { memo, useEffect } from 'react'
import { useState } from 'react'

const useEffectHook = memo(() => {
  const [count, setCount] = useState(0)

  // 一个函数式组件中, 可以存在多个useEffect
  
  useEffect(() => {
    document.title = count
    console.log("执行修改title的操作")
  })

  useEffect(() => {
    console.log("监听redux中的数据")
    return () => {
      // 取消redux中数据的监听
    }
  })

  useEffect(() => {
    console.log("监听eventBus的myEvent事件")
    eventBus.on("myEvent", foo)
    return () => {
      // 取消eventBus中的myEvent事件监听
    }
  })

  return (
    <div>
      <h1>当前计数:{count}</h1>
      <button onClick={e => setCount(count+1)}>+1({count})</button>
    </div>
  )
})

export default useEffectHook

从上述示例代码中,我们可以分析到,React 将按照 effect 声明的顺序依次调用组件中的每一个 effect。当组件被挂载和更新时,依次执行修改标题、监听 redux 中的数据、监听 eventBus 的 myEvent 事件逻辑。

Effect 性能优化

默认情况下, useEffect 的回调函数会在每次渲染时都重新执行,但是这会导致两个问题,一是某些代码我们只是希望执行一次即可,类似于 componentDidMountcomponentWillUnmount 中完成的事情(比如网络请求、订阅和取消订阅),二是多次执行也会导致一定的性能问题。

那么我们如何决定 useEffect 在什么时候应该执行和什么时候不应该执行呢?

useEffect 实际上有两个参数, 参数一: 执行的回调函数 fn;参数二:相关依赖的数组 [],也就是 useEffect 在哪些 state 发生变化时,才重新执行。

示例:

jsx 复制代码
import React, { memo, useEffect } from 'react'
import { useState } from 'react'

const useEffectHook = memo(() => {
  const [count, setCount] = useState(5)

  useEffect(() => {
    document.title = count
    console.log("执行修改title的操作")
  }, [count])

  return (
    <div>
      <h1>当前计数:{count}</h1>
      <button onClick={e => setCount(count+1)}>+1({count})</button>
    </div>
  )
})

export default useEffectHook

从上述示例代码中,我们可以分析到,我们传入 [count] 作为第二个参数,如果 count 的值是 5,组件在重新渲染的时候值还是 5,React 将对前一次渲染的 [5] 和后一次渲染的 [5] 进行比较,因为数组中的所有元素都是相等的,所以 React 就会跳过这个 effect 从而实现性能的优化。

如果 count 的值更新成 6,React 会把前一次渲染的数组 [5],和这一次渲染的数组 [6] 中的元素进行比较,因为这两个数组中的元素显然是不同的,所以 React 就会再次调用 effect。但是,如果数组中有多个元素,即使只有一个元素发生变化,React 也会执行 effect。

值得注意的是,如果一个函数我们不希望依赖任何的内容时,也可以传入一个空的数组 [] ,那么这里的两个回调函数分别对应的就是 componentDidMountcomponentWillUnmount 生命周期函数,即此时的 effect 仅在组件挂载和卸载的时候执行。

示例:

jsx 复制代码
import React, { memo, useEffect } from 'react'
import { useState } from 'react'

const useEffectHook = memo(() => {
  const [count, setCount] = useState(0)

  useEffect(() => {
    console.log("在这里发送网络请求, 从服务器获取数据")
    return () => {
      console.log("此处会在组件被卸载时, 才会执行一次")
    }
}, [])

  return (
    <div>
      <h1>当前计数:{count}</h1>
      <button onClick={e => setCount(count+1)}>+1({count})</button>
    </div>
  )
})

export default useEffectHook

从上述示例代码中,我们可以分析到,在 useEffect 的第二个参数中,可以传入空数组 [],当组件被挂载的时候,就会执行发送网络请求的逻辑,当组件更新的时候,不会执行任何逻辑,因为第二个参数,也就是数组中没有所依赖的 state,当组件卸载的时候,会执行回调函数中 return 中的函数。

useEffect 总结

useEffect Hook 有两个参数,第一个参数为一个回调函数,第二个参数是一个数组。

当只传入一个回调函数的时候,组件第一次渲染和更新的时候会执行里面的逻辑,类似于 class 组件中的componentDidMountcomponentDidUpdate 生命周期,此外,如果你在回调函数中 return 一个函数,这个函数会在组件更新和卸载的时候执行,一般都是执行一些清除的逻辑。

当传入第二个参数的时候,如果传入的不是一个空数组 ,React 会对前一次和后一次的数据进行比较,相等的话,就会跳过此 effect,达到性能优化的目的,否则就会重新执行 effect。如果传入的是一个空数组,effect 仅仅会在组件挂载和卸载的时候执行。

useContext

在 class 组件开发中,我们使用共享的 Context 有两种方式。

第一种,类组件可以通过类名.contextType = MyContext 的方式,在类中获取 context。

第二种,多个 Context 或者在函数式组件中通过 MyContext.Consumer 的方式共享 context。但是多个 Context 共享时的方式会存在大量的嵌套,而 Context Hook 允许我们通过 Hook 来直接获取某个 Context 的值。

useContext 可以让你读取和订阅组件中的 context

useContext 接收一个 context 对象。也就是 React.createContext 的返回值,并返回该 context 的当前值,当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider>valueprop 决定。

当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重新渲染,并使用最新传递给 MyContext provider 的 contextvalue 值。

示例:

jsx 复制代码
import React, { memo, useContext, createContext} from 'react'

const UserContext = createContext()
const ThemeContext = createContext()

function MyPage() {
    // 使用useContext
  const user = useContext(UserContext)
  const theme = useContext(ThemeContext)

  return (
    <div>
      <h1>用户: {user.name}-{user.age}</h1>
      <h2 style={{color: theme.color}}>主题</h2>
    </div>
  )
}

const App = memo(() => {
  return (
    <ThemeContext.Provider value={{ color: 'dark' }>
      <UserContext.Provider value={{ name: 'lisi', age: '20' }>
        <MyPage />
      </UserContext.Provider>
    </ThemeContext.Provider>
  )
})

export default App

useReducer

useReducer 仅仅是 useState 的一种替代方案,并不能替代 Redux。在某些场景下,如果 state 的处理逻辑比较复杂且包含多个子值或者下一个 state 依赖于之前的 state 等,我们可以通过 useReducer 来对其进行拆分。

示例:

jsx 复制代码
import React, { memo, useReducer } from 'react'

function reducer(state, action) {
  switch(action.type) {
    case "increment":
      return { ...state, counter: state.counter + 1 }
    case "decrement":
      return { ...state, counter: state.counter - 1 }
    case "add_number":
      return { ...state, counter: state.counter + action.num }
    case "sub_number":
      return { ...state, counter: state.counter - action.num }
    default:
      return state
  }
}

const App = memo(() => {
  const [state, dispatch] = useReducer(reducer, { counter: 0 })

  return (
    <div>
      <h1>当前计数: {state.counter}</h2>
      <button onClick={e => dispatch({type: "increment"})}>+1</button>
      <button onClick={e => dispatch({type: "decrement"})}>-1</button>
      <button onClick={e => dispatch({type: "add_number", num: 5})}>+5</button>
      <button onClick={e => dispatch({type: "sub_number", num: 5})}>-5</button>
      <button onClick={e => dispatch({type: "add_number", num: 100})}>+100</button>
    </div>
  )
})

export default App

useCallback

useCallback 是一个允许你在多次渲染中缓存函数的 React Hook,实际的目的是为了进行性能的优化。

useCallback 可以传入两个参数,第一个参数是一个函数 fn,第二个参数是有关是否更新 fn 的所有响应式值的一个列表。

useCallback 的返回值 是返回一个函数的 memoized(记忆的)值,即返回一个回调函数,在依赖不变的情况下多次定义的时候,返回的值是相同的。使用 useCallback 的目的是不希望子组件进行多次渲染,并不是为了函数进行缓存。

当需要将一个函数传递给子组件时,最好使用 useCallback 进行优化,将优化之后的函数, 传递给子组件,防止子组件进行无效的重新渲染。

优化之前的示例:

jsx 复制代码
import React, { memo, useState, useCallback, useRef } from 'react'

// 当 props 中的属性发生改变时, 组件本身就会被重新渲染
const Children = memo(function(props) {
  const { increment } = props
  console.log("Childern组件被渲染")
  return (
    <div>
      <button onClick={increment}>increment+1</button>

      {/* 假设此处有100个子组件 */}
    </div>
  )
})

const App = memo(function() {
  const [count, setCount] = useState(0)

  // 普通的函数
  const increment1 = () => {
    console.log("普通的函数---increment1")
    setCount(count+1)
  }

  return (
    <div>
      <h1>计数: {count}</h1>
      <button onClick={increment1}>+1</button>
      <Children increment={increment1}/>      
    </div>
  )
})

export default App

从上述代码示例中,我们可以分析出,当每次点击按钮 +1 的时候,React 都会把 increment1 函数传给 Children 组件,此时组件 APP 重新渲染,increment1 函数也会被重新定义,所以传给 Children 组件的 increment1 函数与上一次传入的是不相同的,这样就会导致 Children 组件被重新渲染,每次点击按钮,Children 组件都会被重新渲染,显然这是没有必要的。

使用 useCallback 优化之后的示例:

jsx 复制代码
import React, { memo, useState, useCallback, useRef } from 'react'

// 当 props 中的属性发生改变时, 组件本身就会被重新渲染
const Children = memo(function(props) {
  const { increment } = props
  console.log("Childern组件被渲染")
  return (
    <div>
      <button onClick={increment}>increment+1</button>

      {/* 假设此处有100个子组件 */}
    </div>
  )
})

const App = memo(function() {
  const [count, setCount] = useState(0)

  // 使用 useRef, 在组件多次渲染时, 返回的是同一个值
  const countRef = useRef()
  countRef.current = count

  // 使用 useCallback 优化后的函数 increment2
  const increment2 = useCallback(function foo() {
    console.log("使用 useCallback 优化后函数---increment2")
    setCount(countRef.current + 1)
  }, [])

  return (
    <div>
      <h1>计数: {count}</h1>
      <button onClick={increment2}>+1</button>
      <Children increment={increment2}/>
    </div>
  )
})

export default App

从上述示例代码中,我们可以分析到,当点击按钮 +1 的时候,Children 组件只会渲染一次,不会被重新渲染,因为每次传入给组件 Children 的 increment2 函数是经过 useCallback 优化后的。这得益于 useCallback Hook,它会在其依赖不变的情况下,返回相同的回调函数。其中涉及到为了解决闭包陷阱,使用 useRef Hook来保存数据,只有这样才能在组件被重新渲染时返回的是同一个值。

useMemo

useMemo 在每次重新渲染的时候能够缓存计算的结果,与 useCallback 相比,useMemo 返回是 return 返回的内容,useCallback 返回的是一个函数。

示例:

jsx 复制代码
import React, { memo, useState, useCallback, useRef } from 'react'

// 当 props 中的属性发生改变时, 组件本身就会被重新渲染
const Children = memo(function(props) {
  const { increment } = props
  console.log("Childern组件被渲染")
  return (
    <div>
      <button onClick={increment}>increment+1</button>

      {/* 假设此处有100个子组件 */}
    </div>
  )
})

const About = memo(function(props) {
    console.log('About被渲染');
    return (
        <div>
            <p>About</p>
            <p>{props.user.name}</p>
            <p>{props.user.age}</p>
        </div>
    )
})

const App = memo(function() {
  const [count, setCount] = useState(0)

  // 普通的函数
  const increment1 = () => {
    console.log("普通的函数---increment1")
    setCount(count+1)
  }

  // 使用 useMemo 缓存具体的数据
  const user = useMemo(()=>{
    return {name: 'zhangsan', age: 20};
  }, [])

  return (
    <div>
      <h1>计数: {count}</h1>
      <button onClick={increment1}>+1</button>

      <Children increment={increment1}/>
      <About user={user}/>
    </div>
  )
})

export default App

从上述示例代码中,我们可以分析到,我们使用 useMemo 缓存数据,当点击按钮时,App 组件被重新渲染,因为 user 被缓存,所以 About 子组件不会被重新渲染。

除此之外,useCallback(fn, [])useMemo(() => fn, []) 其实是等价的。我们可以使用 useMemo 重写上述 useCallback 示例,同样达到优化性能的效果。

示例:

jsx 复制代码
import React, { memo, useState, useMemo, useRef } from 'react'

// 当 props 中的属性发生改变时, 组件本身就会被重新渲染
const Children = memo(function(props) {
  const { increment } = props
  console.log("Childern组件被渲染")
  return (
    <div>
      <button onClick={increment}>increment+1</button>

      {/* 假设此处有100个子组件 */}
    </div>
  )
})

const App = memo(function() {
  const [count, setCount] = useState(0)

  // 使用 useRef, 在组件多次渲染时, 返回的是同一个值
  const countRef = useRef()
  countRef.current = count

  // 使用 useMemo 实现 useCallback 同样的效果

  const increment2 = useMemo(function foo() {
    console.log("使用 useMemo 优化后函数---increment2")
    return () => {
      setCount(countRef.current + 1)
    }
  }, [])

  return (
    <div>
      <h1>计数: {count}</h1>
      <button onClick={increment2}>+1</button>

      <Children increment={increment2}/>
    </div>
  )
})

export default App

useRef

useRef 返回一个 ref 对象,返回的 ref 对象在组件的整个生命周期保持不变。 useRef 一方面用于引入 DOM,另一方面可以保存一个数据,这个对象在整个生命周期中可以保存不变。

示例1:引入DOM

jsx 复制代码
import React, { memo, useRef } from 'react'

const App = memo(() => {
  const titleRef = useRef()
  const inputRef = useRef()
  
  function showTitleDom() {
    console.log(titleRef.current)
    inputRef.current.focus()
  }

  return (
    <div>
      <h1 ref={titleRef}>我是标题</h1>
      <input type="text" ref={inputRef} />
      <button onClick={showTitleDom}>查看title的dom</button>
    </div>
  )
})

export default App

从上述示例代码中,我们可以分析到,使用 useRef 的返回值 titleRef 和 inputRef 赋值给 h1 标签和 input 标签的 ref 属性,我们就可以从其 current 属性中获取到 h1 标签的内容和使input 标签聚焦输入框。

示例2:保存数据,使用 ref 保存上一次的某个值,从而解决闭包陷阱

jsx 复制代码
import React, { memo, useRef } from 'react'
import { useCallback } from 'react'
import { useState } from 'react'

let obj = null

const App = memo(() => {
  const [count, setCount] = useState(0)
  const nameRef = useRef()
  console.log(obj === nameRef)
  obj = nameRef

  // 通过useRef解决闭包陷阱
  const countRef = useRef()
  countRef.current = count

  const increment = useCallback(() => {
    setCount(countRef.current + 1)
  }, [])

  return (
    <div>
      <h1>当前计数: {count}</h1>
      <button onClick={e => setCount(count+1)}>+1</button>
      <button onClick={increment}>+1</button>
    </div>
  )
})

export default App

useLayoutEffect

useEffect 会在渲染的内容更新到 DOM 上后执行,不会阻塞DOM的更新,可能会出现闪屏的情况,因为组件已经渲染到屏幕上,再去更改其布局和样式,会看到闪屏的情况。

useLayoutEffect 会在渲染的内容更新到 DOM 上之前执行,会阻塞DOM的更新,不会出现闪屏的情况,因为组件这时还没有渲染到屏幕上,去更新其布局和样式,看不到闪屏的情况。

因此,如果想修改 DOM 的布局样式,推荐使用 useLayoutEffect。

useEffect示例:

jsx 复制代码
import React, { memo, useEffect, useLayoutEffect, useState } from 'react'

function Home() {
  const divRef = useRef()
  
  useEffect(()=>{
    console.log('组件被挂载或更新完成---useEffect');
    divRef.current.style.fontSize = '50px';
    divRef.current.style.fontSize = '100px';
  
    return ()=>{
      console.log('组件即将被卸载---useEffect');
    }
  })
  
  return (
    <div>
      <div ref={divRef}>文字大小</div>
    </div>
  )
}

const App = memo(() => {
  const [show, setShow] = useState(true)
  return (
    <div>
      {show && <Home />}
      <button
        onClick={() => {
          setShow(!show)
        }}
      >
        点击按钮切换
      </button>
    </div>
  )
})

export default App

useLayoutEffect示例:

jsx 复制代码
import React, { memo, useEffect, useLayoutEffect, useState } from 'react'

function Home() {
  const divRef = useRef()
  
  useLayoutEffect(() => {
    console.log('组件被挂载或更新完成---useLayoutEffect');
    divRef.current.style.fontSize = '50px'
    divRef.current.style.fontSize = '100px'

    return () => {
      console.log('组件即将被卸载---useLayoutEffect')
    }
  })
  
  return (
    <div>
      <div ref={divRef}>文字大小</div>
    </div>
  )
}

const App = memo(() => {
  const [show, setShow] = useState(true)
  return (
    <div>
      {show && <Home />}
      <button
        onClick={() => {
          setShow(!show)
        }}
      >
        点击按钮切换
      </button>
    </div>
  )
})

export default App

其它 React 内置 Hooks

useId:可以生成传递给无障碍属性的唯一 ID。

useTransition:帮助你在不阻塞 UI 的情况下更新状态的 React Hook。

useDeferredValue:可以让你延迟更新 UI 的某些部分。

useSyncExternalStore:是一个让你订阅外部 store 的 React Hook。

useInsertionEffect:是为 CSS-in-JS 库的作者特意打造的专属 Hook。

useImperativeHandle:它能让你自定义由 ref 暴露出来的句柄。

useDebugValue 可以让你在 React 开发工具 中为自定义 Hook 添加标签。

Hooks 使用规则

1. 只能在函数最外层调用 Hook。

2. 不要在循环、条件判断或者子函数中调用 Hook,确保总是在你的 React 函数的最顶层以及任何 return 之前调用他们。

3. 只能在 React 的函数组件中调用 Hook,不要在其他 JavaScript 函数中调用,你也可以在自定义 Hook 中调用其他 Hook。

自定义 Hook

通过自定义 Hook,可以对其它 Hook 的代码进行复用,在 React 中只能在函数式组件中和自定义 Hook 中使用 Hook。只要在函数名称前面加上 use,那么就表示这个函数是一个自定义 Hook,就表示可以在这个函数中使用其它的 Hook。

示例1:自定义一个生命周期 Hook

jsx 复制代码
import React, {useEffect, useState} from 'react'

// 自定义 useAddListenr Hook
function useAddListenr(name) {
  // 在自定义 Hook 中可以使用 React 内置的 Hook
  useEffect(()=>{
    console.log(name, '组件被挂载或者更新完成,可在此阶段添加监听');
    return ()=>{
      console.log(name, '组件即将被卸载,可在此阶段移出监听');
    }
  })
}

function Home() {
  useAddListenr('Home')
  return (
    <div>Home</div>
  )
}

function About() {
  useAddListenr('About')
  return (
      <div>About</div>
  )
}

function App() {
  const [show, setShow] = useState(true)
  return (
    <div>
      {show && <Home/>}
      {show && <About/>}
      <button onClick={()=>{setShow(!show)}}>切换</button>
    </div>
  )
}

export default App

示例2:自定义一个获取 Context 的 Hook

jsx 复制代码
import React, {createContext, useContext} from 'react'

const UserContext = createContext({})
const InfoContext = createContext({})

function useGetContext() {
  const user = useContext(UserContext)
  const info = useContext(InfoContext)
  return [user, info]
}

function Home() {
  const [user, info] = useGetContext()
  return (
    <div>
      <p>{user.name}</p>
      <p>{user.age}</p>
      <p>{info.gender}</p>
      <hr/>
    </div>
  )
}

function About() {
  const [user, info] = useGetContext()
  return (
    <div>
      <p>{user.name}</p>
      <p>{user.age}</p>
      <p>{info.gender}</p>
      <hr/>
    </div>
  )
}

function App() {
  return (
    <UserContext.Provider value={{ name: 'lisi', age: 20 }}>
      <InfoContext.Provider value={{ gender: 'man' }}>
        <Home/>
        <About/>
      </InfoContext.Provider>
    </UserContext.Provider>
  )
}

export default App

在企业开发中,有一个不成文的规定,只要我们涉及到抽取代码,被抽取的代码中用到了其它的 Hook,那么就必须把这些代码抽取到自定义 Hook 中。

第三方 Hooks

ahooks 是由阿里巴巴开源的一套高质量可靠的 React Hooks 库。

React Use 是一个丰富的 React Hooks 集合,其包含了传感器、用户界面、动画效果、副作用、生命周期、状态这六大类的 Hooks。

Beautiful React Hook 是一组漂亮的(希望有用的)React hooks 来加速你的组件和 Hooks 开发。

React Hook Form 是一个高性能、灵活、易拓展、易于使用的表单校验库,用于 React Web 和 React Native 的表单验证。

总结

使用 Hooks 开发已经成为目前的主流形式,此概念最初由 React 率先提出,Hooks 的出现是开创性的,可以让我们在不书写类式组件的前提下,使用函数式组件开发应用,大大提高开发者的开发效率,同时也降低了开发者开发大型复杂应用时的心智负担。这无疑是 React 技术上的一次革新。

目前,React 已经全面拥抱 Hooks,我们可以放心大胆的去使用 Hooks 书写函数式组件,除了 React 官网提供的内置 Hooks,社区中也有不少优秀的第三方 Hooks 供我们使用。

参考

React 旧版文档

React 新版文档(英文)

React 新版文档(中文)

效率宝典:10个超实用的React Hooks库

相关推荐
神仙别闹15 分钟前
基于tensorflow和flask的本地图片库web图片搜索引擎
前端·flask·tensorflow
sszmvb123442 分钟前
测试开发 | 电商业务性能测试: Jmeter 参数化功能实现注册登录的数据驱动
jmeter·面试·职场和发展
测试杂货铺1 小时前
外包干了2年,快要废了。。
自动化测试·软件测试·python·功能测试·测试工具·面试·职场和发展
王佑辉1 小时前
【redis】redis缓存和数据库保证一致性的方案
redis·面试
真忒修斯之船1 小时前
大模型分布式训练并行技术(三)流水线并行
面试·llm·aigc
GIS程序媛—椰子1 小时前
【Vue 全家桶】7、Vue UI组件库(更新中)
前端·vue.js
DogEgg_0011 小时前
前端八股文(一)HTML 持续更新中。。。
前端·html
ZL不懂前端1 小时前
Content Security Policy (CSP)
前端·javascript·面试
木舟10091 小时前
ffmpeg重复回听音频流,时长叠加问题
前端
王大锤43912 小时前
golang通用后台管理系统07(后台与若依前端对接)
开发语言·前端·golang