Hook
入门及特性
1.从React 16.8开始支持得新增特性,可以在不使用类class的情况下使用state的其他React特性
2.特性:
(1)完全可选
(2)100%向后兼容
(3)现在可用
Hook概念
hook是可以在函数组件中钩入React state及生命周期等特性的函数。
- Hook是一个函数
- Hook有些特殊,它可以在函数组件中钩入React的特性
- Hook它们的名字通常以use开头
就是可以在函数组件中也可以使用state以及生命钩子函数等react特性
注:hook不能在类组件中使用
State Hook
- useState Hook是允许在React函数组件中添加state的Hook
- 使用场景: 在编写函数组件并需要向其添加一些state
注 Hook 在 class 内部是不起作用的,但可以使用它们来取代 class 。
使用State Hook的函数组件和等价的class组件的对比
import React, { useState } from 'react'
function Example() {
// 声明一个叫 "count" 的 state 变量
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
)
}
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
}
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Click me
</button>
</div>
)
}
}
- 声明State变量
- class中通过设置this.state为count:0初始化count state为0
- 在函数组件中直接调用State Hook来初始化count
constructor(props) {
super(props)
this.state = {
count: 0
}
}
// 对比
const [count, setCount] = useState(0)
注: useState的调用都做了些什么?
(1)定义了一个‘state变量’,变量的名称可以是任何名字
(2)useState() 方法里面需要的唯一的参数就是初始 state。不同于class可以按照需要使用任何数据类型对齐进行赋值,不一定使用对象。
(3)useState() 的返回值是当前的state变量以及更新state的函数。通过数组解构赋值
的方式可以获取声明的state变量和更新函数
- 读取State变量
// class
<p>You clicked {this.state.count} times</p>
//hook
<p>You clicked {count} times</p>
- 更新State变量方式
(1)class使用this.setState({})
(2)hook使用State Hook返回的更新state变量的函数
// class
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Click me
</button>
// hook
<button onClick={() => setCount(count + 1)}>
Click me
</button>
State Hook的使用
(1)从react中引入useState Hook
(2)声明一个函数组件
(3)在函数组件内部通过调用useState Hook声明了一个新的state变量。通过数组解构赋值获取useState Hook的返回值(含有当前state和更新state的函数的数组)
(4)在其他函数中使用更新state的函数通过传递形参更改state数据,React重新渲染DOM
使用useState定义state变量时候,它返回一个有两个值的数组。第一个值是当前的state,第二个值是更新state的函数
const [fruit, setFruit] = useState('banana')
使用多个state变量
- 如果需要使用多个state变量,可以使用useState声明多个变量
// 声明多个 state 变量
const [age, setAge] = useState(42);
const [fruit, setFruit] = useState('banana');
const [todos, setTodos] = useState([{ text: '学习 Hook' }]);
- age,fruit 和 todos为当前函数组件的局部变量,并且可以单独更新
function handleOrangeClick() {
// 和 this.setState({ fruit: 'orange' }) 类似
setFruit('orange');
}
可以在函数组件中声明多个state变量,也可以使用对象和数组来声明,方便管理。为了方便于管理,可以将相关的数据归为一组
区别:class中更改state中的数据是通过setState,而hook是使用useState提供的state更新函数。setSate是更改this.state中的数据,而hook更新state变量是替换state变量而不是合并。
小结
-
hook定义:hook是可以在函数组件中钩入React state及生命周期等特性的特殊函数。
-
useState Hook可以在函数组件中声明state变量
-
useState Hook使用
(1) 从Rect中导入useState Hook,在函数组件中存储内部state
(2)通过useState Hook声明一个新的state变量
useState Hook调用会返回一个包含state变量初始值和更新state变量的函数,通过数组解构接收返回值
(3)在事件中通过返回的更新函数,更新state变量
(4)React重新渲染组件
(5)可以使用useState Hook可以声明多个state变量
可以将相关数据分为一组进行声明。
注:-
Hook 在 class 内部不起作用
-
state变量可以是任意数据类型,不一定是对象
-
可以使用对象或数组将相关的数据存储为一组
-
更新state变量是通过替换的方式,并非合并
-
import React, { useState } from 'react';
function Example() {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}
useEffect Hook
- 它能在函数组件中执行副作用,并且它与 class 中的生命周期函数极为类似。
- 数据获取,设置订阅以及手动更改 React 组件中的 DOM 都属于副作用。
解释什么是副作用?
首先解释纯函数(Pure function):给一个 function 相同的参数, 永远会返回相同的值,并且没有副作用;
这个概念拿到 React 中,就是给一个 Pure component 相同的 props, 永远渲染出相同的视图,并且没有其他的副作用;
纯组件的好处是,容易监测数据变化、容易测试、提高渲染性能等;
副作用(Side Effect)是指一个 function 做了和本身运算返回值无关的事,比如:修改了全局变量、修改了传入的参数、甚至是 console.log(),所以 ajax 操作,修改 dom 都是算作副作用的;
在React组件中执行的数据获取、订阅或手动修改DOM,这些操作都称为“副作用”,或简称“作用”
- useEffect Hook作用:给函数组件增加操作副作用的能力,和class组件中的componentDidMount、componentDidUpdate和componentWillUnmount具有相同的用途
useEffect Hook使用示例
- 使用示例
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
// Similar to componentDidMount and componentDidUpdate:
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比作class组件中componenDidMount、componentDidUpdate和componentWillUnmount三个周期函数的组合
React中两种常见副作用操作
无需清除的effect
- .在 React 更新 DOM 之后运行的一些额外代码。如发送网络请求,手动变更DOM,记录日志,在执行完这些操作后就可以忽略它们
- 对比class和hook实现这些副作用
需要清除的effect
如订阅外部数据源等都需要清除,可以防止引起内存泄漏
在class和hook中无需清除的effect操作
(1)class示例
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>
);
}}
(2)使用hook示例
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做了什么?
- 使用useEffect Hook,告诉React组件需要在渲染后执行某些操作。
- React会保存通过useEffect传递的函数(称为“effect”),并在执行DOM更新之后调用它。
- 在组件内部调用useEffect可以在effect中直接访问组件中的props和state,不需要特殊的API来读取它——它已经保存在函数作用域中
- useEffect会在每次渲染后都执行,在第一次渲染之后和每次更新之后都会执行
比较:①class组件中把副作用操作放到coponentDidMount和componentDidUpdate函数中,
②hook把副作用操作放到了useEffect调用的实参
在class和hook中需要清除的effect操作
(1)在class中示例
有一个 ChatAPI 模块,它允许我们订阅好友的在线状态
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';
}
}
在class中需要清除的effect一般会在componentDidMoun设置订阅,并在compontWillUnmount中清除
- 使用hook与class等价的示例
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 返回一个函数,React 将会在执行清除操作时调用它。
- React 会在组件卸载的时候执行清除操作。React 会在执行当前 effect 之前对上一个 effect 进行清除。
- effect和返回的函数可以为匿名
使用多个Effect实现关注点分离
- 使用 Hook 其中一个目的就是要解决 class 中生命周期函数经常包含不相关的逻辑,但又把相关逻辑分离到了几个不同方法中的问题。
- Hook可以使用多个effect,可以按照用途分离副作用操作。区别于class将不相关的代码放在一起,将相关代码放在两个周期函数中的方式。
// 需要清除和不需要清除的副作用操作整合
// class
constructor(props) {
super(props);
this.state = { count: 0, isOnline: null };
this.handleStatusChange = this.handleStatusChange.bind(this);
};
componentDidMount() {
document.title = `You clicked ${this.state.count} times`;
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentDidUpdate() {
document.title = `You clicked ${this.state.count} times`;
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
// hook
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
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);
};
});
- Hook允许按照用途将代码分离,Rect会按照effect声明的顺序依次调用组件中的每一个effect。
- Effect在每次数据更新后都会执行,在卸载时或调用effect之前都会调用的清除函数,清除上一个effect。
优点:可以减少逻辑Bug的产生。 - 每次渲染之前都会清除上一个effect,在运行下一个effect,保证数据的实时更新。
- 避免了在 class 组件中因为没有处理更新逻辑而导致常见的 bug
性能优化
如果某些特定值在两次重渲染之间没有发生变化,可以避免重复渲染
- 在 class 组件中,我们可以通过在 componentDidUpdate 中添加对 prevProps 或 prevState 的比较逻辑解决
- hook将这个需求内置到useEffect的Hook API中
可以添加第二个实参,通过比较第二个实参后一次的值与前一次的值,如果数据并无变化可以跳过对effect的调用,对有清除操作的effect同样适用
注:- 如果调用useEffect Hook添加第二个实参,要确保数组中包含了所有外部作用域中会随时间变化并且在 effect 中使用的变量,否则你的代码会引用到先前渲染中的旧变量。
- 如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),避免重复执行,可以传递一个空数组([])作为第二个参数。等于告诉React当前effect不依赖于props或state中的任何值,仅执行一次(有更好的方法避免,这并不是一个好方法)
// class
componentDidUpdate(prevProps, prevState) {
if (prevState.count !== this.state.count) {
document.title = `You clicked ${this.state.count} times`;
}
}
// hook
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count])
小结
- 副作用分为两种:需要清除的effect和不需要清除的effect
- useEffect Hook的使用
(1)从Rect中引入useEffect Hook
(2)通过调用useEffect Hook传递一个函数实参(称为effect),React保存传递的函数,并且在执行DOM更新之后调用effect
(3)在需要清除的副作用effect中可以返回一个函数,将操作添加和清除放在一起,React会在组件卸载的时候执行清除操作。
(4)使用多个Effect实现关注点分离,可以减少逻辑Bug的产生。
(5)可以在调用useEffect Hook添加第二个实参,即effect中使用到的state变量的数组,要保证该数组所有外部作用域中会随时间变化并且在 effect 中使用的变量。Hook可以使用多个effect,可以按照用途分离副作用操作。将不同功能的操作整合到一起。
通过比较第二个实参后一次的值与前一次的值,如果数据并无变化可以跳过对effect的调用,对有清除操作的effect同样适用,减少不必要的操作
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
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);
};
})
// 传入第二个实参
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]);
Hook 使用规则
Hook 就是 JavaScript 函数,但是使用它们会有两个额外的规则:
- 只能在函数最外层调用 Hook
注:
(1)不要在循环,条件或嵌套函数中调用 Hook
(2)确保每次渲染中都按照同样的顺序调用,让 React 能够在多次的 useState 和 useEffect 调用之间保持 hook 状态的正确。 - 只能在React的函数组件中调用Hook,或自定义的Hook中调用其他Hook
注:
(1)不要在其他 JavaScript 函数中调用
目的:遵循Hook规则,确保组件的状态逻辑在代码中清晰可见,保证Rect在每一次渲染中调用Hook顺序正确 - 扩展ESLint强制执行Hook规则的插件
eslint-plugin-react-hooks
详见:React Hook规则
原因:如果将hook放到判断、循环或嵌套函数中会打乱React每次渲染Hook的顺序,不过可以将条件放到effect中
自定义Hook
作用:可以将React中提供的Hook组合到定制的Hook中,以复用不同组件之间常见的状态逻辑。
React中两种共享组件之间状态逻辑的方式:render props和高阶组件
- 自定义Hook定义:自定义 Hook 是一个函数,名称以 “use” 开头,函数内部可以调用其他的 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
function FriendStatus(props) {
const isOnline = useFriendStatus(props.friend.id);
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
- 自定义Hook可以根据需要自由决定它的参数和返回值
- 自定义 Hook 是一种自然遵循 Hook 设计的约定,而并不是 React 的特性。
- 自定义Hook名称必须以‘use’开头,方便linter插件检测使用自定义Hook是否违背Hook规则
- Hook是一种复用状态逻辑的方式,它不复用 state 本身。,每次调用自定义Hook都会创建独立的state,因此可以在同一组件中多次调用同一自定义Hook
例子:
const friendList = [
{ id: 1, name: 'Phoebe' },
{ id: 2, name: 'Rachel' },
{ id: 3, name: 'Ross' }
];
function ChatRecipientPicker() {
const [recipientID, setRecipientID] = useState(1);
// 将recipientID传递给自定义Hook更改在线状态
const isRecipientOnline = useFriendStatus(recipientID);
return (
<div>
<!--选择框选择选项更改recipientID值-->
<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>
<div/>
)
}