大家好,今天给大家分享一篇 React
内置 hooks
的文章。
文章比较基础,但即使对于 React
技术栈的同学,相信也会让你有所收获,特别是其中的 useDebugValue
、useId
、useTransition
、useSyncExternalStore
等比较新的 hook
,要仔细看一下哦~
以下是原文:
React
中的组件有类组件
和函数式组件
之分,函数式组件
因其简洁的语法、更小的性能消耗等优点而被越来越多的开发者所喜欢,但函数式组件
相比类组件
也有存在一些缺陷,例如无状态、没有生命周期等。
React 16.8 开始引入了hooks
的概念,它弥补了函数式组件
的缺陷,基于hooks
,函数式组件
也可以拥有状态和生命周期,在项目开发中可以进行更好的逻辑复用。hooks
基于函数式组件
而生,因此它只能在函数式组件
中使用,只能在函数的最外层调用hook,同时不能在循环、条件判断或者子函数中调用,这是使用hooks
的规则。
接下来本文将讲解React
中内置hooks
的一些概念及用法,欢迎感兴趣的读者阅读!!!
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
可用于获取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
可以弥补函数式组件没有生命周期的缺陷,他支持传入一个函数,函数里的代码会在组件挂载或更新时执行,相当于类组件的componentDidMount
和componentDidUpdate
。参数函数里支持返回一个函数,它会在清除副作用时执行,类似于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
只会在组件首次挂载和卸载时执行一次,相当于生命周期componentDidMount
和componentWillUnmount
。
日常使用中建议传入第二个参数,明确副作用执行的依赖项,以便减少不必要的性能消耗。
useLayoutEffect
与useEffect
作用相同,不同之处在于useLayoutEffect
是同步执行,useLayoutEffect
的执行时机是在DOM更新之后,浏览器绘制之前
,因此如果使用它来修改DOM布局会更友好一点,因为它是在浏览器绘制之前执行,相比useEffect
在浏览器绘制之后执行,可以减少浏览器的回流和重绘。由于useLayoutEffect
是同步的,所以它会阻塞页面渲染,所以需根据场景谨慎使用。
useInsertionEffect
的执行时机早于useEffect
和useLayoutEffect
,它是在DOM更新之前触发,可用它在读取DOM布局之前插入样式,常用于css-in-js之类的第三方库。
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
提供了类似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
同样接收一个回调函数和依赖项数组,返回值是一个函数,执行该函数可以得到回调函数里所返回的值。值得注意的是如果依赖项没有发生改变,回调函数会被缓存。
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
与useCallback
类似,不同之处在于useCallback
返回的是函数,useMemo
返回的是函数的运行结果。
const [count, setCount] = useState(0)
const doubleCount = useMemo(() => {
return count * 2
}, [count])
useDebugValue
可以方便开发者在React DevTools工具中调试输出自定义hook的值。
const useCustom = () => {
useDebugValue('哈哈哈')
}
export default function App() {
const [count, setCount] = useState(0)
useCustom()
return (
<div></div>
)
}
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
可以搭配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直接调用子组件的方法是无效的,接下来我们只需使用useImperativeHandle
和forwardRef
改造子组件的代码便可解决这个问题。
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} />
})
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
一般是第三方状态管理库使用,它可以关联外部的数据源,当外部数据源发生改变时可以触发视图更新。
// 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
也是一个函数,返回外部数据值的快照,当数据发生变化时会重新渲染。
再给我们的辅导服务打个广告,我们目前有面试全流程辅导、简历指导、模拟面试、零基础辅导和付费咨询等增值服务,大厂前端专家一对一辅导。
辅导服务推出了近 2 年的时间,已助力超过 200 + 的同学找到心仪的工作,感兴趣的伙伴可以联系小助手(微信号:interview-fe2)了解详情哦~
原文作者:狂砍2分4篮板
原文链接:https://juejin.cn/post/7244351764458356797