ReactHooks使用

438 阅读10分钟

介绍

16.8版本后,出现hooks的概念,react组件的开发发生了非常大的变化。
使用过hooks的人相信都不会再回到class了。
本文将介绍React的各个hooks的一些简单使用demo

开始

这里介绍的是当前react18版本的相关hooks的简单使用。
包括

useState、useReducer、useEffect、useLayoutEffect、useRef、useImperativeHandle、useCallback、useMemo、useContext、useId、useInsertionEffect、useTransition、useDeferredValue、useDebugValue、useSyncExternalStore

useState

组件state,和class组件的state一样

import { useState } from 'react' 

const App = () => {
  // 第一个值是state的值,第二个值是修改state的方法
  // 参数是state的初始值,也可以像下面这样支持回调的形式
  const [ current, setCurrent ] = useState(0)
  const [ callbackCurrent, setCallbackCurrent ] = useState(() => Math.ceil(Math.random() * 100))

  const handleClick = (type) => {
    switch(type) {
      case 'normal':
        // 直接传参改就行
        setCurrent(current + 1)
      case 'callback':
        // 回调形式是把上一个值当做参数,回调的返回值即是新的state 
        setCurrent(prev => prev + 1)
    }
  }

  return (
    <div>
      <button onClick={() => handleClick('normal')}>current ++</button>
      <button onClick={() => handleClick('callback')}>current callback ++</button>
    </div>
  )
}

回调和直接传值的区别是,直接传值可能会有更新合并的情况,而回调不会出现合并。
并且回调的形式在某些hooks中不想将state作为依赖项时,可以使用回调来避免在依赖项中添加state

useReducer

类似于useState的状态管理,但是它能避免像state状态过多,分散的问题。
比如

import { useState } from 'react'

const App = () => {
  // 多个状态难于管理  
  const [ username, setUsername ] = useState('name')
  const [ email, setEmail ] = useState('email')
  const [ password, setPassword ] = useState('password')

  return (
    <div>
      {username}
      {email}
      {password}
    </div>
  )
}

此时可以使用useReducer代替useState

import { useReducer } from 'react'

// state 为 对应的值  
// action 为 下面dispatch传递的参数,用于识别相应的操作  
// 返回值是新的state值  
function reducer(state, action) {
  switch(action.type) {
    case 'increase':
      return {
        ...state,
        count: state.count + 1
      }
    case 'decrease':
      return {
        ...state,
        count: state.count - 1
      }
  }
  return state 
}

// 初始值
const initialState = {
  count: 0,
  key: 'key'
}

const App = () => {
  // 第一个参数是 处理state 更新的回调
  // 第二个参数 初始值state  
  // 第三个可选参数,用于处理默认初始值的回调,参数为上面第二个参数,比如像下面的回调这样处理
  /*
    // 初始值会新增一个extraValue属性  
    function init(initialState) {
      return {
        ...initialState,
        extraValue: 0
      }
    }
  */
  const [ state, dispatch ] = useReducer(reducer, initialState)
  const { count } = state 

  const handleClick = () => {
    // 触发count + 1
    dispatch({ type: 'increase' })
  }

  return (
    <div onClick={handleClick}>
      {count}
    </div>
  )
}

useEffect

副作用
简单来说就是根据依赖项的改变来触发对应的回调。

import { useEffect, useState } from 'react'

const App = () => {

  const [ current, setCurrent ] = useState(0)

  // 初始化时会自动触发一次回调
  // 之后会在每次依赖项发生变化时,触发回调
  useEffect(() => {
    console.log(`current is changed: ${current}`)
  }, [current])

  // 不传第二个参数依赖项时,则表示每一次刷新都会触发回调
  useEffect(() => {
    console.log('每一次渲染都会调用')
  })

  // 传递空的第二参数时,则只有在初始化的时候会触发一次
  useEffect(() => {
    console.log('只会在初始化的时候调用一次')
  }, [])

  // 回调是支持函数作为返回值的,默认会在每一次调用回调前触发
  // 当然初始化时不会触发
  // 最后组件卸载的时候也会触发一次 相当于是class 的 componentWillUnmount 
  // 比如下面这个useEffect,假设current 从 0 改变成 1 
  // 那么日志从初始化开始显示的内容就是如下的顺序
  /*
    current is changed: 0
    
    current is changed prev: 0
    current is changed: 1
  */
  // 相当于就是拿上一次调用回调,返回的那个函数在下一次触发前执行
  // 所以返回的那个回调拿到的是上一次的 state 
  useEffect(() => {
    console.log(`current is changed: ${current}`)
    return () => {
      console.log(`current is changed prev: ${current}`)
    }
  }, [current])

  return (
    <div onClick={() => setCurrent(prev => prev + 1)}>
      set current state 
    </div>
  )

}

useEffect可以做很多事情,比如

  • 可以在此处调用接口获取数据
  • 绑定事件
  • 清除定时器
    等等

useLayoutEffect

类似于useEffect
不同之处在于

useEffect 是异步的,组件会先render一次,不会阻塞渲染
useLayoutEffect 是同步的,相当于classcomponentDidMount

所以不要把一些费时的逻辑放到useLayoutEffect
或者如果需要在渲染前对dom进行修改,可在此hook中进行

import { useLayoutEffect } from 'react'

const App = () => {

  const [ current, setCurrent ] = useState(0)

  useLayoutEffect(() => {
    // 初始化时,模拟数据改变
    // 如果是使用useEffect,会发现有一个current从0到一个随机数的跳闪的过程  
    // 使用此hook则不会发生这样的情况,因为他会先改变了值之后才render  
    if(current == 0) {
      setCurrent(Math.random())
    }
  }, [current])

  return (
    <div>
      {current}
    </div>
  )

}

useRef

引用值,相当于是一个改变值不会触发刷新的值

借用ahooks中的一个自定义hook(useGetState)

  import { useRef, useState, useCallback, useEffect } from 'react'

  function useGetState(initial) {
    const [ value, setValue ] = useState(initial)

    // 创建一个 ref,初始值为value 
    const valueRef = useRef(value)
    // 每次value 更新就赋值到ref 
    // 赋值是赋值在ref.current上
    valueRef.current = value 

    // 暂时忽略useCallback
    // 相当于就是一个函数返回了ref
    const getValue = useCallback(() => valueRef.current)

    return [
      value,
      setValue,
      getValue
    ]

  }

  function App() {
    const [ current, setCurrent, getCurrent ] = useGetValue(0)
    const [ another, setAnother ] = useState(0)

    useEffect(() => {
      console.log(`我可以在不添加依赖项的同时,拿到current的最新值:${getCurrent()}`)
    }, [another])

    return (
      <div onClick={() => setAnother(prev + 1)}>
        {current}
      </div>
    )
  }

也可以配合useImperativeHandleforwardRef使用,具体例子可以看下面的useImperativeHandle

useImperativeHandle

自定义暴露ref所暴露的属性

我们都知道,函数组件是没有办法直接通过ref来获取到实例的。
但是我们可以通过forwardRef来包裹组件来达到获取到对应组件的实例。

import { useRef, useImperativeHandle, forwardRef, useState } from 'react'

// 通过forwardRef包裹组件
// 组件会新增第二参数ref
// 可以将ref直接赋值到当前组件下的element上,也可以达到属性传递的效果(比如将ref绑定到input上,则外部组件可以通过ref调用input组件上的一些方法,比如focus、click)
const Child = forwardRef((props, ref) => {

  const [ value, setValue ] = useState(0)

  // 将ref作为useImperativeHandle的第一参数,简单理解就是把新增的属性绑定到ref上 
  // 第二参数是一个回调,返回值是一个对象,用于自定义暴露给外面能访问的属性  
  // 第三参数是一个依赖项,表示当依赖项里的值发生改变时,重新调用第二参数的回调来刷新暴露的值  
  useImperativeHandle(ref, () => {
    return {
      value
    }
  }, [value])

  return (
    <div onClick={() => setValue(prev => prev + 1)} >set value: {value}</div>
  )

})

const App = () => {
  const ref = useRef()

  const handleClick = () => {
    console.log(`当前子组件的ref状态值是: ${ref.current}`)
  }

  return (
    <div>
      <div onClick={handleClick}>获取子组件的状态</div>
      <Child ref={ref} />
    </div>
  )

}

useCallback

缓存函数
依赖项不改变的情况下,re-render不会重复创建。

import { useCallback, useEffect } from 'react'

function App() {

  const doSomething = useCallback(() => {

  }, [])

  useEffect(() => {
    // 只有当doSomething发生改变时,才会重新调用该方法
    doSomething()
  }, [doSomething])

  return (
    <div>useCallback demo</div>
  )

}

useMemo

缓存
useCallback功能类似,但是缓存的是值。

import { useMemo } from 'react'

function App(props) {

  const { value } = props 

  // 只有value改变时,才会重新计算
  // 当存在很复杂的计算逻辑时,可以通过useMemo来缓存计算结果,避免重复计算   
  const cacheValue = useMemo(() => {
    return value + 1
  }, [value])

  return (
    <div>useMemo demo</div>
  )

}

useContext

全局状态控制。
平时可能碰到过跨层级的状态传递或者兄弟间的状态传递的情况,比如下面这样

import { useState } from 'react'

const GrandFather = () => {
  const [ state, setState ] = useState(0)
  return (
    <Parent state={state} />
  )
}

const Parent = (props) => {

  const { state } = props 
  const [ broState, setBroState ] = useState(0)

  return (
    <>
      <Son state={state} broState={broState} />
      <Brother setBroState={setBroState} />
    </>
  )

}

const Son = (props) => {
  const { state, broState } = props 
  return (
    <div>{state}{broState}</div>
  )
}

const Brother = (props) => {

  const { setBroState } = props 

  return (
    <div></div>
  )
}

  • Son组件需要GrandFatherstate属性,只能通过Parent组件向下传递,但是Parent组件完全不需要依赖state属性。
  • Son组件需要来自其兄弟组件Brother的属性broState,也只能通过将状态设置在Parent组件上来进行传递。

此时可以使用useContext创建全局的状态,用于在各个地方进行状态共享。
一般配合createContext来进行使用。

import { useContext, createContext } from 'react'

// 创建全局的状态
const Context = createContext()

const GrandFather = () => {
  const [ state, setState ] = useState(0)
  const [ broState, setBroState ] = useState(0)

  // 包裹最外层组件
  return (
    <Context.Provider
      value={{
        state,
        broState
      }}
    >
      {/*这里就不再需要状态透传了*/}
      {/*<Parent state={state} />*/}
      <Parent />
    </Context.Provider>
  )
}

const Parent = (props) => {

  // 无须无用状态
  // const { state } = props 
  // const [ broState, setBroState ] = useState(0)

  return (
    <>
      <Son />
      <Brother />
    </>
  )

}

const Son = (props) => {
  // 使用useContext接收全局的状态
  const { state, broState } = useContext(Context) 

  return (
    <div>{state}{broState}</div>
  )
}

const Brother = (props) => {

  return (
    <div></div>
  )
}

需要注意的是,Context中的任何一个属性的改变,都会导致子组件的重新渲染,包括那些本身并不依赖该属性的使用useContext的组件,这在大型项目中会造成性能的问题,所以使用时还需斟酌。
可以使用github上的解决方案来避免该问题(use-context-selector)。

useId

  • 简单来说就是为了让服务端客户端生成的id保持一致。
  • 或者说可以保证在一个组件中使用一个随机id,而组件多次渲染,保证组件的id的唯一性。

具体的描述可以查看这篇文章

import { useId } from 'react'

const RandomIdComp = () => {
  const id = useId()
  return (
    <div>{id}</div>
  )
}

const App = () => {
  return (
    <div>
      {/*两次生成的id是不一样的*/}
      <RandomIdComp />
      <RandomIdComp />
    </div>
  )
}

useInsertionEffect

类似于useEffectuseLayoutEffecthook
它的执行时机比useLayoutEffect更早,它执行时,Dom还未更新。

它的唯一作用就是解决CSS-in-JS在渲染中注入样式的性能问题。

import { useInsertionEffect } from 'react'

const App = () => {

  useInsertionEffect(()=>{
     /* 动态创建 style 标签插入到 head 中 */
     const style = document.createElement('style')
     style.innerHTML = `
       .css-in-js{
         color: red;
         font-size: 20px;
       }
     `
     document.head.appendChild(style)
  },[])

  return <div className="css-in-js">useInsertionEffect </div>
}

useTransition

帮助state更新,不会阻塞渲染。
当出现有那种耗性能的更新时,通过useTransition包裹更新任务,它会为更新创建过渡状态isPending,提升用户体验。

比如在这样一个场景
用户在百度搜索的时候,输入框在输入的同时,也会同时根据输入内容调用接口查询数据。
网络请求本身就是一个费时的行为,如果同时页面还有其他的行为,可能会阻塞渲染,体验不好。
此时可以通过useTransition延迟更新状态(React本身有自己的一套更新规则,会把一些更新列表划分优先级)。

import { useTransition, useState, useCallback } from 'react'

const dataSource = new Array(1000).fill(0).map(item => Math.random())

const List = (props) => {
  const { value, isPending } = props 

  const [ searchResult, setSearchResult ] = useState([])

  // 根据延迟状态进行数据筛选
  // 模拟网络请求
  useEffect(() => {
    setSearchResult(dataSource.filter(item => Math.random() > 0.7))
  }, [value])

  return (
    <div>
      <div>当前值({value})</div>
      <div>isPending?{isPending.toString()}</div>
      {
        searchResult.map((item, index) => {
          return (
            <div key={item}>{index + 1}: {item}</div>
          )
        })
      }
    </div>
  )
}

const App = function () {
  
  const [ value, setValue ] = useState()
  // 第一个值 是否处于pending 
  // 第二个值 将更新通过此函数包裹达到对应的过渡效果
  const [isPending, startTransition] = useTransition()

  const onChange = useCallback((e) => {
    // 包裹更新状态
    startTransition(() => {
      setValue(e.target.value)
    })
  }, [])

  return (
    <div>
      {/*当用户输入内容时,改变value*/}
      <input value={value} onChange={onChange} />
      {/*筛选列表依赖了 延迟状态*/}
      <List value={value} isPending={isPending} />
    </div>
  )
};

useDeferredValue

一个可以允许延迟更新的state值。
相当于就是使用上面的useTransition做了一个简单的封装

接着使用上面的useTransitiondemo做一下简单修改

import { useDeferredValue, useState } from 'react'

const dataSource = new Array(1000).fill(0).map(item => Math.random())

const List = (props) => {
  const { value } = props 

  const [ searchResult, setSearchResult ] = useState([])
  const [ dealCount, setDealCount ] = useState(0)

  // 根据延迟状态进行数据筛选
  // 模拟网络请求
  useEffect(() => {
    setDealCount(prev => prev + 1)
    setSearchResult(dataSource.filter(item => Math.random() > 0.7))
  }, [value])

  return (
    <div>
      <div>我执行了{dealCount}次</div>
      <div>当前值({value})</div>
      {
        searchResult.map((item, index) => {
          return (
            <div key={item}>{index + 1}: {item}</div>
          )
        })
      }
    </div>
  )
}

const App = function () {
  
  const [ value, setValue ] = useState()
  // 将 value 作为一个 延迟状态
  const deferredValue = useDeferredValue(value)

  return (
    <div>
      {/*当用户输入内容时,改变value*/}
      <input value={value} onChange={e => setValue(e.target.value)} />
      {/*筛选列表依赖了 延迟状态*/}
      <List value={deferredValue} />
    </div>
  )
};

实际在浏览器运行上面的代码时,可能看不出啥,但是当修改cpu性能时,就能明显发现下方列表的展示出现卡顿,但是没有影响到上面输入框的用户输入行为。

useDeferredValue-cpu.png

可能你会发现他和防抖的效果类似,但是正如上面所说,本身执行上面的demo并没有发现什么异样,这就是他和防抖的区别。

防抖是不管计算机性能好坏,都会延迟更新。
useDeferredValue会自动根据计算机性能调整state更新的优先级。

useDebugValue

顾名思义是一个偏向于调试性的hook,它一般使用于React开发者工具中,在编码自定义hook时,用于实时观察状态的变化。

import { useState, useDebugValue } from 'react'

function useDebug() {

  const [ value, setValue ] = useState(0)

  // 接收两个参数
  // 第一个是观察状态的值
  // 第二个用于对观察的状态展示进行一个格式化,参数即为对应的状态
  useDebugValue(value, (value) => {
    const realValue = parseInt(value)
    if(Number.isNaN(realValue)) return '不是数字'
    return realValue % 2 === 0 ? '偶数' : '奇数'
  })

  return [
    value,
    setValue 
  ]

}

function App() {

  // 使用hook 
  const [ value, setValue ] = useDebug()
  
  return (
    <div onClick={() => setValue(prev => prev + 1)}>
      set current value: {value}
    </div>
  )

}

useDebugValue.jpg

从图上可以看出,上面的hook(useDebug)已经被开发者工具接收到。
注意上面的名称是Debug,因为它会自动将useDebug的前缀use当做是hook的标志,如果hook不是以use开头,那么他就是hook的全名(比如usDebug名称就是usDebug)。

useSyncExternalStore

简单来说主要服务于第三方库。
快速接入外部的数据源,比如redux、`zustand等。
并且他的功能也远不止于此(以后再探究吧)。

import React, { useSyncExternalStore, useState, useEffect } from 'react@18';

let store = {
  name: 'Daniel',
  age: 20
}
let listeners = []

function reducer(type) {
  switch(type) {
    case 'INCREASE':
      store.age ++
      break 
    case 'DECREASE':
      store.age --
      break
    default:
  }
  console.log(store.age)
  listeners.forEach(listener => listener())
}

// 订阅函数
// 参数是一个callback 用于数据发生变化时调用触发
// 返回回调用于清理订阅函数  
// 比如下面的用于清理调指定的callback
function subscribe(callback) {
  listeners.push(callback)
  return () => {
    listeners = listeners.filter(item => item != callback)
  }
}

// 返回最新的 state 状态
// 并且是不可变的值
function getSnapshot() {
  return store.age 
}

function getServerSnapshot() {

}

const App = function () {

  // subscribe 表示订阅函数,当数据发生变化时,触发此函数
  // getSnapshot 用于对数据发生变化时,返回对应新的 state状态
  // getServerSnapshot 则是用于在 ssr 模式下 的 getSnapshot(有待研究)
  const state = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)

  return (
    <div onClick={() => reducer(Math.random() > 0.5 ? 'INCREASE' : 'DECREASE')}>
      {store.name}:{state}
    </div>
  )
};

结束

以上就是当前版本所有hooks的使用demo,如有错误欢迎指正。

参考
useEffect 和 useLayoutEffect
useReducer就是useState的升级版
useDeferredValue 与 debounce
useDeferredValue 与 debounce
useDeferredValue-example
为了生成唯一id,React18专门引入了新Hook:useId
「React 进阶」 React 全部 Hooks 使用大全 (包含 React v18 版本 )
React Concurrent 模式抢先预览下篇: useTransition 的平行世界
React useSyncExternalStore 一览