Hook
是 React 16.8.0
的新增特性,React Native 0.59
及以上版本支持 Hook
。它可以让你在不编写 class
的情况下使用 state
以及其他的 React
特性。
请注意,要启用 Hook
,所有 React
相关的 package
都必须升级到 16.8.0
或更高版本。如果你忘记更新诸如 React DOM
之类的 package
,Hook
将无法运行。
Hook
是一些可以让你在函数组件里“钩入” React state
及生命周期等特性的函数。Hook
不能在 class
组件中使用 —— 这使得你不使用 class
也能使用 React
。
HOOK
可以让我们在函数组件中使用 state
、生命周期以及其他 react 特性,而不仅限于 class
组件中使用。react hooks
的出现,标示着 react
中不会在存在无状态组件,而是包含类组件和函数组件。react hooks
即是应用在函数组件中。
如果你在编写函数组件并意识到需要向其添加一些 state
,以前的做法是必须将其它转化为 class
。现在你可以在现有的函数组件中使用 Hook
。
hooks
使我们在函数组件中拥有使用state
的能力, 就是通过 useState
来实现的,首先来看一个简单的例子,这个例子用来显示一个计数器。当你点击按钮,计数器的值就会增加:
import React, { useState } from 'react';
function App () {
// 声明一个叫 “count” 的 state 变量。
const [ count, setCount ] = useState(0)
return (
<div>
点击次数: { count }
<button onClick={() => { setCount(count + 1)}}>点我</button>
</div>
)
}
其中,useState
就是一个 Hook
。从代码中可以看到,useState
的使用非常简单,我们从 React
中拿到 useState
后,只需要在使用的地方直接调用 useState
函数就可以。 通过在函数组件里调用它来给组件添加一些内部 state
。React
会在重复渲染时保留这个 state
。useState
会返回一对值:当前状态和一个让你更新它的函数,你可以在事件处理函数中或其他一些地方调用这个函数。它类似 class
组件的 this.setState
,但是它不会把新的 state
和旧的 state
进行合并。
那么这里为什么叫 count
和 setCount
?可以使用别的名字吗,这里使用了 es6
的解构赋值,所以你可以给它起任何名字,updateCount
, doCount
、anything
,当然,为了编码规范,所以建议统一使用一种命名规范,尤其是第二个值。
useState
唯一的参数就是初始 state
。在上面的例子中,计数器是从零开始的,所以初始 state
就是 0。值得注意的是,不同于 this.state
,这里的 state
不一定要是一个对象 —— 如果你有需要,它也可以是。这个初始 state
参数只有在第一次渲染时会被用到。
当我们在使用 useState
时,修改值时传入同样的值,我们的组件会重新渲染吗,例如这样
import React, { useState } from 'react';
function App () {
const [ count, setCount ] = useState(0)
console.log('component render count')
return (
<div>
点击次数: { count }
<button onClick={() => { setCount(count)}}>点我</button>
</div>
)
}
结果是不会,这种设计思路提升了组件的渲染性能。
useState 默认值useState
支持我们在调用的时候直接传入一个值,来指定 state
的默认值,比如这样 useState(0), useState({ a: 1 }), useState([ 1, 2 ])
,还支持我们传入一个函数,来通过逻辑计算出默认值,比如这样:
import React, { useState } from 'react';
function App (props) {
const [ count, setCount ] = useState(() => {
return props.count || 0
})
return (
<div>
点击次数: { count }
<button onClick={() => { setCount(count + 1)}}>点我</button>
</div>
)
}
这个时候,就有小伙伴问了,那我组件每渲染一次,useState
中的函数就会执行一遍吗?如果是的话,就会产生性能问题。其实不会,useState
中的函数只会执行一次,我们可以做个测试:
import React, { useState } from 'react';
function App (props) {
const [ count, setCount ] = useState(() => {
console.log('useState default value function is call')
return props.count || 0
})
return (
<div>
点击次数: { count }
<button onClick={() => { setCount(count + 1)}}>点我</button>
</div>
)
}
结果如下:
声明多个 state 变量
当我们使用多个 useState
的时候,React
怎么知道哪个 state
对应哪个 useState
?答案是 React
靠的是 Hook
调用顺序。Hook
的调用顺序在每次渲染中都是相同的,所以它能够正常工作:
import React, { useState } from 'react';
function App (props) {
let count, setCount
let sum, setSum
if (count > 2) {
[ count, setCount ] = useState(0)
[ sum, setSum ] = useState(10)
} else {
[ sum, setSum ] = useState(10)
[ count, setCount ] = useState(0)
}
return (
<div>
点击次数: { count }
总计:{ sum }
<button onClick={() => { setCount(count + 1); setSum(sum - 1)}}>点我</button>
</div>
)
}
当我们在运行时改变 useState
的顺序,数据会混乱,增加 useState
, 程序会报错。
Effect Hook
可以让你在函数组件中执行副作用操作,什么是副作用呢,就是除了状态相关的逻辑,比如网络请求,监听事件,查找 dom等动作均视为副作用。
在 React
组件中有两种常见副作用操作:需要清除的和不需要清除的。我们来更仔细地看一下他们之间的区别。
无需清除的 effect
有时候,我们只想在 React
更新 DOM
之后运行一些额外的代码。比如发送网络请求,手动变更 DOM,记录日志,这些都是常见的无需清除的操作。因为我们在执行完这些操作之后,就可以忽略他们了。让我们对比一下使用 class 和 Hook 都是怎么实现这些副作用的。
使用 class 的示例
在 React
的 class
组件中,render 函数
是不应该有任何副作用的。一般来说,在这里执行操作太早了,我们基本上都希望在 React
更新 DOM
之后才执行我们的操作。
这就是为什么在 React class
中,我们把副作用操作放到 componentDidMount
和 componentDidUpdate
函数中。以 React
实现计数器的 class 组件
为例。它在 React
对 DOM
进行操作之后,立即更新了 document
的 title
属性:
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
componentDidMount() {
document.title = `You clicked ${this.state.count} times`;
}
componentDidUpdate() {
document.title = `You clicked ${this.state.count} times`;
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Click me
</button>
</div>
);
}
}
在上面的代码中,需要在 class
组件中在两个生命周期函数中编写重复的代码逻辑。
这是因为很多情况下,我们希望在组件加载和更新时执行同样的操作。从概念上说,我们希望它在每次渲染之后执行 —— 但 React
的 class
组件没有提供这样的方法。即使我们提取出一个方法,我们还是要在两个地方调用它。
现在让我们来看看如何使用 useEffect
执行相同的操作。
使用 Hook 的示例
useEffect
解决了 class 组件
存在的生命周期臃肿问题。
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
useEffect
做了什么? 通过使用这个 Hook
,你可以告诉 React
组件需要在渲染后执行某些操作。React
会保存你传递的函数(我们将它称之为 “effect”),并且在执行 DOM
更新之后调用它。在这个 effect
中,我们设置了 document 的 title 属性,不过我们也可以执行数据获取或调用其他命令式的 API。
为什么在组件内部调用 useEffect
? 将 useEffect
放在组件内部让我们可以在 effect
中直接访问 count state 变量(或其他 props)。我们不需要特殊的 API 来读取它 —— 它已经保存在函数作用域中。Hook
使用了 JavaScript 的闭包机制,而不用在 JavaScript
已经提供了解决方案的情况下,还引入特定的 React API
。
useEffect
会在每次渲染后都执行吗? 是的, 默认情况下,它在第一次渲染之后和每次更新之后都会执行。 你可能会更容易接受 effect
发生在“渲染之后”这种概念,不用再去考虑“挂载”还是“更新”。React
保证了每次运行 effect
的同时,DOM
都已经更新完毕。
需要清除的 effect
之前,我们研究了如何使用不需要清除的副作用,还有一些副作用是需要清除的。例如订阅外部数据源。这种情况下,清除工作是非常重要的,可以防止引起内存泄露!现在让我们来比较一下如何用 Class 和 Hook 来实现。
使用 Class 示例
在 React class
中,你通常会在 componentDidMount
中设置订阅,并在 componentWillUnmount
中清除它。例如,假设我们有一个 ChatAPI 模块,它允许我们订阅好友的在线状态。以下是我们如何使用 class 订阅和显示该状态:
class FriendStatus extends React.Component {
constructor(props) {
super(props);
this.state = { isOnline: null };
this.handleStatusChange = this.handleStatusChange.bind(this);
}
componentDidMount() {
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
handleStatusChange(status) {
this.setState({
isOnline: status.isOnline
});
}
render() {
if (this.state.isOnline === null) {
return 'Loading...';
}
return this.state.isOnline ? 'Online' : 'Offline';
}
}
你会注意到 componentDidMount
和 componentWillUnmount
之间的代码逻辑相互对应。使用生命周期函数迫使我们拆分这些逻辑代码,即使这两部分代码都作用于相同的副作用。
使用 Hook 示例
你可能认为需要单独的 effect
来执行清除操作。但由于添加和删除订阅代码的紧密性,所以 useEffect
的设计原则是相关的业务逻辑需要在同一个地方执行。如果 effect
返回一个函数,React
将会在执行清除操作时调用它:
import React, { useState, useEffect } from 'react';
function FriendStatus(props) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
// Specify how to clean up after this effect:
return function cleanup() {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
为什么要在 effect
中返回一个函数? 这是 effect
可选的清除机制。每个 effect
都可以返回一个清除函数。如此可以将添加和移除订阅的逻辑放在一起。它们都属于 effect
的一部分。
React
何时清除 effect
? React
会在组件卸载的时候执行清除操作。 正如之前学到的,effect
在每次渲染的时候都会执行。这就是为什么 React
会在执行当前 effect
之前对上一个 effect
进行清除。
注意:
并不是必须为 effect 中返回的函数命名。这里我们将其命名为 cleanup 是为了表明此函数的目的,但其实也可以返回一个箭头函数或者给起一个别的名字。
useEffect 生命周期
如果你熟悉 React class
的生命周期函数,你可以把 useEffect Hook
看做 componentDidMount
,componentDidUpdate
和 componentWillUnmount
这三个函数的组合。
以往我们在绑定事件、解绑事件、设定定时器、查找 dom 的时候,都是通过 componentDidMount
、componentDidUpdate
、componentWillUnmount
生命周期来实现的,而 useEffect
会在组件每次 render
之后调用,就相当于这三个生命周期函数,只不过可以通过传参来决定是否调用。
需要注意的是,useEffect
会返回一个回调函数,作用于清除上一次副作用遗留下来的状态,如果useEffect
只调用一次,该回调函数相当于 componentWillUnmount
生命周期。
具体看下面例子:
function App () {
const [ count, setCount ] = useState(0)
const [ width, setWidth ] = useState(document.body.clientWidth)
const onChange = () => {
setWidth(document.body.clientWidth)
}
useEffect(() => {
window.addEventListener('resize', onChange, false)
return () => {
window.removeEventListener('resize', onChange, false)
}
})
useEffect(() => {
document.title = count
})
return (
<div>
页面名称: { count }
页面宽度: { width }
<button onClick={() => { setCount(count + 1)}}>点我</button>
</div>
)
}
上面例子要处理两种副作用逻辑,这里我们既要处理 title,还要监听屏幕宽度改变,按照 class
的写法,我们要在生命周期中处理这两种逻辑,但在 hooks
中,我们只需要两个 useEffect
就能解决这些问题。
我们之前提到,useEffect
能够返回一个函数,用来清除上一次副作用留下的状态,这个地方我们可以用来解绑事件监听,这个地方存在一个问题,就是 useEffect
是每次 render
之后就会调用,比如 title 的改变,相当于 componentDidUpdate
,但我们的事件监听不应该每次 render
之后,进行一次绑定和解绑,就是我们需要 useEffect
变成 componentDidMount
, 它的返回函数变成 componentWillUnmount
,这里就需要用到 useEffect
函数的第二个参数。
useEffect 的第二个参数
useEffect
的第二个参数,分三种情况:
render
之后 useEffect
都会调用,相当于 componentDidMount
和 componentDidUpdate
;componentDidMount
和 componentWillUnmount
;useEffect
才会执行;具体看下面例子:
function App () {
const [ count, setCount ] = useState(0)
const [ width, setWidth ] = useState(document.body.clientWidth)
const onChange = () => {
setWidth(document.body.clientWidth)
}
useEffect(() => {
// 相当于 componentDidMount
console.log('add resize event')
window.addEventListener('resize', onChange, false)
return () => {
// 相当于 componentWillUnmount
window.removeEventListener('resize', onChange, false)
}
}, [])
useEffect(() => {
// 相当于 componentDidMount、componentDidUpdate
document.title = count
})
useEffect(() => {
// count变动时,`useEffect` 才会执行;
console.log(`count change: count is ${count}`)
}, [ count ])
return (
<div>
页面名称: { count }
页面宽度: { width }
<button onClick={() => { setCount(count + 1)}}>点我</button>
</div>
)
}
根据上面例子的运行结果,第一个 useEffect
中的 ‘add resize event’ 只会在第一次运行时输出一次,无论组件怎么 render
,都不会在输出;第二个 useEffect
会在每次组件 render
之后都执行,title 每次点击都会改变; 第三个 useEffect
, 只有在第一次运行和 count
改变时,才会执行,屏幕发生改变引起的 render
并不会影响第三个 useEffect
。
context
中的 Provider
和 Consumer
,在类组件和函数组件中都能使用,contextType
只能在类组件中使用,因为它是类的静态属性,具体如何使用 useContext
呢?
const value = useContext(MyContext);
接收一个 context
对象(React.createContext
的返回值)并返回该 context
的当前值。当前的 context
值由上层组件中距离当前组件最近的 <MyContext.Provider>
的 value prop
决定。
当组件上层最近的 <MyContext.Provider>
更新时,该 Hook
会触发重渲染,并使用最新传递给 MyContext provider
的 context value
值。即使祖先使用 React.memo
或 shouldComponentUpdate
,也会在组件本身使用 useContext
时重新渲染。
useContext
的参数必须是 context
对象本身:
useContext(MyContext)
useContext(MyContext.Consumer)
useContext(MyContext.Provider)
调用了 useContext
的组件总会在 context
值变化时重新渲染。如果重渲染组件的开销较大,你可以通过使用 memoization
来优化。
提示
如果你在接触 Hook
前已经对 context API
比较熟悉,那应该可以理解,useContext(MyContext)
相当于 class
组件中的 static contextType = MyContext
或者 <MyContext.Consumer>
。
useContext(MyContext)
只是让你能够读取 context
的值以及订阅 context
的变化。你仍然需要在上层组件树中使用 <MyContext.Provider>
来为下层组件提供 context
。
把如下代码与 Context.Provider
放在一起
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>
);
}
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
把“创建”函数和依赖项数组作为参数传入 useMemo
,它仅会在某个依赖项改变时才重新计算 memoized
值。这种优化有助于避免在每次渲染时都进行高开销的计算。
useMemo
是什么呢?它跟 memo
有关系吗?memo
就是类组件的 PureComponent
,用来做性能优化的手段,useMemo
也是,useMemo
和 Vue
的computed
计算属性类似,都是根据依赖的值计算出结果,当依赖的值未发生改变的时候,不触发状态改变,useMemo
具体如何使用呢,看下面例子:
function App () {
const [ count, setCount ] = useState(0)
const add = useMemo(() => {
return count + 1
}, [count])
return (
<div>
点击次数: { count }
<br/>
次数加一: { add }
<button onClick={() => { setCount(count + 1)}}>点我</button>
</div>
)
}
上面的例子中,useMemo
也支持传入第二个参数,用法和 useEffect
类似
useMemo
返回的值);需要注意的是,useMemo
会在渲染的时候执行,而不是渲染之后执行,这一点和 useEffect
有区别,所以 useMemo
不建议有副作用相关的逻辑。
同时,useMemo
可以作为性能优化的手段,但不要把它当成语义上的保证,将来,React 可能会选择“遗忘”以前的一些 memoized
值,并在下次渲染时重新计算它们。
useCallback
是 useMemo
的语法糖,能用 useCallback
实现的,都可以使用 useMemo
。在 react
中我们经常面临一个子组件渲染优化的问题,尤其是在向子组件传递函数props
时,每次 render
都会创建新函数,导致子组件不必要的渲染,浪费性能,这个时候,就是 useCallback
的用武之地了,useCallback
可以保证,无论 render
多少次,我们的函数都是同一个函数,减小不断创建的开销,具体如何使用看下面例子
const onClick = useMemo(() => {
return () => {
console.log('button click')
}
}, [])
const onClick = useCallback(() => {
console.log('button click')
}, [])
同样,useCallback
的第二个参数和useMemo
一样,没有区别。
const refContainer = useRef(initialValue);
useRef
返回一个可变的 ref
对象,其 .current
属性被初始化为传入的参数(initialValue
)。返回的 ref
对象在组件的整个生命周期内保持不变。
useRef
有两种用法:
render
重复声明, 类似于类组件的 this.xxx
;获取子组件实例
上面提到了一点,useRef
只能获取子组件的实例,这在类组件中也是同样的道理,具体看下面的例子:
// 使用 ref 子组件必须是类组件
class Children extends PureComponent {
render () {
const { count } = this.props
return (
<div>{ count }</div>
)
}
}
function App () {
const [ count, setCount ] = useState(0)
const childrenRef = useRef(null)
// const
const onClick = useMemo(() => {
return () => {
console.log('button click')
console.log(childrenRef.current)
setCount((count) => count + 1)
}
}, [])
return (
<div>
点击次数: { count }
<Children ref={childrenRef} count={count}></Children>
<button onClick={onClick}>点我</button>
</div>
)
}
useRef
在使用的时候,可以传入默认值来指定默认值,需要使用的时候,访问 ref.current
即可访问到组件实例。
类组件属性
有些情况下,我们需要保证函数组件每次 render
之后,某些变量不会被重复声明,比如说 Dom 节点,定时器的 id 等等,在类组件中,我们完全可以通过给类添加一个自定义属性来保留,比如说 this.xxx
, 但是函数组件没有 this
,自然无法通过这种方法使用,使用useState
来保留变量的值,会触发组件 render
,在这里完全是不需要的,我们就需要使用 useRef
来实现了,具体看下面例子:
function App () {
const [ count, setCount ] = useState(0)
const timer = useRef(null)
let timer2
useEffect(() => {
let id = setInterval(() => {
setCount(count => count + 1)
}, 500)
timer.current = id
timer2 = id
return () => {
clearInterval(timer.current)
}
}, [])
const onClickRef = useCallback(() => {
clearInterval(timer.current)
}, [])
const onClick = useCallback(() => {
clearInterval(timer2)
}, [])
return (
<div>
点击次数: { count }
<button onClick={onClick}>普通</button>
<button onClick={onClickRef}>useRef</button>
</div>
)
}
当我们们使用普通的按钮去暂停定时器时发现定时器无法清除,因为 App 组件每次 render
,都会重新申明一次 timer2, 定时器的 id 在第二次 render 时,就丢失了,所以无法清除定时器,针对这种情况,就需要使用到 useRef
,来为我们保留定时器 id,类似于 this.xxx
,这就是 useRef
的另外一种用法。
useReducer
类似 redux
中的功能,相较于 useState
,它更适合一些逻辑较复杂且包含多个子值,或者下一个 state
依赖于之前的 state
等特定场景, useReducer
总共有三个参数:
reducer
,就是一个类似 (state, action) => newState
的函数,传入上一个 state
和本次的 action
;state
,也就是默认值,是比较简单的方法;state
的逻辑提取到 reducer
外部,这也为将来对重置 state
的 action
做处理提供了便利;具体使用方法看下面的例子:
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}
function App() {
const [state, dispatch] = useReducer(reducer, {
count: 0
});
return (
<>
点击次数: {state.count}
<button onClick={() => dispatch({type: 'increment'})}>+</button>
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
</>
);
}
useImperativeHandle
可以让你在使用 ref
时自定义暴露给父组件的实例值,说简单点就是,子组件可以选择性的暴露给父组件一些方法,这样可以隐藏一些私有方法和属性,官方建议,useImperativeHandle
应当与 forwardRef
一起使用,具体如何使用看下面例子:
function Kun (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 KunKun = forwardRef(Kun)
function App () {
const [ count, setCount ] = useState(0)
const kunRef = useRef(null)
const onClick = useCallback (() => {
setCount(count => count + 1)
kunRef.current.introduce()
}, [])
return (
<div>
点击次数: { count }
<KunKun ref={kunRef} count={count}></KunKun>
<button onClick={onClick}>点我</button>
</div>
)
}
其函数签名与 useEffect
相同,但它会在所有的 DOM
变更之后同步调用 effect
。可以使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect
内部的更新计划将被同步刷新。
尽可能使用标准的 useEffect
以避免阻塞视觉更新。
提示
如果你正在将代码从 class
组件迁移到使用 Hook
的函数组件,则需要注意 useLayoutEffect
与componentDidMount
、componentDidUpdate
的调用阶段是一样的。但是,建议你一开始先用useEffect
,只有当它出问题的时候再尝试使用 useLayoutEffect
。
如果你使用服务端渲染,请记住,无论 useLayoutEffect
还是 useEffect
都无法在 Javascript
代码加载完成之前执行。这就是为什么在服务端渲染组件中引入 useLayoutEffect
代码时会触发 React
告警。解决这个问题,需要将代码逻辑移至 useEffect
中(如果首次渲染不需要这段逻辑的情况下),或是将该组件延迟到客户端渲染完成后再显示(如果直到 useLayoutEffect
执行之前
HTML 都显示错乱的情况下)。
若要从服务端渲染的 HTML 中排除依赖布局 effect
的组件,可以通过使用 showChild && <Child />
进行条件渲染,并使用 useEffect(() => { setShowChild(true); }, [])
延迟展示组件。这样,在客户端渲染完成之前,UI 就不会像之前那样显示错乱了。
useDebugValue(value)
useDebugValue
可用于在 React
开发者工具中显示自定义 hook
的标签。
例如,
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
// ...
// 在开发者工具中的这个 Hook 旁边显示标签
// e.g. "FriendStatus: Online"
useDebugValue(isOnline ? 'Online' : 'Offline');
return isOnline;
}
提示
不建议你向每个自定义 Hook
添加 debug
值。当它作为共享库的一部分时才最有价值。
延迟格式化 debug 值
在某些情况下,格式化值的显示可能是一项开销很大的操作。除非需要检查 Hook
,否则没有必要这么做。
因此,useDebugValue
接受一个格式化函数作为可选的第二个参数。该函数只有在 Hook
被检查时才会被调用。它接受 debug
值作为参数,并且会返回一个格式化的显示值。
例如,一个返回 Date 值的自定义 Hook 可以通过格式化函数来避免不必要的 toDateString 函数调用:
useDebugValue(date, date => date.toDateString());
通过自定义 Hook
,可以将组件逻辑提取到可重用的函数中。
示例代码如下:
import React, { useState, useEffect } from 'react';
function FriendStatus(props) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
现在我们假设聊天应用中有一个联系人列表,当用户在线时需要把名字设置为绿色。我们可以把上面类似的逻辑复制并粘贴到 FriendListItem 组件中来,但这并不是理想的解决方案:
import React, { useState, useEffect } from 'react';
function FriendListItem(props) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
return (
<li style={{ color: isOnline ? 'green' : 'black' }}>
{props.friend.name}
</li>
);
}
相反,我们希望在 FriendStatus 和 FriendListItem 之间共享逻辑。
目前为止,在 React
中有两种流行的方式来共享组件之间的状态逻辑: render props
和高阶组件
,现在让我们来看看 Hook
是如何在让你不增加组件的情况下解决相同问题的。
提取自定义 Hook
当我们想在两个函数之间共享逻辑时,我们会把它提取到第三个函数中。而组件和 Hook
都是函数,所以也同样适用这种方式。
自定义 Hook
是一个函数,其名称以 “use
” 开头,函数内部可以调用其他的 Hook
。 例如,下面的 useFriendStatus 是我们第一个自定义的 Hook:
import { useState, useEffect } from 'react';
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});
return isOnline;
}
此处并未包含任何新的内容——逻辑是从上述组件拷贝来的。与组件中一致,请确保只在自定义 Hook
的顶层无条件地调用其他 Hook。
与 React
组件不同的是,自定义 Hook 不需要具有特殊的标识。我们可以自由的决定它的参数是什么,以及它应该返回什么(如果需要的话)。换句话说,它就像一个正常的函数。但是它的名字应该始终以 use
开头,这样可以一眼看出其符合 Hook
的规则。
此处 useFriendStatus 的 Hook 功能是订阅某个好友的在线状态。这就是我们需要将 friendID 作为参数,并返回这位好友的在线状态的原因。
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
// ...
return isOnline;
}
现在让我们看看应该如何使用自定义 Hook。
使用自定义 Hook
我们一开始的目标是在 FriendStatus 和 FriendListItem 组件中去除重复的逻辑,即:这两个组件都想知道好友是否在线。
现在我们已经把这个逻辑提取到 useFriendStatus 的自定义 Hook 中,然后就可以使用它了:
function FriendStatus(props) {
const isOnline = useFriendStatus(props.friend.id);
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
function FriendListItem(props) {
const isOnline = useFriendStatus(props.friend.id);
return (
<li style={{ color: isOnline ? 'green' : 'black' }}>
{props.friend.name}
</li>
);
}
这段代码等价于原来的示例代码吗?等价,它的工作方式完全一样。如果你仔细观察,你会发现我们没有对其行为做任何的改变,我们只是将两个函数之间一些共同的代码提取到单独的函数中。自定义 Hook 是一种自然遵循 Hook
设计的约定,而并不是 React
的特性。
自定义 Hook
必须以 “use
” 开头吗?必须如此。这个约定非常重要。不遵循的话,由于无法判断某个函数是否包含对其内部 Hook
的调用,React
将无法自动检查你的 Hook
是否违反了 Hook
的规则。
在两个组件中使用相同的 Hook
会共享 state
吗?不会。自定义 Hook
是一种重用状态逻辑的机制(例如设置为订阅并存储当前值),所以每次使用自定义 Hook
时,其中的所有 state
和副作用都是完全隔离的。
自定义 Hook
如何获取独立的 state
?每次调用 Hook
,它都会获取独立的 state
。由于我们直接调用了 useFriendStatus,从 React
的角度来看,我们的组件只是调用了 useState
和 useEffect
。 正如我们在之前章节中了解到的一样,我们可以在一个组件中多次调用 useState
和 useEffect
,它们是完全独立的。
在多个 Hook
之间传递信息
由于 Hook
本身就是函数,因此我们可以在它们之间传递信息。
我们将使用聊天程序中的另一个组件来说明这一点。这是一个聊天消息接收者的选择器,它会显示当前选定的好友是否在线:
const friendList = [
{ id: 1, name: 'Phoebe' },
{ id: 2, name: 'Rachel' },
{ id: 3, name: 'Ross' },
];
function ChatRecipientPicker() {
const [recipientID, setRecipientID] = useState(1);
const isRecipientOnline = useFriendStatus(recipientID);
return (
<>
<Circle color={isRecipientOnline ? 'green' : 'red'} />
<select
value={recipientID}
onChange={e => setRecipientID(Number(e.target.value))}
>
{friendList.map(friend => (
<option key={friend.id} value={friend.id}>
{friend.name}
</option>
))}
</select>
</>
);
}
我们将当前选择的好友 ID 保存在 recipientID 状态变量中,并在用户从 <select>
中选择其他好友时更新这个 state
。
由于 useState
为我们提供了 recipientID 状态变量的最新值,因此我们可以将它作为参数传递给自定义的 useFriendStatus Hook:
const [recipientID, setRecipientID] = useState(1);
const isRecipientOnline = useFriendStatus(recipientID);
如此可以让我们知道当前选中的好友是否在线。当我们选择不同的好友并更新 recipientID 状态变量时,useFriendStatus Hook 将会取消订阅之前选中的好友,并订阅新选中的好友状态。
Hook
本质就是 JavaScript
函数,但是在使用它时需要遵循两条规则。可以通过 eslint-plugin-react-hooks 插件来强制执行这些规则:
Hook
;Hook
, 确保总是在你的 React
函数的最顶层调用他们。遵守这条规则,就能确保 Hook
在每一次渲染中都按照同样的顺序被调用。这让 React
能够在多次的 useState
和 useEffect
调用之间保持 hook
状态正确。React 函数
中调用 Hook
;JavaScript
函数中调用 Hook
。你可以:✅ 在 React 的函数组件中调用 Hook;
✅ 在自定义 Hook 中调用其他 Hook;
遵循此规则,确保组件的状态逻辑在代码中清晰可见。
版权说明 : 本文为转载文章, 版权归原作者所有 版权申明
原文链接 : https://shq5785.blog.csdn.net/article/details/117729579
内容来源于网络,如有侵权,请联系作者删除!