React-Hook 应用

Hook 是 react 16.8 推出的新特性,具有如下优点:

  • Hook 使你在无需修改组件结构的情况下复用状态逻辑。——自定义 hook
  • Hook 将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据)——Effect
  • Hook 使你在非 class 的情况下可以使用更多的 React 特性。——拥抱函数式组件

1. 简介

Hook 就是 JavaScript 函数,但是使用它们会有两个额外的规则:

  • 只能在函数最外层调用 Hook,也即Hook 需要在我们组件的最顶层调用。不要在循环、条件判断或者子函数中调用,这会破坏更新时 hook 顺序的一致性,造成数据读取错误。
  • 只能在 React 的函数组件中调用 Hook。不要在其他 JavaScript 函数中调用。(还有一个地方可以调用 Hook —— 就是自定义的 Hook 中)

可以通过 ESLint 配置来提醒自己遵循 Hook 开发规则:

  • 安装插件 npm install eslint-plugin-react-hooks --save-dev

  • 配置 package.json 中的eslint-config:

// 你的 ESLint 配置
"eslintConfig": {
  "plugins": [
    // ...
    "react-hooks"
  ],
  "rules": {
    // ...
    "react-hooks/rules-of-hooks": "error", // 检查 Hook 的规则
    "react-hooks/exhaustive-deps": "warn" // 检查 effect 的依赖
  }
}

2. React Hook 的基础 API (附实例)

具体 API 介绍可以查阅官网 Hook API Reference

2.1 组件中的状态——useState

在使用 class 定义组件时,可以通过 this.state 定义组件内的状态属性,在 render 时使用 this.state[key] 获取状态值,使用 this.setState 来修改状态。Hook 中提供了 useState 方法,用于快速定义函数组件内的状态和对应的更改函数。

使用方法:const [state, setState] = useState(defaultValue)

实例:

import React, { useState } from 'react'
function Counter() {
  // 初始化 count = 0
  const [count, setCount] = useState(0)
  return (

你点击了 {count} 次

{/* 直接调用 setCount,传入新的值,赋值给 count */}
) }
2.2 组件中的生命周期——useEffect

函数式组件在发生更新时,都会顺序执行函数主体,相当于类组件中的 render 函数。而在 render 过程中,是不允许执行改变 DOM、添加订阅、设置定时器、记录日志等包含副作用的操作,因此在类组件中,我们通常在生命周期函数中执行必要的包含副作用操作。在函数式组件中,useEffect 提供了执行副作用操作的支持,当 React 渲染组件时,会保存已使用的 effect,并在更新完 DOM 后执行它。useEffect ≈ componentWillMount + componentDidUpdate + componentWillUnmount,其内部可以访问到组件的 propsstate

使用方法:useEffect( didUpdateFn )

实例:

import React, { useState, useEffect } from 'react'
function Counter() {
  // 初始化 count = 0
  const [count, setCount] = useState(0)
  
  // 如果第二个参数为空,则只有在组件被销毁时才解绑,也就是 副作用 等价于 componentDidMount,解绑 等价于 componentWillUnMount
  useEffect(() => {
      console.log('useEffect => 组件挂载')
      // 返回一个清除函数,当副作用中有定时器或监听事件时清除
      return () => {
          console.log('useEffect => 组件被销毁')
      };
  }, [])
  // 第二个参数,每次当 count 发生变化就执行解绑原来的数据并重新执行副作用
  useEffect(() => {
      console.log('useEffect => count 数据挂载')
      return () => {
          console.log('useEffect => count 数据解绑')
      };
  }, [count])
  
  return (

你点击了 {count} 次

) }

React 会在调用一个新的 effect 之前对前一个 effect 进行清理。在上述程序中,当 count 值更新时,会先输出 ’useEffect => count 数据挂载‘,再输出 ’useEffect => count 数据挂载‘。因此当我们在 useEffect 中设置定时器/事件时,通过返回一个清除函数,使得在下一次依赖发生更新时,能够清除上一次的定时器/事件,以此避免内存泄露。否则每次依赖更新时,都会增加一个定时器/事件。

2.3 跨组件通信——useContext

在跨组件通信时,可以借助 context 实现组件间的传值。useContext 用于快速获取组件上层最近的 contextObj.Provider 所提供的 value 值,等价于 contextObj.Consumer

使用方法:useContext(ContextObj)contextObjReact.createContext 返回的 context 对象

实例:

import React, { useState, useEffect, useContext, createContext } from 'react'

const ColorContext = createContext()

function Container() {
  const [color, setColor] = useState('#ffff00')
  const toggleColor = () => {
      const saturation = () => Math.random() * 255
      setColor(`rgb(${saturation()}, ${saturation()}, ${saturation()})`)
  }
  // 1. 使用 ColorContext.Provider 将 color 值传给内部的组件
  // 3. 当按钮点击切换背景色时, color 值发生变化,将通知到 Counter 组件中
  return (
     
     
  )
}

function Counter() {
  // ...
  // 2. 使用 useContext 返回最近的 Provider 提供的 value 值,本例中也即 color 值,并订阅 color 值的变化
  const color = useContext(ColorContext)
  return (

你点击了 {count} 次

) }
2.4 复杂状态管理——useReducer

在 redux 状态管理中,使用 reducer 根据 action 的不同,对 state 执行不同的操作。在 hook 中,useState 支持直接修改 state,但是当修改逻辑较为复杂时,可以改用 useReducer 来定义不同的更改行为。通过传入一个形如 (state, action) => {} 的 reducer,返回状态及其 dispatch 函数。还可以使用后面的两个参数对 state 执行初始化操作,initialArg 将作为 init 函数的参数传入。

使用方法:const [state, dispatch] = useReducer(reducer, initialArg, init)

实例:

import React, { useState, useEffect, useContext, createContext, useReducer } from 'react'

function Container() {
  // ...
  return (
     
     
  )
}
function Counter() {
  // ...
  const init = initialCount => ({count: initialCount})
  const [state, dispatch] = useReducer((state, action) => {
        switch(action.type) {
            case 'add': 
                return {count: state.count + 1}
            case 'sub':
                return {count: state.count - 1}
            case 'reset':
                    return init(action.payload)
            default:
                return state
        }
  }, initialCount, init)
  return (

你点击了 {state.count} 次

) }
2.5 组件性能优化——useCallback / useMemo

在组件生命周期的应用中,常常有利用 shouldCompnentUpdate 判断参数/状态的相等性,避免不必要的组件渲染。 useCallback 也是一种类似的组件优化手段,其返回一个 memoized 函数,仅在依赖项发生变化时,函数体才会更新。useCallback(fn, deps) 相当于 useMemo(() => fn, deps),不同的是 useMemo 返回的是一个 memoized 值,当依赖项发生变化时,fn 才会执行,该值才会发生更新。传入 useMemo 的函数会在渲染期间执行,因此useMemo 内部,不要执行与渲染无关的操作。依赖项并不会作为参数传入回调函数中,但内部执行函数可以直接使用依赖项,如 fn(deps)

使用方法:useCallback(() => { doSomething() }, depsArr) / useMemo(() => doSomething(), depsArr)

实例:

import React, { useState, useEffect, useContext, createContext, useReducer, useMemo } from 'react'
// ...
function Counter() {
    // ...
    const [asyncName, setName] = useState('')
    function sayName(name) {
        setTimeout(() => {
            // 模拟异步请求,并根据请求结果设置状态值
            console.log(`${name} 正在操作`)
            setName(`user_${name}`)
        }, 1000)  
    }
  
    // 直接调用 sayName 的话,每次 state.count 发生变化时,虽然 asyncName 并不会变化,但 sayName 每次都会被执行。如果是一个比较耗时的异步请求,将降低组件的性能
    // sayName(name) 
  
    // 对比直接调用,使用 useMemo,能够避免 count 变化时 sayName 的频繁调用,从而优化组件性能
    // 仅在依赖项 name 值发生变化时,sayName 方法才会被执行
    useMemo(() => sayName(name), [name])
    return (

用户名:{asyncName}

{/* ... */}
) }

效果:

  • 直接调用 sayName
每次点击 count 的按钮都会打印 xxx 正在操作
  • 使用 useMemo
    仅有更新 name 时才会打印 xxx 正在操作
2.6 组件内值的保存——useRef

ref 是一种访问 DOM 的方式,useRef 返回一个“盒子”,可以在其 current 属性中保存一个任何类型的可变值,如 DOM 元素、定时器、订阅器等。useRef 在每次渲染时返回同一个 ref 对象,当 ref 对象内容发生变化时,useRef不会通知你。变更 .current 属性不会引发组件重新渲染。

使用方法:const oRef = useRef(initialValue)

实例:

import React, {useRef, useEffect} from 'react'
function InputItem() {
    const inputEle = useRef(null)
    const timerId = useRef(null)
    const [time, setTime] = useState(0)
    
    useEffect(() => {
        const id = setInterval(() => {
            // 使用函数更新的方式,避免依赖项
            setTime(t => t + 1)
        }, 1000)
        timerId.current = id
        return () => clearInterval(id)
    }, [])

    const focusBtnClick = () => {
        // inputEle.current 已经挂载到 DOM 中的文本输入框元素上
        inputEle.current.focus()
    }
    const clearBtnClick = () => {
        // timerId.current 已经被写入了定时器的 id,可以在 click 事件中中止定时器
        clearInterval(timerId.current)
    }

    return (

定时器数值为:{time}

) }

效果:

React-Hook 应用_第1张图片
useRef.gif
2.7 其它

以下三个 hook 都是极少使用的方法,简单介绍其应用,有必要时可以查阅官方文档

  • useImperativeHandle :用于自定义暴露给父组件的子组件内部某一 ref 实例值,与 forwardRef(将内部某一 ref 实例值全部暴露给父组件) 配合使用。
  • useLayoutEffect :作用同 useEffect,不同的是 useEffect 是在 DOM 元素渲染完成后执行,而 useLayoutEffect 是与 DOM 更新同步执行。
  • useDebugValue :用于在 React 开发者工具中显示自定义 hook 的标签。

3. 自定义 Hook

在函数化开发时,我们常常将多个函数间共用的逻辑抽离为某一功能函数,增强代码的复用性。而在组件化开发过程中,两个组件之间也可能存在同样的功能逻辑,比如需求列表和详情页都需要获取需求项的状态(规划中/进行中/已完成),此时可以把 “查询需求项状态” 这一功能用自定义 hook 抽离出来,不仅能够提高代码复用性和可读性,还能方便测试。自定义 hook 命名需要以 use 开头,以方便 react 自动检查是否违反了 hook 规则。目前,也有很多第三方 hook 实现:https://github.com/streamich/react-use

实例:

import React, { useState, useEffect } from 'react'

// 自定义 hook : 根据需求项的 id 值查询状态
function useStatus(demandId) {
    // 需求项状态,0 - 规划中, 1 - 进行中, 2 - 已完成
    const [status, setStatus] = useState(0)
    useEffect(() => {
        // 模拟一下异步请求
        const getStatus = setTimeout(() => {
            setStatus(demandId % 3)
        }, 200)
        return () => {
            clearTimeout(getStatus)
        }
    }, [demandId])
    const description = ['规划中', '进行中', '已完成']
    return {code: status, status: description[status]}
}

// 需求列表组件
function DemandList() {
    const [list, setList] = useState([])
    const [selectedId, setSelectedId] = useState(null)

    useEffect(() => {    
        // 模拟一下数据
        let mockList = []
        for(let i = 0; i <= 9; i++) {
            const id = i + Math.round(Math.random() * 100)
            mockList.push({
                id,
                name: `需求${id}`
            })
        }
        setList(mockList)
    }, [])

    return (
    {list.map( demand => ( {demand.name} ) )}

{/* 这里为了偷懒,就没有用路由,而是直接显示在下面 */} { selectedId && }
) } // 需求项组件 function DemandItem({demandId, selectedMethod, children}) { // 调用自定义 hook 获取需求项状态 const {code, status} = useStatus(demandId) const color = ['#F4A460', '#FFD700', '#32CD32'] return (
  • {children} {selectedMethod(demandId)}} >{status}
  • ) } // 需求详情页组件 function DemandDetail({demandId}) { // 调用自定义 hook 获取需求项状态 const {status} = useStatus(demandId) const [info, setInfo] = useState({}) useEffect(() => { // 根据 id 查询需求的详细信息 setInfo({ name: `需求${demandId}`, detail: '这是需求详情信息呀~这是需求详情信息呀~这是需求详情信息呀~这是需求详情信息呀~这是需求详情信息呀~' }) }, [demandId]) return (

    项目名称为:{info.name}({status})

    {info.detail}

    ) } export default DemandList

    效果:

    自定义 hook

    你可能感兴趣的:(React-Hook 应用)