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 = () => { 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
| ;<Child cRef={this.myRef} />
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('../')) console.log(useHref('../../')) console.log(useHref('/page1')) console.log(useHref('/pageabc')) console.log(useHref({ pathname: '/page1', search: 'name=123', hash: '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') navigate('../') navigate(-1) navigate(1) navigate('/user/page2/',{ replace: true, state: { name:123 }} },[]) return <div></div> }
export default Page2
|
useNavigationType
1 2
| declare function useNavigationType(): NavigationType type NavigationType = 'POP' | 'PUSH' | 'REPLACE'
|
这个钩子返回当前的导航类型或用户如何到达当前页面;通过历史堆栈上的pop
、push
或replace
操作。
这个hooks
类似react router v5
中组件传入参数中的history
对象的action
属性,返回触发当前navigate
的是何种操作
useOutlet
1
| declare function useOutlet(): React.ReactElement | null
|
返回路由当前路由的子路由的元素。这个hook
被Outlet
组件在内部使用用于渲染子路由。
useParams
1
| declare function useParams<K extends string = string>(): Readonly<Params<K>>
|
useParams
返回 URL 参数的键/值对的对象。使用它来访问当前Route
的match.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
。
使用hooks
和connect
()一样,需要将应用包裹在<Provider/>
中。
1 2 3 4 5 6 7
| const store = createStore(rootReducer)
ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById('root')
|
useSelector
作用:从redux
的store
对象中提取数据(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()
同样会订阅Redux
的store
,并且在分发action
时,都会被执行一次。
selector
和connect
的mapStateToProps
的差异:
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()
作为首选,只有在个别场景下才会需要使用它,比如替换 store
的 reducers
。
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
代替
useEffect
、useLayoutEffect
、useMemo
、useCallback
、useImperativeHandle
中依赖数组依赖项最好不要太多,太多则考虑拆分一下,感觉不超 3 到 4 个会比较合适。
- 去掉不必要的依赖项
- 合并相关的
state
为一个
- 通过
setState
回到函数方式去更新 state
- 按照不同维度这个 hook 还能不能拆分的更细
useMemo
多用于对 React
元素做 memorize
处理或者需要复杂计算得出的值,对于简单纯 js 计算就不要进行 useMemo
处理了。
- useCallback 要配合
memo
使用