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 (<div>
    <p>你点击了 {count} 次</p>  
    {/* 直接调用 setCount,传入新的值,赋值给 count */}
    <button onClick={()=> setCount(count+1)}>点击</button>
  </div>)
}
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 (<div>
    <p>你点击了 {count} 次</p>  
    <button onClick={()=> setCount(count+1)}>点击</button>
  </div>)
}

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 (<ColorContext.Provider value={color}>
     <Counter />
     <button onClick={toggleColor}>切换背景色</button>
  </ColorContext.Provider>)
}

function Counter() {
  // ...
  // 2. 使用 useContext 返回最近的 Provider 提供的 value 值,本例中也即 color 值,并订阅 color 值的变化
  const color = useContext(ColorContext)
  return (<div>
    <p style={{backgroundColor: color}}>你点击了 {count} 次</p>  
    <button onClick={()=> setCount(count+1)}>点击</button>
  </div>)
}
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 (<ColorContext.Provider value={color}>
     <Counter initialCount={0}/>
     <button onClick={toggleColor}>切换背景色</button>
  </ColorContext.Provider>)
}
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 (<div>
    <p style={{backgroundColor: color}}>你点击了 {state.count} 次</p> 
    <button onClick={() => dispatch({type: 'add'})}>+</button>
    <button onClick={() => dispatch({type: 'sub'})}>-</button>
    <button onClick={() => dispatch({type: 'reset', payload: initialCount})}>Reset</button>
  </div>)
}
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 (<div>
        <h1>用户名:{asyncName}</h1>
        {/* ... */}
    </div>)
}

效果:

  • 直接调用 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 (<div>
        <h2>定时器数值为:{time}</h2>
        <input type='text' ref={inputEle} />
        <button onClick={focusBtnClick}>focus</button>
        <button onClick={clearBtnClick}>stop</button>
    </div>)
}

效果:

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 (<div>
        <ul>
            {list.map( demand => (
                <DemandItem 
                    demandId = {demand.id}
                    selectedMethod = {setSelectedId}
                >
                    {demand.name}
                </DemandItem>
            ) )}
        </ul>
        <hr />
        {/* 这里为了偷懒,就没有用路由,而是直接显示在下面 */}
        { selectedId && <DemandDetail demandId = {selectedId} />}
    </div>)
    
}

// 需求项组件
function DemandItem({demandId, selectedMethod, children}) {
    // 调用自定义 hook 获取需求项状态
    const {code, status} = useStatus(demandId)
    const color = ['#F4A460', '#FFD700', '#32CD32']

    return (<li>
        <span style = {{display: 'inline-block', width: '100px'}}>{children}</span>
        <span 
            style = {{backgroundColor: color[code], cursor: 'pointer'}}
            onClick = {() => {selectedMethod(demandId)}}
        >{status}</span>
    </li>)
}

// 需求详情页组件
function DemandDetail({demandId}) {
    // 调用自定义 hook 获取需求项状态
    const {status} = useStatus(demandId)
    const [info, setInfo] = useState({})

    useEffect(() => {
        // 根据 id 查询需求的详细信息
        setInfo({
            name: `需求${demandId}`,
            detail: '这是需求详情信息呀~这是需求详情信息呀~这是需求详情信息呀~这是需求详情信息呀~这是需求详情信息呀~'
        })
    }, [demandId])

    return (<div>
        <h3>项目名称为:{info.name}({status})</h3>
        <p>{info.detail}</p>
    </div>)
}

export default DemandList

效果:

自定义 hook
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,240评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,328评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,182评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,121评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,135评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,093评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,013评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,854评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,295评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,513评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,398评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,989评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,636评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,657评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352

推荐阅读更多精彩内容

  • 概述 Hook 使你在非 class 的情况下可以使用更多的 React 特性。Hook 是一些可以在函数组件里钩...
    bowen_wu阅读 349评论 0 0
  • Hooks是 React v16.8 的新特性,可以在不使用类组件的情况下,使用 state 以及其他的React...
    hellomyshadow阅读 13,438评论 0 5
  • Hook Hook 是 React 16.8.0 的新增特性。 Hook 使你在非 class 的情况下可以使用更...
    tigerHee阅读 396评论 0 0
  • 你还在为该使用无状态组件(Function)还是有状态组件(Class)而烦恼吗?——拥有了hooks,你再也不需...
    水落斜阳阅读 82,323评论 11 100
  • Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他...
    Oldboyyyy阅读 4,847评论 0 3