ReactHooks

react-hooks

Hook 是 React16.8 的新特性,Hook 使你在无需修改组件结构的情况下复用状态逻辑。

弥补了 functin Component 没有实例没有生命周期的问题,react 项目基本上可以全部用 function Component 去实现了。

hook 总览

常用的官方的 hook 主要是下面几个:

  • useState()
  • useReducer()
  • useContext()
  • useRef()
  • useImperative()
  • useEffect()
  • useLayoutEffect()
  • useMemo()
  • useCallback()

hook 基础

hook 使用规则:

  • 不要在循环,条件,或者嵌套函数中调用 hook,在最顶层使用 hook
  • 不能在普通函数中调用 hook

下面主要记录每个 hook 基础的用法,

useState

存取数据的一种方式,对于简单的 state 适用,复杂的更新逻辑的 state 考虑使用 useReducer

使用:

1
const [state, setState] = useState(initState)

更新:

1
2
setState(newState)
setState(state => newState) // 函数式更新

setState 是稳定的,所以在一些 hook 依赖中可以省略

useReducer

useState 的一种代替方案,当 state 的处理逻辑比较复杂的时候,有多个子值得时候,可以考虑用 useReducer

使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
const initialState = { count: 0 }

function reducer(state, action) {
switch (action.type) {
case 'increment':
return { ...state, count: state.count + 1 }
case 'decrement':
return { ...state, count: state.count - 1 }
default:
throw new Error()
}
}
const [state, dispatch] = useReducer(reducer, initialState)

如果有第三个参数,则第三个参数为一个函数,接受第二个参数的值作为参数,返回初始值。

dispatch 是稳定的,所以在一些 hook 依赖中可以省略

useContext

1
const value = useContext(MyContext)

接受一个 context 对象,并返回该 context 对象的当前值,配合 context 使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
const themes = {
light: {
foreground: '#000000',
background: '#eeeeee',
},
dark: {
foreground: '#ffffff',
background: '#222222',
},
}

const ThemeContext = React.createContext(themes.light)

function App() {
return (
<ThemeContext.Provider value={themes.dark}>
<Toolbar />
</ThemeContext.Provider>
)
}

function Toolbar(props) {
return (
<div>
<ThemedButton />
</div>
)
}

function ThemedButton() {
const theme = useContext(ThemeContext)
return (
<button style={{ background: theme.background, color: theme.foreground }}>I am styled by theme context!</button>
)
}

useRef

1
const refContainer = useRef(initValue)

返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内持续存在。

  • 访问 dom 的一个方式
  • 可以将其作为一个值来使用,在每次渲染时都返回同一个 ref 对象
  • 改变其 ref 的值,不会引起组件的重新渲染

例子 1,访问 dom 的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
function TextInputWithFocusButton() {
const inputEl = useRef(null)
const onButtonClick = () => {
// `current` 指向已挂载到 DOM 上的文本输入元素
inputEl.current.focus()
}
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
)
}

例子 2,作为一个对象来保存值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import { useEffect, useRef, useState } from 'react'

function App() {
const [count, setCount] = useState(0)
// 记录定时器,方便可以随时停止计时器
let timer = useRef(null)
useEffect(() => {
timer.current = setInterval(() => {
console.log(1)
setCount(count => count + 1)
}, 1000)
return () => {
clearInterval(timer.current)
timer.current = null
}
}, [])

const stop = () => {
if (timer.current) {
clearInterval(timer.current)
timer.current = null
}
}
return (
<div>
<span>{count}</span>
<button onClick={stop}>stop</button>
</div>
)
}

export default App

useImperativeHandle

useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值

1
useImperativeHandle(ref, createHandle, [deps])
  • ref:需要被赋值的 ref 对象
  • createHandle:的返回值作为 ref.current 的值。
  • [deps]:依赖数组,依赖发生变化重新执行 createHandle 函数

使用例子:

1
2
3
4
5
6
7
8
9
const Child = React.forwardRef((props, ref) => {
const inputRef = useRef()
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus()
},
}))
return <input ref={inputRef} />
})

或者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// App.js
;<Child cRef={this.myRef} />

// Child.js
const Child = props => {
const inputRef = useRef()
const { cRef } = props
useImperativeHandle(cRef, () => ({
focus: () => {
inputRef.current.focus()
},
}))
return <input ref={inputRef} />
}

useEffect

引入副作用,销毁函数和回调函数在 commit 阶段异步调度,在 layout 阶段完成后异步执行,不会阻塞 ui 得渲染。

1
2
3
4
5
6
useEffect(() => {
//...副作用
return () => {
// ...清除副作用
}
}, [deps])
  • 副作用在 commit 阶段异步执行,清除副作用的销毁函数会在下一阶段的的 commit 阶段执行,
  • [deps]:依赖数组,依赖发生变化重新执行

useLayoutEffect

引入副作用的,用法和 useEffect 一样,但 useLayoutEffect 会阻塞 dom 的渲染,同步执行,上一次更新的销毁函数在 commit 的 mutation 阶段执行,回调函数在在 layout 阶段执行,和 componentDidxxxx 是等价的。

useMemo

返回一个memo 值,作为一种性能优化的手段,只有当依赖项的依赖改变才会重新渲染值

1
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b])

useCallback

返回一个 memoized 回调函数,作为一种性能优化的手段,只有当依赖项的依赖改变才会重新构建该函数

1
2
3
const memoizedCallback = useCallback(() => {
doSomething(a, b)
}, [a, b])

useDebugValue

useDebugValue 可用于在 React 开发者工具中显示自定义 hook 的标签, 浏览器装有 react 开发工具调试代码的时候才有用。

useTransition

返回一个状态值表示过渡任务的等待状态,以及一个启动该过渡任务的函数。

1
const [isPending, startTransition] = useTransition()
  • isPending: 指示过渡任务何时活跃以显示一个等待状态,为 true 时表示过渡任务还没更新完。
  • startTransition: 允许你通过标记更新将提供的回调函数作为一个过渡任务,变为过渡任务则说明更新往后放,先更新其他更紧急的任务。

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import React, { useEffect, useState, useTransition } from 'react'

const SearchResult = props => {
const resultList = props.query
? Array.from({ length: 50000 }, (_, index) => ({
id: index,
keyword: `${props.query} -- 搜索结果${index}`,
}))
: []
return resultList.map(({ id, keyword }) => <li key={id}>{keyword}</li>)
}

export default () => {
const [isTrans, setIstrans] = useState(false)
const [value, setValue] = useState('')
const [searchVal, setSearchVal] = useState('')
const [loading, startTransition] = useTransition({ timeoutMs: 2000 })

useEffect(() => {
// 监听搜索值改变
console.log('对搜索值更新的响应++++++' + searchVal + '+++++++++++')
}, [searchVal])

useEffect(() => {
// 监听输入框值改变
console.log('对输入框值更新的响应-----' + value + '-------------')
}, [value])

useEffect(() => {
if (isTrans) {
startTransition(() => {
setSearchVal(value)
})
} else {
setSearchVal(value)
}
}, [value])

return (
<div className="App">
<h3>StartTransition</h3>
<input value={value} onChange={e => setValue(e.target.value)} />
<button onClick={() => setIstrans(!isTrans)}>{isTrans ? 'transiton' : 'normal'}</button>
{loading && <p>数据加载中,请稍候...</p>}
<ul>
<SearchResult query={searchVal}></SearchResult>
</ul>
</div>
)
}

useDeferredValue

useDeferredValue 接受一个值,并返回该值的新副本,该副本将推迟到更紧急地更新之后。如果当前渲染是一个紧急更新的结果,比如用户输入,React 将返回之前的值,然后在紧急渲染完成后渲染新的值。本,该副本将推迟到更紧急地更新之后

react-router v6 hooks

useHref

1
declare function useHref(to: To): string

useHref钩子返回一个 URL,可以用来链接到给定的 to 位置,甚至在 React router 之外。

useHref传入一个字符串或To对象,返回一个 URL 的绝对路径。可以在参数中传入一个相对路径、绝对路径、To 对象。
react hooks v6中的Link组件内部使用useHref获取它的 href 值

1
2
3
4
5
6
export interface Path {
pathname: Pathname
search: Search
hash: Hash
}
export type To = string | Partial<Path>

例子:

当前路由:/page1/page2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React, { useEffect } from 'react'
import { useHref } from 'react-router-dom'

function Page2(props) {
useEffect(() => {
console.log(useHref('../')) // 输出 '/page1/'
console.log(useHref('../../')) // 输出 '/'
console.log(useHref('/page1')) // 输出 '/page'
console.log(useHref('/pageabc')) // 输出 '/pageabc' 可见路径与当前路由无关
console.log(useHref({ pathname: '/page1', search: 'name=123', hash: 'test' })) // 输出 '/page1?name=123#test'
}, [])
return <div></div>
}

export default Page2

useInRouterContext

1
declare function useInRouterContext(): boolean

如果组件在Router组件中的上下文中渲染,useInRouterContext将返回 true,否则返回 false。这对于一些需要知道是否在react router的上下文中渲染的第三方扩展非常有用。

判断当前组件是否在由Router组件创建的Context

useLocation

1
2
3
4
5
6
declare function useLocation(): Location

interface Location extends Path {
state: unknown
key: Key
}

这个hook返回当前路由的location对象。

useMatch

1
declare function useMatch<ParamKey extends string = string>(pattern: PathPattern | string): PathMatch<ParamKey> | null

返回给定路径上相对于当前位置的路由的匹配数据。

react router v5中的useRouteMatch在 v6 版本中更名为useMatch。内部示例调用了matchPath方法根据 URL 路径名匹配路由路径模式,并返回有关匹配的信息。如果模式与给定路径名不匹配,则返回null

useNavigate

1
2
3
4
5
6
declare function useNavigate(): NavigateFunction

interface NavigateFunction {
(to: To, options?: { replace?: boolean; state?: any }): void
(delta: number): void
}

useNavigate钩子返回一个函数,该函数允许您以编程方式进行导航,例如在提交表单之后。

react router v5中组件获取到的useHistory在 v6 版本更名为useNavigate。在用法上作出了较大改变

例子:

当前路由:/user/page2
其他路由:/user/page1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React, { useEffect } from 'react'
import { useNavigate } from 'react-router-dom'

function Page2(){
let navigate = useNavigate()
useEffect(()=>{
navigate('/user/page1') // 导航到'/user/page1'
navigate('../') // 导航到'/user'
navigate(-1) // 导航到/user/page1 相当于v5中history.go(-1) history.goBack()
navigate(1) //导航到/user 相当于v5中history.go(1) history.goForward()
navigate('/user/page2/',{ replace: true, state: { name:123 }} // 相当于v5中history.replace(),并同时传入state
},[])
return <div></div>
}

export default Page2

useNavigationType

1
2
declare function useNavigationType(): NavigationType
type NavigationType = 'POP' | 'PUSH' | 'REPLACE'

这个钩子返回当前的导航类型或用户如何到达当前页面;通过历史堆栈上的poppushreplace操作。

这个hooks类似react router v5中组件传入参数中的history对象的action属性,返回触发当前navigate的是何种操作

useOutlet

1
declare function useOutlet(): React.ReactElement | null

返回路由当前路由的子路由的元素。这个hookOutlet组件在内部使用用于渲染子路由。

useParams

1
declare function useParams<K extends string = string>(): Readonly<Params<K>>

useParams返回 URL 参数的键/值对的对象。使用它来访问当前Routematch.params。子路由从其父路由继承所有参数。

useSearchParams

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
declare function useSearchParams(
defaultInit?: URLSearchParamsInit
): [URLSearchParams, SetURLSearchParams];

type ParamKeyValuePair = [string, string];

type URLSearchParamsInit =
| string
| ParamKeyValuePair[]
| Record<string, string | string[]>
| URLSearchParams;

type SetURLSearchParams = (
nextInit?: URLSearchParamsInit,
navigateOpts?: : { replace?: boolean; state?: any }
) => void;

useSearchParams用于读取和修改 URL 中当前位置的查询字符串。与useState一样,useSearchParams返回一个包含两个值的数组:当前位置的搜索参数一个可用于更新它们的函数

useSearchParams的工作原理与navigate类似,但仅适用于 URL 的search部分。

setSearchParams 的第二个参数要与 navigate 的第二个参数的类型相同

useResolvedPath

1
declare function useResolvedPath(to: To): Path

这个hook解析给定to对象中的pathname与当前位置的路径名,并返回一个Path对象

useRoutes

1
2
3
4
declare function useRoutes(
routes: RouteObject[],
location?: Partial<Location> | string;
): React.ReactElement | null;

useRoutes钩子与Route组件是功能相等的,但它使用 JavaScript 对象而不是Route元素取定义你的路由。该对象与Route元素具有相同的属性,但是它们不需要 JSX

useRoutes 的返回值要么是一个可以让你用于渲染路由树的 React Element,要么是 null(路由不匹配时)

react-redux hooks

首先看一个实际应用中使用connect的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { connect } from 'react-redux'

const Home = props=>{
// 获取数据
const { user, loading, dispatch } = props

// 发起请求
useEffect(()=>{
dispatch({
type:'user/fetchUser',payload:{}
})
}, [])

// 渲染页面
if(loading) return <div>loading...</div>
return (
<div>{user.name}<div>
)
}

export default connect(({ loading, user })=>({
loading:loading.effects['user/fetchUser'],
user:user.userInfo
}))(Home)

现在使用useDispatch useSelector改造一下上面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { useDispatch, useSelector } from 'react-redux'

const Home = props => {

const dispatch = useDispatch()

const loadingEffect = useSelector(state =>state.loading);
const loading = loadingEffect.effects['user/fetchUser'];
const user = useSelector(state=>state.user.userInfo)

// 发起请求
useEffect(()=>{
dispatch({
type:'user/fetchUser',payload:{}
})
},[])

// 渲染页面
if(loading) return <div>loading...</div>
return (
<div>{user.name}<div>
)
}

export default Home

再来说说hooks,它是React-Redux提供用来替代connect()高阶组件。这些hooks API 允许不使用connect()包裹组件的情况下订阅store和分发actions

hooks 需要在函数组件中使用,不能在 React 类中使用hooks

使用hooksconnect()一样,需要将应用包裹在<Provider/>中。

1
2
3
4
5
6
7
const store = createStore(rootReducer)

ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')

useSelector

作用:从reduxstore对象中提取数据(state)。

注意:selector函数应该是个纯函数,因为可能在任何时候执行多次。

1
2
3
4
5
6
7
import React from 'react'
import { useSelector } from 'react-redux'

export const CounterComponent = () => {
const counter = useSelector(state => state.counter)
return <div>{counter}</div>
}

selector函数被调用时将会被传入Redux store的整个state,作为唯一的参数。每次函数组件渲染时,selector函数都会被调用。useSelector()同样会订阅Reduxstore,并且在分发action时,都会被执行一次。

selectorconnectmapStateToProps的差异:

  • selector函数的返回值会被用作调用 useSelector() hook时的返回值,可以是任意类型的值。
  • 当分发action时,useSelector 会将上次调用的结果与当前调用的结果进行引用(===)比较,不一样会进行重新渲染。
  • useSelector()默认使用严格比较===来比较引用,而非浅比较。
  • selector函数不会接收ownProps参数,但是props可以通过闭包获取使用或者通过使用柯里化的selector函数。
1
2
3
4
5
6
7
import React from 'react'
import { useSelector } from 'react-redux'

export const TodoListItem = props => {
const todo = useSelector(state => state.todos[props.id])
return <div>{todo.text}</div>
}

action被分发后,useSelector()selector函数的返回值进行引用比较===,在selector的值改变时会触发 re-render。与connect不同,useSelector()不会阻止父组件重渲染导致的子组件重渲染的行为,即使组件的props没有发生改变。

如果想要进一步的性能优化,可以在React.memo()中包装函数组件。

1
2
3
4
5
6
7
8
9
10
const CounterComponent = ({ name }) => {
const counter = useSelector(state => state.counter)
return (
<div>
{name}: {counter}
</div>
)
}

export const MemoizedCounterComponent = React.memo(CounterComponent)

useDispatch

1
const dispatch = useDispatch()

作用:返回Redux store中对dispatch函数的引用。

当使用dispatch函数将回调传递给子组件时,建议使用useCallback函数将回调函数记忆化,防止因为回调函数引用的变化导致不必要的渲染。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React, { useCallback, memo } from 'react'
import { useDispatch } from 'react-redux'

export const MyIncrementButton = memo(({ onIncrement }) => <button onClick={onIncrement}>Increment counter</button>)

export const Counter = ({ value }) => {
const dispatch = useDispatch()
const incrementCounter = useCallback(() => dispatch({ type: 'increment-counter' }), [dispatch])

return (
<div>
<span>{value}</span>
<MyIncrementButton onIncrement={incrementCounter} />
</div>
)
}

useStore()

1
const store = useStore()

作用:返回传递给组件的 Redux store 的引用。

注意:应该将useSelector()作为首选,只有在个别场景下才会需要使用它,比如替换 storereducers

1
2
3
4
5
6
7
8
import React from 'react'
import { useStore } from 'react-redux'

export const Counter = ({ value }) => {
const store = useStore()

return <div>{store.getState()}</div>
}

hook 进阶

react hook 工作当中也用了一段时间了,中间踩过一些坑,针对不同 hook 的特点,进行总结。

  • 两个 state 是关联或者需要一起发生改变,可以放在同一个 state,但不要太多
  • state 的更新逻辑比较复杂的时候则可以考虑使用 useReducer 代替
  • useEffectuseLayoutEffectuseMemouseCallbackuseImperativeHandle 中依赖数组依赖项最好不要太多,太多则考虑拆分一下,感觉不超 3 到 4 个会比较合适。
  • 去掉不必要的依赖项
  • 合并相关的 state 为一个
  • 通过 setState 回到函数方式去更新 state
  • 按照不同维度这个 hook 还能不能拆分的更细
  • useMemo 多用于对 React 元素做 memorize 处理或者需要复杂计算得出的值,对于简单纯 js 计算就不要进行 useMemo 处理了。
  • useCallback 要配合memo使用