likes
comments
collection
share

React内置hooks一览

作者站长头像
站长
· 阅读数 17

React中的组件有类组件函数式组件之分,函数式组件因其简洁的语法、更小的性能消耗等优点而被越来越多的开发者所喜欢,但函数式组件相比类组件也有存在一些缺陷,例如无状态、没有生命周期等。

React 16.8 开始引入了hooks的概念,它弥补了函数式组件的缺陷,基于hooks函数式组件也可以拥有状态和生命周期,在项目开发中可以进行更好的逻辑复用。hooks基于函数式组件而生,因此它只能在函数式组件中使用,只能在函数的最外层调用hook,同时不能在循环、条件判断或者子函数中调用,这是使用hooks的规则。

接下来本文将讲解React中内置hooks的一些概念及用法,欢迎感兴趣的读者阅读!!!

useState

useState用来描述状态以及状态更新,它使得函数式组件可以像类组件一样拥有状态,通过它更新数据可以使视图更新。

import { useState } from 'react'

export default function App() {
  const [count, setCount] = useState(0)
  
  const handleClick = () => {
    setCount(count + 1)
  }
  
  return (
    <>
      <p>{count}</p>
      <button onClick={handleClick}> + </button>
    </>
  )
}

useState函数接收一个参数作为状态的默认值,函数返回结果为一个数组,数组第一项为状态值,第二项是一个可以变更状态值的函数,日常开发中我们常采用数组解构的形式,这样有利于更好的代码阅读,像这样[xxx, setXxx]

set更新函数的第一个参数可以直接传值进行赋值变更,这也是比较常用的一种方式。除此之外,还可以通过传入一个函数进行值的更新。

setCount(count + 1) // 第一种方式
setCount(prevState => prevState + 1) // 第二种方式

值得注意的是,set更新函数是异步的。

setCount(count + 1) // 更新为1
console.log(count) // 打印的count仍然是0

useRef

useRef可用于获取DOM节点,返回值是一个对象,值存在该对象的current属性里。

import { useRef } from "react"

export default function App() {
  const inputRef = useRef(null)
  
  const handleClick = () => {
    // 获取input输入框并聚焦
    inputRef.current.focus()
  }
  
  return (
    <>
      <input ref={inputRef}></input>
      <button onClick={handleClick}>按钮</button>
    </>
  )
}

此外,useRef也可以跟useState一样用来保存状态。

import { useRef } from 'react'

export default function App() {
  const ref = useRef(0)
  
  const handleClick = () => {
    ref(ref.current + 1)
  }
  
  return (
    <>
      <p> {ref.current} </p>
      <button onClick={handleClick}> + </button>
    </>
  )
}

跟 Vue3 的 ref 可以说是很像了!!!

useEffect

useEffect可以弥补函数式组件没有生命周期的缺陷,他支持传入一个函数,函数里的代码会在组件挂载或更新时执行,相当于类组件的componentDidMountcomponentDidUpdate。参数函数里支持返回一个函数,它会在清除副作用时执行,类似于componentWillMount

import { useEffect } from 'react'

useEffect(() => {
  // console.log('开启定时器')
  const timer = setInterval(() => {
    // ...
  }, 1000)

  return () => {
    console.log('清除定时器')
    clearInterval(timer)
  }
})

useEffect的第二个参数是一个数组,当数组里的项发生改变时会重新触发执行effect函数里的代码,类似于Vue里的watch,如果存在上一次执行的副作用,则会先执行返回函数里的代码再执行effect函数里的代码。

export default function App() {
  const [count, setCount] = useState(0)
  
  const handleClick = () => {
    setCount(count + 1)
  }

  useEffect(() => {
    console.log('count===', count)
    
    return () => {
      console.log('卸载')
    }
  }, [count])

  return (
    <>
      <button onClick={handleClick}> + </button>
    </>
  )
}

组件首次加载时打印内容为count===0,当触发setCount改变count的值时,会重新触发执行useEffect里的内容,此时打印内容为卸载 => count===1

如果第二个参数的数组设置为空,useEffect(() => {}, []), useEffect只会在组件首次挂载和卸载时执行一次,相当于生命周期componentDidMountcomponentWillUnmount

日常使用中建议传入第二个参数,明确副作用执行的依赖项,以便减少不必要的性能消耗。

useLayoutEffect

useLayoutEffectuseEffect作用相同,不同之处在于useLayoutEffect是同步执行,useLayoutEffect的执行时机是在DOM更新之后,浏览器绘制之前,因此如果使用它来修改DOM布局会更友好一点,因为它是在浏览器绘制之前执行,相比useEffect在浏览器绘制之后执行,可以减少浏览器的回流和重绘。由于useLayoutEffect是同步的,所以它会阻塞页面渲染,所以需根据场景谨慎使用。

useInsertionEffect

useInsertionEffect的执行时机早于useEffectuseLayoutEffect,它是在DOM更新之前触发,可用它在读取DOM布局之前插入样式,常用于css-in-js之类的第三方库。

useContext

useContext一般用于组件之间的数据传递,方便开发者获取父级组件的传值。useContext接收一个由createContext创建返回的context参数。

第一步:手动创建一个context

// context.js
import { createContext } from 'react'
export const AppContext = createContext('')

第二步:使用context包裹组件并提供数据供子组件使用

// 父级组件
import { AppContext } from './context.js'
import Child from './child.jsx'

export default function App() {
  return (
    <AppContext.provider value='张三'>
      <Child />
    </AppContext.provider>
  )
}

第三步:子组件使用useContext获取数据

// child.jsx
import { AppContext } from './context.js'
import { useContext } from 'react'

export default function Child() {
  const name = useContext(AppContext)
  return (
    <div>{ name }</div>
  )
}

useContext会从使用它的组件开始向上查找离它最近的provider。如下面代码中子组件得到的值为李四。

<AppContext.provider value='张三'>
  <AppContext.provider value='李四'>
    <Child />
  </AppContext.provider>
</AppContext.provider>

useReducer

useReducer提供了类似redux的功能,它可以像useState一样让我们轻松的管理状态。

import { useReducer } from "react"

export default function App() {
  const reducer = (state, action) => {
    switch (action.type) {
      case 'add':
        return [
          ...state,
          action.payload
        ]
        break;
      case 'clear':
        return []
        break;
      default:
        return state
        break;
    }
  }
  const [state, dispatch] = useReducer(reducer, ['李四'])

  return (
    <>
      <button onClick={() => dispatch({ type: 'add', payload: '张三' })}>加</button>
      <button onClick={() => dispatch({ type: 'clear' })}>清除</button>
    </>
  )
}

useReducer第一个函数需要传入一个reducer函数,第二个参数用于设置状态的默认值。返回值是一个数组,包含状态和派发函数。reducer函数里的两个参数分别是state即最新的状态值和action,action为派发函数dispatch所传递的值。

useReducer还可以接收第三个参数,第三个参数是一个函数,函数的返回值会被用作状态的初始值,设置了第三个参数则第二个参数的设置无效。

const [state, dispatch] = useReducer(reducer, '张三', (init) => 'hello,' + init)
console.log(state) // hello,张三

useCallback

useCallBack同样接收一个回调函数和依赖项数组,返回值是一个函数,执行该函数可以得到回调函数里所返回的值。值得注意的是如果依赖项没有发生改变,回调函数会被缓存。

const [count, setCount] = useState(0)
const getValue = useCallback(() => {
  const value = count + 1
  return value
}, [])

const value = getValue()
console.log(value) // 每次输出的结果都是1

export default function App() {
  return (
    <>
      <button onClick={() => setCount(count + 1)}>按钮</button>
    </>
  )
}

点击按钮后上面输出的结果都是1,因为依赖项为空或者依赖项没有发生改变,即使count发生改变,useCallback里的count都是取缓存里的值,即一开始的0,所以结果总是1。

const getValue = useCallback(() => {
  return count + 1
}, [count])

const value = getValue()
console.log(value) // count + 1

将count设置为依赖项,此时输入结果就是最新的count+1了。

useCallBack的一个主要作用是可以缓存子组件,减少子组件的重新渲染。

import { useState, memo } from 'react'

const Child = memo(({ getMessage }: any) => {
  getMessage('hello')
  console.log('子组件')
  return <div></div>
})

export default function RefundOrder() {
  const [count, setCount] = useState(0)
  const getMessage = (message) => {
    console.log(message)
  }
  return (
    <>
      <Child getMessage={getMessage} />
      <button onClick={() => setCount(count + 1)}>按钮</button>
    </>
  )
}

memo的作用是保护子组件在父组件的props发生改变并且这些props与自己无关时能够不受其影响从而减少进行不必要的渲染。但在上面这个代码示例中,子组件只是接收一个函数并没有其他接收的状态,当父组件点击按钮更新count重新渲染时,子组件也会跟着重新渲染,因为React.memo检测的是props中数据的栈地址是否改变,当父组件重新构建时,父组件中的函数也会被重新构建,函数地址发生改变从而子组件的重新渲染,所有上面代码中每点击一次按钮都会打印出"子组件"。此时就可以用useCallback来优化这个问题。

const getMessage = useCallback((message) => {
  console.log(message)
}, [])

在父组件用useCallback包裹函数后,子组件就不会重复渲染了。

useMemo

useMemouseCallback类似,不同之处在于useCallback返回的是函数,useMemo返回的是函数的运行结果。

const [count, setCount] = useState(0)

const doubleCount = useMemo(() => {
  return count * 2
}, [count])

useDebugValue

useDebugValue可以方便开发者在React DevTools工具中调试输出自定义hook的值。

const useCustom = () => {
  useDebugValue('哈哈哈')
}

export default function App() {
  const [count, setCount] = useState(0)
  useCustom()
  return (
    <div></div>
  )
}

React内置hooks一览

useId

useId还是没有参数,返回结果是一个唯一、稳定的ID。

在SSR(服务端渲染中),React组件会渲染成一个字符串,字符串再以html的形式传送得到客户端,到了客户端React还会对组件重新激活渲染。假设我们为某个组件设置一个Math.random()的随机ID,一开始可能是0.123,到客户端渲染时可能变成了0.456。useId生成的ID唯一且稳定便可以解决这种问题。

const id = useId()

return (
  <>
    <label htmlFor={id}>名字:</label>
    <input id={id} type="text" />
  </>
)

useImperativeHandle

useImperativeHandle可以搭配forwardRef在使用ref时暴露子组件的内容供父组件使用。

import { useRef, useImperativeHandle } from 'react'

const Child = () => {
  const inputRef = useRef(null)
  // 使输入框聚焦
  const focusInput = () => {
    inputRef?.current?.focus()
  }
  return <input ref={inputRef} />
}

export default function App() {
  const childRef = useRef(null)
  const handleClick = () => {
    // 调用子组件方法
    childRef?.current.focusInput()
  }

  return (
    <>
      <Child ref={childRef} />
      <button onClick={handleClick}>按钮</button>
    </>
  )
}

上面代码中,父组件通过ref直接调用子组件的方法是无效的,接下来我们只需使用useImperativeHandleforwardRef改造子组件的代码便可解决这个问题。

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

const Child = forwardRef((props, ref) => {
  const inputRef = useRef(null)
  // 使输入框聚焦
  const focusInput = () => {
    inputRef?.current?.focus()
  }
  
  // 暴露focusInput方法出去
  useImperativeHandle(ref, () => {
      return { focusInput }
  })
  
  return <input ref={inputRef} />
})

useTransition

const [isPending, startTransition] = useTransition()

useTransition没有参数,返回值是一个数组,数组的两个值一个表示过渡状态的标识,另一个是一个函数,该函数可以传入一个回调函数用来降低任务执行的优先级。

import { useState, useTransition } from 'react'

export default function App() {
  const [value, setValue] = useState('')
  const [list, setList] = useState<string[]>([])
  const [pending, startTransition] = useTransition()

  const onChange = (e) => {
    setValue(e.target.value)
    startTransition(() => {
      setList(['张三', '李四'])
    })
  }

  return (
    <>
      <input value={value} onChange={onChange} />
      <ul>
        { pending && <div>loading...</div> }
        {
          list.map((item, index) => {
            return <li key={index}>{ item }</li>
          })
        }
      </ul>
    </>
  )
}

在输入框输入时,我们首先保障变更输入框的值,使得所输即所见,然后将列表数据变更的操作放进startTransition里,降低其执行优先级,这在一些数据量大的场景下可以提升用户体验,避免大量的数据渲染导致输入框卡顿。

useSyncExternalStore

useSyncExternalStore一般是第三方状态管理库使用,它可以关联外部的数据源,当外部数据源发生改变时可以触发视图更新。

// usersStore.js
let users = []
let listeners = []

export const userStore = {
  addUser(name) {
    todos = [...users, name]
    emitChange()
  },
  subscribe(listener) {
    listeners = [...listeners, listener]
    return () => {
      listeners = listeners.filter(l => l !== listener)
    }
  },
  getSnapshot() {
    return users
  }
}

function emitChange() {
  for (let listener of listeners) {
    listener()
  }
}

import { useSyncExternalStore } from 'react'
import { userStore } from './userStore.js'

export default function App() {
  const users = useSyncExternalStore(userStore.subscribe, userStore.getSnapshot)
  return (
    <>
      <button onClick={() => userStore.addUser('张三')}>Add user</button>
      <ul>
        {users.map((item, index) => (
          <li key={index}>{item}</li>
        ))}
      </ul>
    </>
  )
}

第一个参数subscribe是一个订阅函数,React会传入一个listener,当数据发生改变时需要调用listener,同时subscribe需要返回一个取消订阅的函数。

第二个参数getSnapshot也是一个函数,返回外部数据值的快照,当数据发生变化时会重新渲染。

感谢

最后的最后,感谢您的阅读,比心!!!