Hooks are a new addition in React 16.8.
They let you use state and other React features without writing a class.
背景
- 在组件之间复用状态逻辑很难 (render props 和 高阶组件容易造成嵌套地狱)
- 部分复杂的组件中,相互关联且需要对照修改的代码被进行了拆分,而完全不相关的代码却在同一个方法中组合在一起(例如,组件常常在 componentDidMount 和 componentDidUpdate 中获取数据。但是,同一个 componentDidMount 中可能也包含很多其它的逻辑,如设置事件监听,而之后需在 componentWillUnmount 中清除。)
- 难以理解的class (this、bind等)
使用规则
- 只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。
- 只能在 React 的函数组件中调用 Hook。
推荐配置 eslint-plugin-react-hooks 来检测语法
Hooks 介绍
下面介绍一下比较常用的几个API
const [count, setCount] = useState(0);
useState函数接受一个参数,为该state的初始值(可以传入函数计算得出初始值),返回一个数组,利用数组解构的语法,可得到该state以及修改该state的函数。
setCount可以setCount(newCount),也可以setCount(prev => prev + 1),注意它与setState的合并是不一样的,它是完全替换的。
React 假设当你多次调用 useState 的时候,你能保证每次渲染时它们的调用顺序是不变的。所以一定要记得:不要在循环、条件判断或者子函数中调用Hook。
1 | useEffect(() => { |
数据获取,设置订阅以及手动更改 React 组件中的 DOM 都属于副作用。这些操作应该放在useEffect中。在组件每次渲染之后才会调用useEffect。useEffect 会在浏览器绘制后延迟执行。
useEffect有清除机制,在return中的函数,React会在执行清除操作时调用它。
useEffect(() => {} , [deps]) 仅当依赖deps变化时才执行函数,若为空数组则只执行一次。
如果你要使用此优化方式,请确保数组中包含了所有外部作用域中会发生变化且在 effect 中使用的变量,否则你的代码会引用到先前渲染中的旧变量。配置上文提到的eslint插件后,可检测出deps是否有遗漏。
PS: React 会确保 setState 函数的标识是稳定的,并且不会在组件重新渲染时发生变化。这就是为什么可以安全地从 useEffect 或 useCallback 的依赖列表中省略 setState。
const value = useContext(MyContext);
接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定。
当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值。
useState 的替代方案。它接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法。
在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等。并且,使用 useReducer 还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递 dispatch 而不是回调函数。
1 | // useCallback |
useCallback(fn, deps) 相当于 useMemo(() => fn, deps)。
有些人可能会误以为 useCallback 可以用来解决创建函数造成的性能问题,其实恰恰相反,单从这个组件看的话 useCallback 只会更慢,因为 inline 函数是无论如何都会创建的,还增加了 useCallback 内部对依赖变化的检测。
useCallback 的真正目的还是在于缓存了每次渲染时 inline callback 的实例,这样方便配合上子组件的 shouldComponentUpdate 或者 React.memo 起到减少不必要的渲染的作用。需要注意的是,在优化子组件的性能时,React.memo 和 React.useCallback 一定记得需要配对使用,缺了一个都可能导致性能不升反“降”
( React.memo是函数式组件的PureComponet实现,详情 > React.memo介绍 )
1 | // 使用了useCallback,性能更差 |
So when should I useMemo and useCallback?
There are specific reasons both of these hooks are built-into React:
- Referential equality
- Computationally expensive calculations
当涉及到引用类型的比较(适用useCallback和useMemo),或需要昂贵的计算时(适用useMemo),我们才考虑使用它们。
1 | // 在useEffect判断依赖时,保持引用的一致 |
1 | // 避免了重复的昂贵计算 |
const refContainer = useRef(initialValue);
useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变。
其函数签名与 useEffect 相同,但它会在所有的 DOM 变更之后同步调用 effect。可以使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect 内部的更新计划将被同步刷新。
尽可能使用标准的 useEffect 以避免阻塞视觉更新。
自定义Hook
通过自定义 Hook,可以将组件逻辑提取到可重用的函数中。
假设我们有几个组件都需要做权限校验,我们就可以把权限校验的逻辑提取出来
1 | // 自定义Hook |
现在我们已经把这个权限校验逻辑提取到 usePermission 的自定义 Hook 中,然后就可以在所有页面组件中使用它了。
自定义 Hook 是一个函数,其名称以 “use” 开头,函数内部可以调用其他的 Hook。
与 React 组件不同的是,自定义 Hook 不需要具有特殊的标识。我们可以自由的决定它的参数是什么,以及它应该返回什么(如果需要的话)。换句话说,它就像一个正常的函数。但是它的名字应该始终以 use 开头,这样可以一眼看出其符合 Hook 的规则。
一般来说,自定义Hook会在内部维护自己的state,当函数参数发生变化或者执行了某种异步操作后,通过useEffect去动态改变state的值,最后返回该state。这样,外部调用的组件就可以根据这个state来获取所需要的信息。
在两个组件中使用相同的 Hook 会共享 state 吗?
不会。自定义 Hook 是一种重用状态逻辑的机制(例如设置为订阅并存储当前值),所以每次使用自定义 Hook 时,其中的所有 state 和副作用都是完全隔离的。提起出自定义Hook的代码等价于原来的代码吗?
等价,它的工作方式完全一样。如果你仔细观察,你会发现我们没有对其行为做任何的改变,我们只是将两个函数之间一些共同的代码提取到单独的函数中。自定义 Hook 是一种自然遵循 Hook 设计的约定,而并不是 React 的特性。
自定义 Hook 解决了以前在 React 组件中无法灵活共享逻辑的问题。你可以创建涵盖各种场景的自定义 Hook,如表单处理、动画、订阅声明、计时器,甚至可能还有其他我们没想到的场景。更重要的是,创建自定义 Hook 就像使用 React 内置的功能一样简单。
(一个自定义Hook的优秀例子:Debouncing with React Hooks)