learn icon indicating copy to clipboard operation
learn copied to clipboard

React Hooks 使用总结

Open yangtao2o opened this issue 3 years ago • 0 comments

React Hooks 使用总结

组件类的缺点

  1. 大型组件很难拆分和重构,也很难测试。
  2. 业务逻辑分散在组件的各个方法之中,导致重复逻辑或关联逻辑。
  3. 组件类引入了复杂的编程模式,比如 render props 和高阶组件

函数组件

  1. 数据流的管道,不是复杂的容器
  2. 组件的最佳写法应该是函数,而不是类
  3. 必须是纯函数,不能包含状态,也不支持生命周期方法,因此无法取代类

类和纯函数

  • 类:数据和逻辑的封装

  • 纯函数:只应该做一件事,就是返回一个值

    • 函数返回结果只依赖参数
    • 函数执行不会对外产生可观察的变化
  • 副作用:数据计算无关的操作

    • 如:生成日志、储存数据、改变应用状态等

Hook

目的:

  • React 函数组件的副作用解决方案
  • 加强版函数组件,完全不使用"类",可写出一个全功能的组件

含义:

  • 组件尽量写成纯函数
  • 当需要外部功能和副作用的时候,就用钩子把外部代码"钩"进来

Hook 方法

  1. useState - 数据存储,派发更新
  2. useEffect - 组件更新副作用钩子
  3. useRef - 获取元素 ,缓存数据
  4. useContext - 自由获取 context
  5. useReducer - 无状态组件中的 redux
  6. useMemo - 小而香性能优化
  7. useCallback - useMemo 版本的回调函数
  8. useLayoutEffect - 渲染更新之前的 useEffect

useState

特点:

  • useState 派发更新函数的执行,会使 function 组件从头到尾执行一次
  • 可以配合 useMemo,usecallback 等 api 配合使用,起到优化作用

使用:

import { useState } from 'react'

export default function Example() {
  // 声明一个叫 "count" 的 state 变量
  const [count, setCount] = useState(0)

  console.log('outer', count) // 会及时更新

  const handleClick = () => {
    setCount(count + 1)
    // 只有当下一次上下文执行的时候,state值才随之改变
    console.log('inner', count) // 不会及时更新
  }
  return (
    <>
      <p>You clicked {count} times</p>
      <button onClick={handleClick}>Click me</button>
    </>
  )
}

useEffect

特点:

  • useEffect 第一个参数不能直接用 async await 语法,可以在内部调用
  • 第二个参数是个数组,可作为限定条件,限制 useEffect 的执行
  • 如果没有第二个参数,useEffect 会受 state 或 props 更新而执行
  • return 可清除 effect

与 useLayoutEffect 的执行过程对比:

  • 组件更新挂载完成 -> 浏览器 dom 绘制完成 -> 执行 useEffect 回调
  • 组件更新挂载完成 -> 执行 useLayoutEffect 回调-> 浏览器 dom 绘制完成

只要是副效应,都可以使用useEffect()引入。它的常见用途有下面几种:

  • 获取数据(data fetching)
  • 事件监听或订阅(setting up a subscription)
  • 改变 DOM(changing the DOM)
  • 输出日志(logging)

注意:如果有多个副效应,应该调用多个useEffect(),而不应该合并写在一起。

使用搜索功能体验下各种状态是如何处理的,详细内容讲解,见这里[译] 如何使用 React hooks 获取 api 接口数据

主要包含:

  • 获取数据,初次自动加载,以及表单搜索也可重新发起请求
  • 使用 Loading
  • 添加错误处理
  • 添加中止数据请求,可防止切换组件,因找不到组件而触发警告
import { useState, useEffect } from 'react'
import axios from 'axios'

export default function Example() {
  const [data, setData] = useState({ items: [] })
  const [target, setTarget] = useState('javascript')
  const [isLoading, setIsLoading] = useState(false)
  const [isError, setIsError] = useState(false)
  const [url, setUrl] = useState(
    'https://api.github.com/search/repositories?sort=stars&q=javascript'
  )

  useEffect(() => {
    let didCancel = false
    const fetchData = async () => {
      setIsError(false)
      setIsLoading(true)
      try {
        const res = await axios(url)
        if (!didCancel && res?.data) {
          setData(res.data)
        }
      } catch (e) {
        if (!didCancel) {
          setIsError(true)
        }
      }
      setIsLoading(false)
    }

    fetchData()

    return () => {
      didCancel = true
    }
  }, [url])

  const handleClick = () => {
    const url = `https://api.github.com/search/repositories?sort=stars&q=${target}`
    setUrl(url)
  }

  return (
    <>
      <input
        type="text"
        value={target}
        onChange={event => setTarget(event.target.value)}
      />
      <button type="button" onClick={handleClick}>
        Search
      </button>
      {isError && <div>出错了...</div>}
      {isLoading ? (
        <div>加载中...</div>
      ) : data.items.length > 0 ? (
        <ul>
          {data.items.map(item => (
            <li key={item.id}>
              <a href={item.html_url}>{item.name}</a>
            </li>
          ))}
        </ul>
      ) : (
        <div>没有更多信息了...</div>
      )}
    </>
  )
}

为什么要在 effect 中返回一个函数?这是 effect 可选的清除机制。

React 何时清除 effect?

  • React 会在组件卸载的时候执行清除操作。
  • effect 在每次渲染的时候都会执行。这就是为什么 React 会在执行当前 effect 之前对上一个 effect 进行清除。

接下来模拟下实际项目中倒计时的写法,使用了 setTimeout 模拟 setInterval,当然也使用到了清除操作。

import { useState, useEffect } from 'react'

const STATUS = {
  STOP: 'stop',
  START: 'start',
  TIMEOUT: 1000,
  MAXTIME: 9,
}

export default function Example() {
  const [time, setTime] = useState(STATUS.MAXTIME)
  const [status, setStatus] = useState(STATUS.STOP)

  useEffect(() => {
    let timerId = null
    // 主体运行函数
    const run = () => {
      if (time <= 1) {
        setTime(STATUS.MAXTIME)
        setStatus(STATUS.STOP)
        return
      }
      setTime(time => time - 1)
      // 回调
      timerId = setTimeout(run, STATUS.TIMEOUT)
    }

    // 根据操作行为切换定时器状态
    if (status === STATUS.START) {
      timerId = setTimeout(run, STATUS.TIMEOUT)
    } else {
      timerId && clearTimeout(timerId)
    }

    // eefect 清除操作
    return () => {
      timerId && clearTimeout(timerId)
    }
  }, [status, time])

  const handleClick = e => setStatus(e.target.value)

  return (
    <div>
      <p>倒计时:{time}</p>
      <button type="button" value={STATUS.START} onClick={handleClick}>
        开始
      </button>
      <button type="button" value={STATUS.STOP} onClick={handleClick}>
        暂停
      </button>
    </div>
  )
}

useRef

特点:

  • 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数
  • 返回的 ref 对象在组件的整个生命周期内保持不变,可缓存数据
  • useRef 会在每次渲染时返回同一个 ref 对象
  • 想要在 React 绑定或解绑 DOM 节点的 ref 时运行某些代码,则需要使用回调 ref 来实现

我们在 class 中通过 ref 属性来访问 DOM,然而useRef()比 ref 属性更好用 —— useRef()可以很方便地保存任何可变值。

访问 DOM:

import { useRef } from 'react'

export default function Example() {
  const inputEl = useRef(null)

  const onButtonClick = () => {
    inputEl.current.focus() // `current` 指向已挂载到 DOM 上的文本输入元素
  }

  return (
    <div>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </div>
  )
}

缓存数据:useRef 可以第一个参数可以用来初始化保存数据,这些数据可以在 current 属性上获取到 ,当然我们也可以通过对 current 赋值新的数据源。

// 初始化
const currenRef = useRef(InitialData)
// 获取
const getCurrentData = currenRef.current
// 更改
currenRef.current = newData

封装一个 usePrevious:

import { useRef, useEffect } from 'react'
const usePrevious = value => {
  const ref = useRef()
  useEffect(() => {
    ref.current = value
  }, value)
  return ref.current
}

还可以用在定时器:

function Demo() {
  const [count, setCount] = useState(0)
  const [isClear, setClear] = useState(false)
  const timerID = useRef()

  useEffect(() => {
    timerID.current = setInterval(() => {
      setCount(count + 1)
    }, 1000)
    return () => clearInterval(timerID.current)
  }, [count])

  useEffect(() => {
    return () => clearInterval(timerID.current)
  }, [isClear])
}

由于 usestate,useReducer 执行更新数据源的函数,会带来整个组件从新执行到渲染,如果在函数组件内部声明变量,则下一次更新也会重置,那我们使用 useRef,就可以既想要保留数据,又不想触发函数的更新。

useContext

  • 接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值
  • 当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定
  • Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法

useReducer

const [state, dispatch] = useReducer(reducer, initialState, init)

特点:

  • useState 的替代方案
  • 数组的第一项就是更新之后 state 的值 ,第二个参数是派发更新的 dispatch 函数
  • dispatch 的触发会触发组件的更新
  • 使用 useReducer 还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递 dispatch 而不是回调函数

使用:

import { useReducer } from 'react'

const DECREMENT = 'decrement'
const INCREMENT = 'increment'
const RESET = 'reset'

const initialState = { number: 0 }

function reducer(state, action) {
  const { type, payload } = action
  switch (type) {
    case DECREMENT:
      return { number: state.number + 1 }
    case INCREMENT:
      return { number: state.number - 1 }
    case RESET:
      return { number: payload.number }
    default:
      return { number: state.number }
  }
}

export default function Example() {
  const [state, dispath] = useReducer(reducer, initialState)
  return (
    <div>
      当前值:{state.number}
      <button onClick={() => dispath({ type: DECREMENT })}>增加</button>
      <button onClick={() => dispath({ type: INCREMENT })}>减少</button>
      <button onClick={() => dispath({ type: RESET, payload: initialState })}>
        重置
      </button>
    </div>
  )
}

useMemo

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b])
  • 第二个参数是一个 deps 数组,数组里的参数变化决定了 useMemo 是否更新回调函数
  • 如果把 memo 比做无状态组件的 ShouldUpdate,那么 useMemo 就是更为细小的 ShouldUpdate 单元

优点

  • useMemo 可以减少不必要的循环,减少不必要的渲染
  • useMemo 可以减少子组件的渲染次数
  • useMemo 让函数在某个依赖项改变的时候才运行,这可以避免很多不必要的开销

比如我们使用防抖函数时需要这样做:

const searchDebounce = useMemo(() => {
  return debounce(handleSearch, 600)
}, [handleSearch])

这样的话,用 useMemo 包裹之后的 debounce 函数可以避免了每次组件更新再重新声明,可以限制上下文的执行。

useCallback

const memoizedCallback = useCallback(() => {
  doSomething(a, b)
}, [a, b])
  • useCallback(fn, []) 相当于 useMemo(() => fn, [])
  • 用处:当以 props 的形式传递给子组件时, 可避免非必要渲染
  • 区别: useMemo 返回的是函数运行的结果,useCallback 返回的是函数
  • useCallback ,需要搭配 react.memo 或 pureComponent 一起使用,才能使性能达到最佳

useMemo 和 useCallBack 示例

接下来的组件中,我们维护了两个 state,可以看到 getCount 的计算仅仅跟 count 有关,那么我们兵分三路,逐个了解下各自的军情。

import React, { useState, useMemo, useCallback } from 'react'

const Child = React.memo(function ({ getCount }) {
  return <h4>传过来的Count值:{getCount()}</h4>
})

export default function DemoUseMemo() {
  const [count, setCount] = useState(1)
  const [val, setValue] = useState('')

  // 普通调用
  const getCount = () => {
    console.log('normal-result')
    return count
  }

  // 使用 useMemo
  const getCountWithMemo = useMemo(() => {
    console.log('useMemo-result')
    return count
  }, [count])

  // 使用 useCallback
  const getCountWithCallback = useCallback(() => {
    console.log('useCallback-result')
    return count
  }, [count])

  return (
    <div>
      <h4>
        Count:{getCount()}, {getCountWithMemo}
      </h4>
      <Child getCount={getCountWithCallback} />
      <div>
        <button onClick={() => setCount(count + 1)}>+1</button>
        <input value={val} onChange={event => setValue(event.target.value)} />
      </div>
    </div>
  )
}

当我们点击 +1 时,打印如下:

useMemo-result
normal-result
useCallback-result

当我们输入时,打印如下,只有普通调用的打印结果:

normal-result

如上所示,普通调用时,无论是 count 还是 val 变化,都会导致 getCount 重新计算,所以这里我们希望 val 修改的时候,不需要再次计算,这种情况下我们可以使用 useMemo。

同样的,使用了 useCallback 后,结合 React.memo,显示结果和 useMemo 完全一致。如果这里值使用了 useCallback,而并未使用React.memo,结果如何呢?答案就是和普通调用结果一致。

那 useMemo 和 useCallback 到底有什么异同呢?

  • 相同:接收的参数都是一样,都是在其依赖项发生变化后才执行,都是返回缓存的值
  • 区别:useMemo 返回的是函数运行的结果,useCallback 返回的是函数。

自定义 Hook

特点:

  • 自定义 Hook 是一个函数,其名称以 “use” 开头,函数内部可以调用其他的 Hook
  • 自定义 Hook 是一种自然遵循 Hook 设计的约定,而并不是 React 的特性

规则:

  • 必须以 “use” 开头
  • 两个组件中使用相同的 Hook 不会共享 state
  • 每次调用 Hook,它都会获取独立的 state

注意:一个好用的自定义 hooks,一定要配合 useMemo, useCallback 等 api 一起使用

自定义获取数据的 Reducer Hook:

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

const REQUEST_INIT = Symbol('REQUEST_INIT')
const REQUEST_SUCCESS = Symbol('REQUEST_SUCCESS')
const REQUEST_FAILURE = Symbol('REQUEST_FAILURE')

const requestReducer = (state, action) => {
  switch (action.type) {
    case REQUEST_INIT:
      return {
        ...state,
        isLoading: true,
        isError: false,
      }
    case REQUEST_SUCCESS:
      return {
        ...state,
        isLoading: false,
        isError: false,
        data: action.payload,
      }
    case REQUEST_FAILURE:
      return {
        ...state,
        isLoading: false,
        isError: true,
      }
    default:
      return console.error('出错了')
  }
}

// 请求数据、更新数据
export function useRequest(cb, isRequest) {
  const [isUpdate, setUpdate] = useState(false)
  const [param, setParam] = useState({})

  const [state, dispatch] = useReducer(requestReducer, {
    isLoading: false,
    isError: false,
    data: '',
  })

  useEffect(() => {
    let didCancel = false
    // 如果是 -1 初次不需要请求
    if (isRequest === -1) {
      return dispatch({ type: REQUEST_SUCCESS })
    }
    const requestData = async params => {
      dispatch({ type: REQUEST_INIT })
      try {
        const res = await cb(params)
        if (!didCancel) {
          dispatch({ type: REQUEST_SUCCESS, payload: res.data })
        }
      } catch (error) {
        if (!didCancel) {
          dispatch({ type: REQUEST_FAILURE })
        }
      }
    }

    requestData(param)

    return () => {
      didCancel = true
    }
  }, [isUpdate, param])

  const isUpdateHandle = () => setUpdate(!isUpdate)
  const onUpdateHandle = param => setParam(param)

  return { ...state, isUpdateHandle, onUpdateHandle }
}

Hook 规则

  • 本质: JavaScript 函数
  • 只在最顶层使用 Hook,不要在循环,条件或嵌套函数中调用 Hook
  • 只在 React 函数中调用 Hook,不要在普通的 JavaScript 函数中调用 Hook

参考资料

yangtao2o avatar Mar 21 '21 15:03 yangtao2o