React Hooks
Hook
是 React 16.8
的新增特性。它可以让你在不编写 class
的情况下使用 state
以及其他的 React
特性。
- 只在最顶层使用
Hook
- 只在
React
函数中调用 Hook
基础 Hook
useState
useEffect
useContext
额外的 Hook
useReducer
useCallback
useMemo
useRef
useImperativeHandle
useLayoutEffect
useDebugValue
注意
React 16.8.0
是第一个支持 Hook
的版本。升级时,请注意更新所有的 package
,包括 React DOM
React Native
从 0.59
版本开始支持 Hook
。
ESLint 插件
一个名为 eslint-plugin-react-hooks
的 ESLint
插件来强制执行这两条规则。如果你想尝试一下,可以将此插件添加到你的项目中:
1
| npm install eslint-plugin-react-hooks --save-dev
|
1 2 3 4 5 6 7 8 9 10 11 12
| // 你的 ESLint 配置 { "plugins": [ // ... "react-hooks" ], "rules": { // ... "react-hooks/rules-of-hooks": "error", // 检查 Hook 的规则 "react-hooks/exhaustive-deps": "warn" // 检查 effect 的依赖 } }
|
后续版本默认添加此插件到 Create React App
及其他类似的工具包中。
useState
useState
是最基本的 API
,它传入一个初始值,每次函数执行都能拿到新值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import React, { useState } from 'react'; import styles from './index.module.scss'; function PageCount() {
const [count, setCount] = useState(0);
return ( <div className={styles.PageMain}> <button onClick={() => setCount(count + 1)}>+</button> <h1>{count}</h1> <button onClick={() => setCount(count - 1)}>-</button> </div> ); } export default PageCount;
|
useReducer
useReducer
和 useState
几乎是一样的,需要外置外置 reducer
(全局),通过这种方式可以对多个状态同时进行控制。仔细端详起来,其实跟 redux
中的数据流的概念非常接近。
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
| import React, { useReducer } from 'react'; import styles from './index.module.scss';
function reducer(state, action) { switch (action.type) { case 'up': return { count: state.count + 1 }; case 'down': return { count: state.count - 1 }; default: return "" } }
function PageCount() { const [state, dispatch] = useReducer(reducer, { count: 1 }) return ( <div className={styles.PageMain}> <button onClick={() => dispatch({ type: 'up' })}>+</button> <h1>{state.count}</h1> <button onClick={() => dispatch({ type: 'down' })}>-</button> </div> ); }
export default PageCount;
|
useEffect
在 React hook
中,useEffect
用来取代 componentDidMount
和 componentDidUpdate
。主要作用是当页面渲染后,进行一些副作用操作(比如访问 DOM
,请求数据)。
数据获取,设置订阅或者手动直接更改 React
组件中的 DOM
都属于副作用。有的人习惯成这种行为为 effect
Effect Hook
可以让你能够在 Function
组件中执行副作用
React
组件中有两种常见的副作用:
- 需要清理的副作用
- 不需要清理的副作用。
如果在重新渲染之间没有更新某些值,则可以告诉 React
跳过 effect
,为了实现这种方式,需要将数组作为可选的第二个参数传递给 useEffect
从 effect
中返回一个 function
,这是 effect
可选的清理机制。每个 effect
都可以返回一个在它之后清理的 function
useEffect
最后,不加[]
就表示每一次渲染都执行
useEffect
最后,加了[]
就表示只第一次执行
useEffect
最后,加[]
,并且[]
里面加的字段就表示,这个字段更改了,我这个effect
才执行
useEffect
不能被判断包裹
useEffect
不能被打断
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
| import React, { useState, useEffect } from 'react'; import styles from './index.module.scss';
function PageCount() { const [count, setCount] = useState(0);
useEffect(() => { document.title = `You clicked ${count} times`; return function cleanup() { document.title = 'app'; } }, [count]);
return ( <div className={styles.PageMain}> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div> ); }
export default PageCount;
|
React
何时清除 effect?
React
会在组件卸载的时候执行清除操作。正如之前学到的,effect
在每次渲染的时候都会执行。这就是为什么 React
会在执行当前 effect
之前对上一个 effect
进行清除。
这将助于避免 bug
以及如何在遇到性能问题时跳过此行为。
useMemo
useMemo
缓存计算数据的值
useMemo
主要用于渲染过程优化,两个参数依次是计算函数(通常是组件函数)和依赖状态列表,当依赖的状态发生改变时,才会触发计算函数的执行。如果没有指定依赖,则每一次渲染过程都会执行该计算函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import React, { useState, useMemo } from 'react'; import styles from './index.module.scss';
export default function WithMemo() { const [count, setCount] = useState(1); const [val, setValue] = useState(''); const expensive = useMemo(() => { let sum = 0; for (let i = 0; i < count * 100; i++) { sum += i; } return sum; }, [count]);
return <div className={styles.PageMain}> <h4>{count}-{expensive}</h4> <p>{val}</p> <div> <button onClick={() => setCount(count + 1)}>click me +1</button> <input value={val} onChange={event => setValue(event.target.value)} /> </div> </div>; }
|
useContext
useContext
与useReducer
的使用
调用useContext
,传入从React.createContext
获取的上下文对象。
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 51 52 53 54 55 56 57 58 59 60 61 62 63
| import React, { createContext, useContext, useReducer } from "react";
export const defaultState = { value: 0 }
export function reducer(state, action) { switch (action.type) { case 'ADD_NUM': return { ...state, value: state.value + 1 }; case 'REDUCE_NUM': return { ...state, value: state.value - 1 }; default: throw new Error(); } }
const StoreContext = createContext(null);
export function ChildFirst() { const AppContext = useContext(StoreContext) return ( <div> <button onClick={() => { AppContext.dispatch({ type: "ADD_NUM", payload: {} }) }}>增加</button> <button onClick={() => { AppContext.dispatch({ type: "REDUCE_NUM", payload: {} }) }}>递减</button> </div> ) }
export function ChildSecond() { const AppContext = useContext(StoreContext) return ( <div> {AppContext.state.value + 's'} </div> ) }
export function RootCompent() { const [state, dispatch] = useReducer(reducer, defaultState)
return ( <StoreContext.Provider value={{ state, dispatch: dispatch }}> <ChildFirst /> <ChildSecond /> </StoreContext.Provider> ) }
|
useRef
在函数式组件里没有了 this
来存放一些实例的变量,所以 React
建议使用 useRef
来存放一些会发生变化的值,useRef
并不再单单是为了 DOM
的 ref
准备的,同时也会用来存放组件实例的属性。
useRef
主要有两个使用场景:
- 获取子组件或者
DOM
节点的句柄
- 渲染周期之间的共享数据的存储
createRef
与useRef
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 React, { useState, createRef, useRef } from "react"; export function RootCompent() { const [renderIndex, setRenderIndex] = useState(1) const fromUseRef = useRef(); const fromCreateRef = createRef(); if (!fromUseRef.current) { fromUseRef.current = renderIndex; } if (!fromCreateRef.current) { fromCreateRef.current = renderIndex; } return ( <div> <p>当前渲染index : {renderIndex}</p> <p>fromUseRef : {fromUseRef.current}</p> <p>fromCreateRef {fromCreateRef.current}</p> <button onClick={() => { setRenderIndex(prev => prev + 1); }}> 点击 </button> </div> ) }
|
useRef
经典例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| import React, { useState, useRef } from "react"; export function RootCompent() { const [count, setCount] = useState(1) const lastCount = useRef(count) function handleClick() { setTimeout(() => { alert("你点击了" + count + "次"); alert("你点击了" + lastCount.current + "次"); }, 1000) } useEffect(() => { lastCount.current = count; })
return ( <div> <p>你点击了 : {count}次</p> <button onClick={() => { setCount(count + 1) }}>点击</button> <button onClick={() => { handleClick() }}>显示提示</button> </div> ) }
|
useRef
每次都会返回同一个引用, 所以在 useEffect
中修改的时候 ,在 alert
中也会同时被修改. 这样子, 点击的时候就可以弹出实时的 count
了.
useRef
获取上一次的值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import React, { useState, useRef } from "react"; export function RootCompent() { const [count, setCount] = useState(1) const prevtCount = useRef()
useEffect(() => { prevtCount.current = count; }) return ( <div> <p>prevtCount : {prevtCount.current}次</p> <p>你点击了 : {count}次</p> <button onClick={() => { setCount(count + 1) }}>点击</button> </div> ) }
|
将获取上一次的值封装成自定义钩子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| function usePrevious(state) { const ref = useRef();
useEffect(() => { ref.current = state; }) return ref.current; }
export function RootCompent() { const [count, setCount] = useState(1) const prevtCount = usePrevious(count)
return ( <div> <p>prevtCount : {prevtCount}次</p> <p>你点击了 : {count}次</p> <button onClick={() => { setCount(count + 1) }}>点击加</button> <button onClick={() => { setCount(count - 1) }}>点击减</button> </div> ) }
|
useCallback
useCallback
缓存函数的引用
使用useCallback
去缓存我们的 handleClick
setCount
方法支持接收一个函数,通过这个函数可以获取最新的state
值
useCallback
是基于useMemo
的封装。只有当依赖数组产生变化时,useCallback
才会返回一个新的函数,否则始终返回第一次的传入callback
。
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 51 52 53 54 55 56 57 58 59
| const Child = React.memo(({ onClick }) => { return <div onClick={onClick}>click</div>; });
export function RootCompent() { const [count, setCount] = useState(0)
const handleClick = useCallback(() => { setCount(c => c + 1); }, [])
return ( <div> <p>{count}</p> <Child onClick={handleClick}></Child> </div> ) } ````
`useCallback` 的真正目的还是在于缓存了每次渲染时 `inline callback` 的实例,这样方便配合上子组件的 `shouldComponentUpdate` 或者 `React.memo` 起到减少不必要的渲染的作用。
需要不断提醒自己注意的是,在大部分 `callback` 都会是 `inline callback` 的未来,`React.memo`和 `React.useCallback` 一定记得需要配对使用,缺了一个都可能导致性能不升反“降”,毕竟无意义的浅比较也是要消耗那么一点点点的性能。
只有对于大计算量的函数来说,利用`useCallback`才能起到良好的优化效果
## `useLayoutEffect`
其函数签名与 `useEffect` 相同,但它会在所有的 `DOM` 变更之后同步调用 `effect`。可以使用它来读取 `DOM` 布局并同步触发重渲染。
在浏览器执行绘制之前,`useLayoutEffect` 内部的更新计划将被同步刷新。
### 提示
* `useLayoutEffect` 与 `componentDidMount、componentDidUpdate` 的调用阶段是一样的。但是,我们推荐你一开始先用 `useEffect`,只有当它出问题的时候再尝试使用 `useLayoutEffect`
```javascript //当你每次点击 div, count 会更新为 0, 之后 useEffect 内又把 count 改为一串随机数。 //所以页面会先渲染成0,然后再渲染成随机数,由于更新很快,所以出现了闪烁 function RootCompent() { const [count, setCount] = useState(0);
useEffect(() => { if (count === 0) { const randomNum = 10 + Math.random() * 200 setCount(10 + Math.random() * 200); } }, [count]);
return ( <div> <p>{count}</p> <div onClick={() => setCount(0)}>点击</div> </div> ); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| function RootCompent() { const [count, setCount] = useState(0);
useLayoutEffect(() => { if (count === 0) { const randomNum = 10 + Math.random() * 200 setCount(10 + Math.random() * 200); } }, [count]);
return ( <div> <p>{count}</p> <div onClick={() => setCount(0)}>点击</div> </div> ); }
|
useDebugValue
useDebugValue
可用于在 React
开发者工具中显示自定义 hook
的标签。
提示
不推荐你向每个自定义 Hook
添加 debug
值。当它作为共享库的一部分时才最有价值。
useDebugValue
接受一个格式化函数作为可选的第二个参数。该函数只有在 Hook
被检查时才会被调用。它接受 debug
值作为参数,并且会返回一个格式化的显示值。
1
| useDebugValue(date, date => date.toDateString());
|
useImperativeHandle
1
| useImperativeHandle(ref, createHandle, [deps])
|
useImperativeHandle
可以让你在使用 ref
时自定义暴露给父组件的实例值。在大多数情况下,应当避免使用 ref
这样的命令式代码。useImperativeHandle
应当与 forwardRef
一起使用
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
| function Child(props, ref) { const kun = useRef()
const introduce = useCallback(() => { console.log('i can sing, jump, rap, play basketball') }, [])
useImperativeHandle(ref, () => ({ introduce: () => { introduce() } }));
return ( <div ref={kun}> {props.count}</div> ) }
const FChild = forwardRef(Child)
function RootCompent() { const [count, setCount] = useState(0) const fcRef = useRef(null)
const onClick = useCallback(() => { setCount(count => count + 1) fcRef.current.introduce() }, [])
return ( <div> 点击次数: {count} <FChild ref={fcRef} count={count}></FChild> <button onClick={onClick}>点我</button> </div> ) }
|
自定义钩子
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
| function useCounter(initialValue) { const [count, changeCount] = useState(initialValue); const decrease = () => { changeCount(count - 1); } const increase = () => { changeCount(count + 1); } const resetCounter = () => { changeCount(0); } return [count, { decrease, increase, resetCounter }] }
function RootCompent() { const [count, controlCount] = useCounter(10); return ( <div> 当前数量:{count} <button onClick={controlCount.decrease}>减少</button> <button onClick={controlCount.increase}>增加</button> <button onClick={controlCount.resetCounter}>重置</button> </div> ) }
|
相关链接
- React-Hooks
- 官方文档
- 聊聊 useCallback