前言
Hook 是 React v16.8 新增特性。让函数组件也拥有状态。使我们具有不编写类组件也可以使用
state和其他的 React 特性的能力
为什么要使用 Hook
类组件的缺点
类组件带来了生命周期特性和 state、props 等特性的同时,也带来了一些问题
-
状态逻辑难复用: 在组件之间复用状态很难,可能要用到 render props (渲染属性)或者 HOC(高阶组件),但无论是渲染属性,还是高阶组件,都会在原先的组件外包裹一层父容器(一般都是 div 元素),导致层级冗余
-
趋向复杂难以维护: 在生命周期中混杂不相干的逻辑(如:在
componentDidMount中注册事件以及其他的逻辑、componentWillUnmount中卸载事件,这样分散不集中的写法,很容易写出 bug )。类组件中到处都是对状态的访问和处理,导致组件难以拆分成更小的组件 -
this 指向问题: 父组件给子组件传递函数时,必须绑定
this
常用的 Hook
useState
import React, { useState } from 'react'
const Example1 = () => {
// 声明⼀个叫 "count" 的 state 变量
const [count, setCount] = useState(0)
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
)
}
默认值
const [ state, setState ] = useState(initialValue)
const Example2 = props => {
const defaultCount = props.count || 0
const [ count, setCount ] = useState(defaultCount)
return (
<div>
点击次数: { count }
<button onClick={() => setCount(count + 1)}>点我</button>
</div>
)
}
虽然 defaultCount 只有在第一次渲染的时候才会⽤到,但是计算的逻辑会在组件的每次渲染中都会运行,如果复杂度过⾼的话,会很浪费资源
惰性初始 state
useState 支持我们在调⽤的时候直接传⼊一个值,来指定 state 的默认值。还⽀持我们传入⼀个函数,在函数中计算并返回初始的 state,此函数只在初始渲染时被调用
const Example2 = props => {
const [ count, setCount ] = useState(() => props.count || 0 })
return (
<div>
<span>点击次数: { count }</span>
<button onClick={() => setCount(count + 1)}>点我</button>
</div>
)
}
组件每渲染一次,useState 中的函数就会执⾏⼀遍、浪费性能吗?我们可以做个测试
const Example3 = props => {
const [ count, setCount ] = useState(() => {
console.log('useState default value function is call')
return props.count || 0
})
return (
<div>
<span>点击次数: { count }</span>
<button onClick={() => setCount(count + 1)}>点我</button>
</div>
)
}
可以看到,useState 中的 console 只被执行一次,即函数只会执行⼀次
获取上一轮的值
在使用 useState 的第二个参数时,如果我们想要获取上⼀轮该 state 的值,只需要在 useState 第⼆个参数传入函数形式。也就是上面的例子中 setCount 使⽤时,传⼊⼀个函数
// 该函数的参数就是上一轮的 `state` 的值
setCount(count => count + 1)
注意
1、你可能想知道: 为什么叫 useState ⽽不叫 createState?
使用 "create" 可能不是很准确。因为 state 只在组件首次渲染的时被创建,在下一次渲染时,都是更新 state,useState 只返回当前的 state,否则它就不是 "state" 了!这也是 Hook 名字总以 use 开头的一个原因
2、⽅括号有什么用?
const [count, setCount] = useState(0)
这种语法叫数组解构。它意味着我们同时创建了 count 和 setCount 两个变量,count 的值为 useState 返回的第一个值,setCount 是返回的第二个值
它等价于以下代码:
var countStateVariable = useState(0) // 返回⼀个有两个元素的数组
var count = countStateVariable[0] // 数组⾥的第⼀个值
var setCount = countStateVariable[1] // 数组⾥的第⼆个值
当我们使⽤ useState 定义 state 变量时候,它返回一个有两个值的数组。第⼀个值是当前的 state ,第二个值是更新 state 的函数
3、我们并没有传递 this 给 React,是怎么知道 useState 对应的是哪个组件?
React 保持对当前渲染中的组件的追踪。多亏了 React Hook 规范,我们得知 Hook 只会在 React 组件中被调用(或⾃定义 Hook —— 同样只会在 React 组件中被调用)
4、多个 useState 的情况,React 是如何识别哪个是哪个呢
其实很简单,它是靠第一次执行的顺序来记录的。每个组件存放 useState 是以链表的形式,每使用⼀个新的 useState ,就向链表末尾添加⼀个 useState。 这就是多个 useState 调用会得到各自独立的本地 state 的原因
只在最顶层使用 Hook。不可以嵌套在任何循环、
if判断或函数中。因为它导致我们的state变得可选,这会破坏 React 的useState链表顺序,导致我们的state拿不到对应的正确的值
5、不像 Class 中的 this.setState ,更新 state 变量总是替换它而不是合并它
const Box = () => {
const [state, setState] = useState({
top: 0,
left: 0,
width: 100,
height: 100
})
// ...
useEffect(() => {
handleWindowMouseMove = e => {
// 展开 「...state」 以确保我们没有 「丢失」 width 和 height
setState(state => ({
...state,
left: e.pageX,
top: e.pageY
})
)}
// 注意:这是个简化版的实现
window.addEventListener('mousemove', handleWindowMouseMove)
return () => (
window.removeEventListener('mousemove', handleWindowMouseMove)
)
}, [])
}
6、我该使用单个还是多个 state 变量?
把多个 state 都放在同一个 useState 调用中,或是每一个字段都对应一个 useState 调用,这两⽅式都行得通。你需要在这两个极端之间找到平衡,然后把相关 state 组合到几个独立的 state 变量时,组件会更加的可读
如果 state 的逻辑开始变得复杂,我们推荐用 reducer 来管理它,或使用⾃定义 Hook 进行包装
useEffect
Effect Hook 中可以执行任何在函数组件中带来副作用的操作,⽐如⽹络请求,监听事件,查找 DOM
Effect Hook 告诉 React 在完成对 DOM 的更改后运行你的“副作用”函数。可以看做 React 类组件的⽣命周期函数中 componentDidMount, componentDidUpdate 和 componentWillUnmount 这三个生命周期函数的组合
import React, { useState, useEffect } from 'react'
const Example4 = () => {
const [count, setCount] = useState(0)
// Similar to componentDidMount and componentDidUpdate:
useEffect(() => {
// Update the document title using the browser API
document.title = `You clicked ${count} times`
})
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
)
}
第二个参数
有以下三种情况:
1、什么都不传,组件每次 render 之后 useEffect 都会调用
2、传⼊一个空数组 [],则只会调用一次,之后它永远都不会重复执行
3、传⼊一个数组,其中包括变量,只有这些变量变动时,useEffect 才会执⾏
如果某些特定值在两次渲染之间没有发生变化,可以通知 React 跳过对 Effect Hook 的调用,只要传递数组作为 useEffect 的第二个可选参数即可
// 仅在 count 更改时更新
useEffect(() => {
document.title = `You clicked ${count} times`
}, [count])
如果想 Effect Hook 执行只运⾏一次 (仅在组件挂载和卸载时执行),可以传递一个空数组([])作为第二个参数。这就告诉 React 你的 Effect 不依赖于 props 或 state 中的任何值,所以它永远都不需要重复执行
// 仅在第一次渲染时执行
useEffect(() => {
document.title = `You clicked ${count} times`
}, [])
清除effect
通常,组件卸载时需要清除 Effect 创建的诸如订阅或计时器 ID 等资源。要实现这一点,useEffect 函数需返回⼀个清除函数。作用于清除上一次副作用遗留下来的状态,如果该 Effect 只调用一次,该回调函数相当于 componentWillUnmount ⽣命周期。以下就是一个创建订阅的例子
useEffect(() => {
const subscription = props.source.subscribe()
return () => {
subscription.unsubscribe()
}
}, [props.source])
为防⽌内存泄漏,清除函数会在组件卸载前执⾏。另外,如果组件多次渲染(通常如此),则在执行下⼀个 Effect 之前,上一个 Effect 会被清除
注意
1、Effect Hook 会在每次渲染后都执行吗?
默认情况下,它在第一次渲染之后和每次更新之后都会执行(但是,可以添加依赖数组来控制是否执行)
你可能会更容易接受 Effect Hook 发⽣在 DOM 渲染之后这种概念,不用再去考虑挂载还是更新。React 保证了每次运行 Effect Hook 的同时,DOM 都已经更新完毕
2、如果 Effect Hook 的依赖频繁变化,我该怎么办?
const Example5 = () => {
const [count, setCount] = useState(0)
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1); // 这个 Effect 依赖于 `count` state
}, 1000)
return () => clearInterval(id)
}, [count])
return <h1>{count}</h1>
}
指定 count 作为依赖会导致每次改变发生时定时器都被重置。事实上,每个 setInterval 在被清除前(类似于 setTimeout)都会调用一次。但这并不是我们想要的。要解决这个问题,我们可以使⽤ setState 的函数式更新形式。它允许我们指定 state 该如何改变而不用引用当前 state:
const Example6 = () => {
const [count, setCount] = useState(0)
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1) // 在这不依赖于外部的 `count` 变量
}, 1000)
return () => clearInterval(id)
}, []) // 我们的 Effect 不使⽤组件作⽤域中的任何变量
return <h1>{count}</h1>
}
useContext
上下文
const value = useContext(MyContext)
接收一个 context 对象(React.createContext 的返回值)并返回该 context 当前值。当前 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定
// 创建一个 context
const Context = createContext(0)
三种写法
// 组件一, Consumer 写法
class Item1 extends PureComponent {
render () {
return (
<Context.Consumer>
{(count) => (<div>{count}</div>) }
</Context.Consumer>
)
}
}
缺点:嵌套复杂,
Consumer第⼀个⼦节点必须为函数,⽆形增加了工作量
// 组件⼆, contextType 写法
class Item2 extends PureComponent {
static contextType = Context render () {
const count = this.context
return (
<div>{count}</div>
)
}
}
缺点:只⽀持类组件,⽆法在多
context的情况下使⽤
// 组件三, useContext 写法
const Item3 = () => {
const count = useContext(Context);
return (
<div>{ count }</div>
)
}
const Example7 = () => {
const [count, setCount] = useState(0)
return (
<div>
点击次数: { count }
<button onClick={() => setCount(count + 1)}>点我</button>
<Context.Provider value={count}>
<Item1 />
<Item2 />
<Item3 />
</Context.Provider>
</div>
)
}
缺点:不需要嵌套,多
context写法简单
当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 provider context 的 value 值。即使祖先使用 React.memo 或 shouldComponentUpdate,也会在组件本身使用 useContext 时重新渲染
注意
1、别忘记 useContext 的参数必须是 context 对象本身
2、useContext(MyContext) 相当于 Class 组件中的 static contextType = MyContext 或者 <MyContext.Consumer>
useContext(MyContext) 只是让你能够读取 context 的值以及订阅 context 的变化。你仍然需要在上层组件树中使⽤用 <MyContext.Provider> 来为下层组件提供上下文来源
useReducer
useState 的替代方案。它接收一个形如 (state, action) => newState 的 reducer,
并返回当前的 state 以及与其配套的 dispatch 方法
const [state, dispatch] = useReducer(reducer, initialArg, init)
在某些场景下,useReducer 会比 useState 更适⽤,例如 state 逻辑较复杂且包含多个子值,或者下⼀个 state 依赖于之前的 state 等。并且,使用 useReducer 还能给那些会触发深更新的组件做性能优化,因为你可以向⼦组件传递 dispatch ⽽不是回调函数
const initialState = 0;
const reducer = (state, action) => {
switch (action) {
case 'increment': return state + 1;
case 'decrement': return state - 1;
case 'reset': return 0;
default: throw new Error('Unexpected action');
}
};
首先,我们定义了初始化的 initialState 以及 reducer。注意这里的 state 仅是一个数字,不是对象。熟悉 Redux 的开发者可能是困惑的,但在 hook 中是适宜的。此外,action 仅是一个普通的字符串。
下面是一个使用 useReducer 的组件
const Example01 = () => {
const [count, dispatch] = useReducer(reducer, initialState);
return (
<div>
{count}
<button onClick={() => dispatch('increment')}>+1</button>
<button onClick={() => dispatch('decrement')}>-1</button>
<button onClick={() => dispatch('reset')}>reset</button>
</div>
);
};
useMemo
useMemo 不会影响逻辑,只是做性能优化。 useEffect 是副作⽤,是在渲染之后完成的,而useMemo 是在渲染期间完成的
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b])
const Example10 = () => {
const [ count, setCount ] = useState(0)
const add = useMemo(() => {
return count + 1
}, [count])
return (
<div>
<span>点击次数: { count }<br/>次数加⼀: { add }</span>
<button onClick={() => { setCount(count + 1) }}>点我</button>
</div>
)
}
第⼆个参数
useMemo 也⽀持传入第二个参数,用法和 useEffect 类似
1、不传数组,每次重渲染都会重新计算
2、空数组,只会组件第一次挂载时计算⼀次,后续重渲染均不再计算
3、依赖对应的值,当对应的值发生变化时,才会重新计算(可以依赖另外一个 useMemo 返回的值)
4、把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进⾏⾼开销的计算
注意
1、记住,传⼊ useMemo 的函数在渲染期间执行
因此,请不要在这个函数内部执行与渲染⽆关的操作,诸如副作用这类的操作属于 useEffect 的适用范畴,⽽不是 useMemo
2、可以把 useMemo 作为性能优化的⼿段,但不要把它当成语义上的保证
将来,React 可能会选择“遗忘”以前的⼀些 memoized 值,并在下次渲染时重新计算它们
useCallback
useCallback 可以理解为 useMemo 的语法糖
const memoizedCallback = useCallback(() => {
doSomething(a, b)
}, [a, b])
针对⼦组件渲染优化的问题,尤其是当向子组件传递函数 props 时,每次 render 都会创建新函数,导致⼦组件不必要的渲染,浪费性能,这个时候,就是 useCallback 的用武之地
const Counter = memo(function Counter(props) {
console.log("counter render")
return (
<div>
Counter: <h1 onClick={props.onClick}>{props.count}</h1>
</div>
)
})
const Example11 = () => {
const [count, setCount] = useState(0)
const double = useMemo(() => {
return count * 2 },[count === 3])
const onClick = () => { console.log("click")
}
return (
<div>
<button type="button" onClick={() => setCount(count + 1)}>
Click{count},double:{double}
</button>
<Counter count={double} onClick={onClick}/>
</div>
)
}
注意了:在这里是 count = 3 的期间 double 会重新计算,也就是当 count 变为 3 , double 为 6时,重新计算,得到 count 为 4,double 为 8的结果,以后,double 就不会重新计算了。count 变化的时候,⽗组件重新渲染,导致 onClick 句柄也发⽣了变化,就导致组件也发生重新渲染了, 但是实际上 onClick 句柄不应该发生变化
使⽤ useMemo 或者 useCallback 锁定 onClick 句柄
const onClick = useMemo(() => {
return () => {
console.log("click")
}
},[]) // 表示只有在第⼀次执⾏
const onClick = useCallback(() => {
return () => {
console.log("click")
}
},[])
把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的子组件,它将⾮常有⽤
经过优化的子组件:使用引用相等性去避免⾮必要渲染的子组件(例如使用 shouldComponentUpdate 的子组件)
useRef
useRef 有什么作⽤呢,总共有两种⽤法
1、获取子组件的实例
2、在函数组件中的⼀个全局变量,不会因为重复 render 重复申明, 类似于类组件的 this.xxx const refContainer = useRef(initialValue)
useRef 返回⼀个可变的 Ref 对象,其 current 属性被初始化为传入的参数
(initialValue)。返回的 Ref 对象在组件的整个⽣命周期内保持不变
const 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>
</>
)
}
本质上,useRef 就像是可以在其 current 属性中保存⼀个可变值的“盒子”。 你应该熟悉 Ref 这⼀种访问 DOM 的主要⽅式。如果你将 Ref 对象以 <div ref={myRef} />
形式传入组件,则⽆论该节点如何改变,React 都会将 Ref 对象的 current 属性设
置为相应的 DOM 节点
然而,useRef ⽐ ref 属性更有用。它可以很方便地保存任何可变值,其类似于在 class 中使用实例字段的⽅式。这是因为它创建的是一个普通 Javascript 对象。而 useRef 和⾃建⼀个 {current: ...} 对象的唯一区别是,useRef 会在每次渲染时返回同⼀个 Ref 对象
请记住,当 Ref 对象内容发⽣变化时,useRef 并不会通知你。变更 current 属性不会引发组件重新渲染
Ref Hook 不仅可以⽤于 DOM Refs。Ref 对象是⼀个 current 属性可变,且可以容纳任意值的通用容器,类似于 Class 的实例属性
const Example12 = () => {
const [ count, setCount ] = useState(0) const timer = useRef(null)
let timer2
useEffect(() => {
let id = setInterval(() => {
setCount(count + 1)
}, 1000)
timer.current = id timer2 = id
return () => {
clearInterval(timer.current)
}
}, [count])
const onClickRef = useCallback(() => { clearInterval(timer.current) }, [])
const onClick = useCallback(() => { clearInterval(timer2)}, [timer2])
return (
<div>
点击次数: { count }
<button onClick={onClick}>普通</button>
<button onClick={onClickRef}>useRef</button>
</div>
)
}
当我们使⽤普通的按钮去暂停定时器时,发现定时器⽆法清除,因为组件每次 render,都会重新申明一次 timer2, 定时器的 ID 在第二次 render 时,就丢失了,所以⽆法清除定时器。针对这种情况,就需要使⽤到 useRef,来保留定时器 ID,类似于类组件中 this.xxx,这就是 useRef 的另外⼀种⽤法
useImperativeHandle
forwardRef
React.forwardRef 字面意思理解为转发 ref,它会创建一个 React 组件,这个组件能够将其接受的 ref 属性转发到其组件树下的另一个组件中
useImperativeHandle(ref, createHandle, [deps])
useImperativeHandle 可以让你在使用 ref 时⾃定义子组件暴露给⽗组件的实例值。在⼤多数情况下,应当避免使用 ref 这样的命令式代码。useImperativeHandle 应与 forwardRef ⼀起使用:
import { forwardRef, useRef, useImperativeHandle } from 'react';
const ExposeFocusInput = forwardRef((props, ref) => {
const realInputRef = useRef(null);
useImperativeHandle(ref, () => ({
// 曝光 focus
focus:() => realInputRef.current.focus();
}));
return <input {...props} ref={realInputRef} />;
});
const Form: React.FC = () => {
const inputRef = useRef(null);
const handleClick = () => {
inputRef.current.focus();
}
return (
<>
<ExposeFocusInput ref={inputRef} />
<button onClick={handleClick}>
Focus the input
</button>
</>
);
}
在本例中,⽗组件的inputRef.current中会有获取ExposeFocusInput组件中input的focus()方法
⾃定义 Hook
⾃定义 Hook 是⼀个函数,其名称常以 "use" 开头,函数内部可以调⽤其他的 Hook。 自定义 Hook 是⼀种⾃然遵循 Hook 设计的约定,并不是 React 的特性
import React, { useState, useRef, useEffect } from 'react'
const useCounter = (count) => {
return (
<div>
Counter: <h1>{count}</h1>
</div>
)
}
const useCount = (defaultCount) => {
const [count, setCount] = useState(0)
const it = useRef()
useEffect(() => {
it.current = setInterval(() => {
setCount(count => count+1)
}, 1000)
}, [])
useEffect(() => {
if (count >= 10) {
clearInterval(it.current)
}
}, [count])
return [count, setCount]
}
const Example13 = props => {
const [count, setCount] = useCount(0)
const Counter = useCounter(count)
return (
<div>
<button type="button" onClick={()=>{ setCount(count+1) }}>
Click---{count}
</button>
{Counter}
</div>
)
}
注意
1、自定义 Hook 必须以 "use" 开头吗?
必须如此。这个约定⾮常重要。不遵循的话,由于⽆法判断某个函数是否包含对其内部 Hook 的调用,React 将无法⾃动检查你的 Hook 是否违反了 Hook 的规则
2、在两个组件中使用相同的 Hook 会共享 state 吗?
不会。⾃定义 Hook 是⼀种重⽤状态逻辑的机制(例如设置为订阅并存储当前值),所以每次使用⾃定义 Hook 时,其中的所有 state 和副作⽤都是完全隔离的。