本文已参与「新人创作礼」活动,一起开启掘金创作之路。详情
本篇文章知识点速览:hook的简介、为什么要引入hook,使用hook解决了什么问题、hook的api(10种hook)讲解、如何使用自定义hook、使用hook的注意事项等。
React Hook简介
Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
1. 引入hook的动机
-
难以理解的class
我们在学习
react的时候,使用class组件需要去理解其概念和js中的this。还需要绑定事件处理器。除此之外,class组件的写法也很繁琐。理解成本和编写成本都很高。 -
复杂组件变得难以理解
我们使用类组件后,随着业务逻辑变得复杂,组件的内容也越发庞大。组件中时常会包含很多不相关的逻辑状态或者副作用。比如在
componentDidMount中我们需要获取多种数据,还要处理其他的逻辑,比如订阅,监听等。不同的事件处理都放在一起混杂且容易产生bug。而Hook 将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据) ,而并非强制按照生命周期划分。 -
组件之间的复用状态逻辑很难
当我们类组件跨组件复用时(如把组件连接到store),我们可以使用
render props或高阶组件。但是使用这些方案时需要修改当前组件结构且很复杂。且由 providers,consumers,高阶组件,render props 等其他抽象层组成的组件嵌套过多会形成“嵌套地狱“。此种情况我们可以使用 自定义Hook 从组件中提取状态逻辑,使得这些逻辑可以单独测试并复用。
2. hook解决的问题
- 函数组件可以代替class:hook的出现使得函数组件有了状态,实现了类似React class组件的state、生命周期钩子等特性,可以执行副作用。
- 跨组件复用: 其实 render props / HOC 也是为了复用,相比于它们,Hooks 作为官方的底层 API,最为轻量,而且改造成本小,不会影响原来的组件层次结构和传说中的HOC嵌套地狱;
- 状态与UI隔离: 正是由于 Hooks 的特性,状态逻辑会变成更小的粒度,并且极容易被抽象成一个自定义 Hooks,组件中的状态和 UI 变得更为清晰和隔离。
3. 函数组件与类组件对比
- 类组件需要继承 class,函数组件不需要;
- 类组件可以访问生命周期方法,函数组件不能;
- 类组件中可以获取到实例化后的 this,并基于这个 this 做各种各样的事情,而函数组件不可以;
- 类组件中可以定义并维护 state(状态),而函数组件不可以;
当React引入hook后,我们就可以使用函数组件来代替class组件,以避免出现类组件的这些情况:
- 不同的生命周期会使逻辑变得分散且混乱,不易维护和管理;
- 时刻需要关注this的指向问题;
- 代码复用代价高,高阶组件的使用经常会使整个组件树变得臃肿;
4. 注意事项
- 避免在
循环/条件判断/嵌套函数中调用 hooks,保证调用顺序的稳定; - 只有
函数定义组件和hooks可以调用 hooks,避免在类组件或者普通函数中调用; - 不能在useEffect中使用useState,React 会报错提示;
- 类组件不会被替换或废弃,不需要强制改造类组件,两种方式能并存;
HOOK API
基础hook:
useState
用于定义组件的 State,相似于类组件中定义this.state的功能;
const [num, setNum] = useState(0); // 0是num的初始值
setNum(1); // 修改num为1
// useState和setState的方法还可以传入一个函数
const [num, setNum] = useState(()=>{
const initNum = someExpensiveComputation(props)//一些复杂运算
return initNum;
});
setNum(preNum=>preNum*2);
①调用useState的传参:可以直接传入一个初始值,也可以传入一个函数,通过复杂计算后生成初始值return给useState(state的惰性初始化)。初始值和初始函数的设置只在组件初始化渲染时起作用。
②调用useState的返回结果:返回数组,第一个值为一个组件状态值,上面示例命名为num接收。返回第二个值为一个修改这个状态值的方法,我们命名为setNum。
③调用setNum可以修改num,我们可以直接传入值,也可以是一个回调函数,回调函数可以拿到上一次这个状态的值。
useEffect
这个hook可以做一些副作用操作。默认情况下,effect 将在每轮渲染结束后执行。我们可以使用它达到class组件中生命周期钩子的作用。
useEffect(() => {
// 组件挂载后执行事件绑定
console.log('on')
addEventListener('xx')
// 组件 update 时会执行事件解绑
return () => {
console.log('off')
removeEventListener('xx')
}
}, [source]);
①useEffect接收两个参数:第一个参数是执行副作用的函数;第二个参数是依赖项,是一个数组。
②当依赖项数组中有值变化时,执行第一个参数的函数。
③当依赖项为空数组[]时,相当于类组件的componentDidMount,在组件挂载后执行一次副作用函数。同时第一个参数 副作用函数可以return一个方法,在组件卸载时调用一次,类似于componentWillUnmount
const useMount = (fn) => useEffect(fn, [])
const useUnmount = (fn) => useEffect(() => fn, [])
④effect的执行时机是在浏览器重新渲染之后延迟执行操作。
【注】组件多次渲染(通常如此),则在执行下一个 effect 之前,上一个 effect 就已被清除。创建依赖性后,effect只有依赖性更新后才会创建新的订阅
useContext
通过useContext我们可以使用Context进行组件传值通信。
import React, { useContext, useState } from 'react';
const Context = React.createContext();
// 1. 通过useContext使用Context组件通信
function HookDemo () {
const value = useContext(Context)
return(<div>
useContext得到value:{value}
</div>)
}
// 2.使用Context.Consumer方式通信
const ConsumerDemo = ()=>{
return <Context.Consumer>
{ (value)=> <div> Context.Consumer方式获取value:{ value }</div> }
</Context.Consumer>
}
// 父组件
export default function () {
const [num,setNum] = useState(1)
return <div>
<Context.Provider value={num}>
<HookDemo />
<ConsumerDemo />
</Context.Provider>
<button onClick={()=>setNum(num+1)}>++</button>
</div>
}
①通过React.createContex创建一个Context。
②使用<Context.Provider value={num}></Context.Provider>将需要跨层级接收传值的组件包裹起来,其中的value是传给子级组件的值。
③使用hookuseContext()可以获取到Context.Provider传入的value值。useContext()方法需要传入①步骤中创建的同一个Context做参数。
④还有其他<Context.Consumer>方法来获取Context.Provider传递的value。
其他hook:
useReducer
语法:
const [state, dispatch] = useReducer(reducer, initialArg, init);
可以替代useState。在某些特殊场景下使用useReducer要比useState合适。
// 1.定义一个初始状态值
const initState = {num: 0};
// 2. 定义一个reducer方法,reducer方法可以回调拿到上一次的state和一个action
function reducer(state, action) {
// 3. 根据不同的action做不同操作
switch (action.type) {
case 'increment':
return {num: state.num + 1};
case 'decrement':
return {num: state.num - 1};
default:
throw new Error();
}
}
function Counter() {
// 使用useReducer定义一个reducer方法,返回一个state和dispatch方法
// 可以调用dispatch方法传入一个action后会触发reducer对state进行修改
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
值: {state.num}
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}
这个写法很类似与Redux或dva中的model
另外useReducer的第三个参数是传入一个函数做参数,函数return一个值做状态值的惰性初始化,与useState的类似。
useMemo
这个方法用法类似useEffect,都是传入一个函数和一个依赖性。
用于缓存传入的 props,避免依赖的组件每次都重新渲染;
const memoizedValue = useMemo(()=>{
const val = someExpensiveComputation()//一些复杂运算
return val
}, [props.num]) // 只有当依赖项中有变化时才执行函数
【注】:
① 传入 useMemo 的函数会在渲染期间执行。请不要在这个函数内部执行与渲染无关的操作,诸如副作用这类的操作属于 useEffect 的适用范畴,而不是 useMemo。
② 传入useEffect的副作用函数不会在渲染阶段执行。在函数组件主体内(这里指在 React 渲染阶段)改变 DOM、添加订阅、设置定时器、记录日志以及执行其他包含副作用的操作都是不被允许的,因为这可能会产生莫名其妙的 bug 并破坏 UI 的一致性。
③如果没有依赖性,useMemo 在每次渲染时都会计算新的值。
使用场景示例:减少dom循环生成
// 只有当dataList变化时才重新渲染相关列表dom的部分
{useMemo(() => (
<div>
{dataList.map((i, v) => (
<span
className={style.listSpan}
key={v} >
{i.patentName}
</span>
))}
</div>
), [dataList])}
// 只有当props中list列表改变的时候,子组件才重新渲染
const goodListChild = useMemo(()=> <GoodList list={ props.list } /> ,[ props.list ])
useCallback
useCallback(fn, deps) 相当于 useMemo(() => fn, deps)。
用于缓存回调函数,避免传入的回调每次都是新的函数实例而导致依赖组件重新渲染,具有性能优化的效果;
①useMemo在依赖变化时执行传入的fn
②useCallback在依赖变化时返回一个fn
const DemoChildren = React.memo((props)=>{
/* 只有初始化的时候打印了 子组件更新 */
console.log('子组件更新')
useEffect(()=>{
props.getInfo('小明')
},[])
return <div>子组件</div>
})
const DemoUseCallback=({ id })=>{
const [number, setNumber] = useState(1)
// 通过useCallback可以拿到子组件传过来的值:'小明'
const getInfoCallback = useCallback((sonName)=>{
console.log(sonName)
},[id])
return <div>
{/* 点击按钮触发父组件更新 ,但是子组件没有更新 */}
<button onClick={ ()=>setNumber(number+1) } >增加{number}</button>
<DemoChildren getInfo={getInfoCallback} />
</div>
}
useLayoutEffect
- DOM更新同步钩子。用法与useEffect类似,只是区别于执行时间点的不同
- useEffect属于异步执行,而useLayoutEffect则会真正渲染后才触发;
- 可以获取更新后的 state;
useEffect执行顺序: 组件更新挂载完成 -> 浏览器 dom 绘制完成 -> 执行 useEffect 回调。
useLayoutEffect 执行顺序: 组件更新挂载完成 -> 执行 useLayoutEffect 回调-> 浏览器dom绘制完成。
所以说 useLayoutEffect 代码可能会阻塞浏览器的绘制 。我们写的 effect和 useLayoutEffect,react在底层会被分别打上PassiveEffect,HookLayout,在commit阶段区分出,在什么时机执行。
const DemoUseLayoutEffect = () => {
const target = useRef()
useLayoutEffect(() => {
/*我们需要在dom绘制之前,移动dom到制定位置*/
const { x ,y } = getPositon() /* 获取要移动的 x,y坐标 */
animate(target.current,{ x,y })
}, []);
return (
<div >
<span ref={ target } className="animate"></span>
</div>
)
}
useRef
使用useRef可以拿到元素/组件实例
import React, {useRef} from 'react';
function RefsTest() {
const nameRef = useRef(null)
const btnClick = () => {
console.log(nameRef.current, nameRef.current.value)
}
return(
<div>
姓名:
<input ref={nameRef}></input>
<button onClick={btnClick}>提交</button>
</div>
)
}
useImperativeHandle
useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值。在大多数情况下,应当避免使用 ref 这样的命令式代码。useImperativeHandle 应当与 forwardRef 一起使用:
function FancyInput(props, ref) {
const inputRef = useRef();
// 暴漏整个input元素出去
useImperativeHandle(ref, ()=>inputRef.current);
// 也可以只暴漏input元素的某些方法和值
// useImperativeHandle(ref, () => ({
// value: inputRef.current.value;
// focus: () => {
// inputRef.current.focus();
// }
// }));
return <div><input ref={inputRef} /></div>;
}
// 需要结合forwardRef转发ref使用
const RefInput = React.forwardRef(FancyInput);
// 父组件
function RefsTest() {
const nameRef = useRef(null)
const btnClick = () => {
console.log(nameRef.current, nameRef.current.value)
}
return(
<div>
<RefInput ref={nameRef} />
<button onClick={btnClick}>提交</button>
</div>
)
}
export default RefsTest
useDebugValue
useDebugValue 可用于在 React 开发者工具中显示自定义 hook 的标签。
例如,“自定义 Hook” 章节中描述的名为 useFriendStatus 的自定义 Hook:
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
// ...
// 在开发者工具中的这个 Hook 旁边显示标签 // e.g. "FriendStatus: Online" useDebugValue(isOnline ? 'Online' : 'Offline');
return isOnline;
}
自定义HOOK
基于 Hooks 可以引用其它 Hooks 这个特性,我们可以编写自定义钩子。
例如我们给每个页面自定义标题:
function useTitle(title) {
useEffect(
() => {
document.title = title;
});
}
// 使用:
function Home() {
const title = '我是首页'
useTitle(title)
return (
<div>{title}</div>
)
}
注意事项
1. hook的使用限制
上面简介中我们说了,不可以在循环、条件或嵌套函数中调用 Hook,这是为什么呢?
因为 Hooks 的设计是基于数组实现的。在调用时按顺序加入数组中,如果使用循环、条件或嵌套函数很有可能导致数组取值错位,执行错误的 Hook。当然,实质上 React 的源码里不是数组,是链表。
2. useEffect 与 useLayoutEffect 区别
相同点:底层签名函数都是调用的 mountEffectImpl;用法一样;都用于处理副作用。
不同点:①useEffect在渲染过程中是异步调用的,适合大多场景。而 LayoutEffect 会在所有的 DOM 变更之后同步调用,主要用于处理 DOM 操作、调整样式、避免页面闪烁等问题。也正因为是同步处理,所以需要避免在 LayoutEffect 做计算量较大的耗时任务从而造成阻塞。
使用:如果实在分不清两者的应用场景,可以先用 useEffect,一般问题不大;如果页面有异常,再直接替换为 useLayoutEffect 即可。