还不会Hook?一份React Hook学习笔记
Hook 是 React 16.8.0 版本增加的新特性,可以在函数组件中使用 state
以及其他的 React 特性。
✌️为什么要使用 Hook?
在组件之间复用状态逻辑很难
由
providers
,consumers
,高阶组件,render props
等其他抽象层组成的组件会形成嵌套地狱,使用 Hook 可以从组件中提取状态逻辑,使得这些逻辑可以单独测试并复用。Hook 使你在无需修改组件结构的情况下复用状态逻辑。复杂组件难以理解
每个生命周期常常包含一些不相关的逻辑。例如,组件常常在
componentDidMount
和componentDidUpdate
中获取数据。但是,同一个componentDidMount
中可能也包含很多其它的逻辑,如设置事件监听,而之后需在componentWillUnmount
中清除。相互关联且需要对照修改的代码被进行了拆分,而完全不相关的代码却在同一个方法中组合在一起。难以理解的 class
下面介绍几个常用的Hook。
2. useState
useState
让函数组件也可以有state
状态,并进行状态数据的读写操作。
const [xxx, setXxx] = useState(initValue); // 解构赋值
📐useState() 方法
参数:
第一次初始化指定的值在内部作缓存。可以按照需要使用数字或字符串对其进行赋值,而不一定是对象。
如果想要在state
中存储两个不同的变量,只需调用 useState()
两次即可。
返回值:
包含2个元素的数组,第1个为内部当前状态值,第2个为更新状态值的函数,一般直接采用解构赋值获取。
📐setXxx() 的写法
setXxx(newValue)
:参数为非函数值,直接指定新的状态值,内部用其覆盖原来的状态值。
setXxx(value => newValue)
:参数为函数,接收原本的状态值,返回新的状态值,内部用其覆盖原来的状态值。
📐 完整示例
const App = () => {
const [count, setCount] = useState(0);
const add = () => {
// 第一种写法
// setCount(count + 1);
// 第二种写法
setCount(count => count + 1);
};
return (
<Fragment>
<h2>当前求和为:{count}</h2>
<button onClick={add}>点我+1</button>
</Fragment>
);
};
useState
就是一个 Hook,唯一的参数就是初始state
,在这里声明了一个叫count
的 state 变量,然后把它设为0
。React会在重复渲染时记住它当前的值,并且提供最新的值给我们的函数。我们可以通过调用setCount
来更新当前的count
。
在函数中,我们可以直接用 count
:
<h2>当前求和为:{count}</h2>
更新state
:
setCount(count + 1);
setCount(count => count + 1);
📐 使用多个 state 变量
可以在一个组件中多次使用State Hook
:
// 声明多个 state 变量
const [age, setAge] = useState(42);
const [fruit, setFruit] = useState('banana');
const [todos, setTodos] = useState([{ text: '学习 Hook' }]);
📌不是必须要使用多个state
变量,仍然可以将相关数据分为一组。但是,不像 class 中的 this.setState
,useState
中更新state
变量是替换。不是合并。
3. useEffect
useEffect
可以在函数组件中执行副作用操作(用于模拟类组件中的生命周期钩子)。
React 中的副作用操作:
- 发
ajax
请求数据获取 - 设置订阅 / 启动定时器
- 手动更改真实 DOM
📐 使用规则
useEffect(() => {
// 在此可以执行任何带副作用操作
// 相当于componentDidMount()
return () => {
// 在组件卸载前执行
// 在此做一些收尾工作, 比如清除定时器/取消订阅等
// 相当于componentWillUnmount()
};
}, [stateValue]); // 监听stateValue
// 如果省略数组,则检测所有的状态,状态有更新就又调用一次回调函数
// 如果指定的是[], 回调函数只会在第一次render()后执行一次
可以把 useEffect
看做如下三个函数的组合:
componentDidMount()
componentDidUpdate()
componentWillUnmount()
📐 每次更新的时候都运行 Effect
// ...
useEffect(() => {
// ...
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
调用一个新的 effect 之前会对前一个 effect 进行清理。下面按时间列出一个可能会产生的订阅和取消订阅操作调用序列:
// Mount with { friend: { id: 100 } } props
ChatAPI.subscribeToFriendStatus(100, handleStatusChange); // 运行第一个 effect
// Update with { friend: { id: 200 } } props
ChatAPI.unsubscribeFromFriendStatus(100, handleStatusChange); // 清除上一个 effect
ChatAPI.subscribeToFriendStatus(200, handleStatusChange); // 运行下一个 effect
// Update with { friend: { id: 300 } } props
ChatAPI.unsubscribeFromFriendStatus(200, handleStatusChange); // 清除上一个 effect
ChatAPI.subscribeToFriendStatus(300, handleStatusChange); // 运行下一个 effect
// Unmount
ChatAPI.unsubscribeFromFriendStatus(300, handleStatusChange); // 清除最后一个 effect
📐 通过跳过 Effect 进行性能优化
如果某些特定值在两次重渲染之间没有发生变化,可以通知 React 跳过对 effect 的调用,只要传递数组作为 useEffect
的第二个可选参数即可:
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新
如果 count
的值是 5
,而且组件重渲染的时候 count
还是等于 5
,React 将对前一次渲染的 [5]
和后一次渲染的 [5]
进行比较。因为数组中的所有元素都是相等的(5 === 5
),React 会跳过这个 effect,这就实现了性能的优化。
当渲染时,如果 count
的值更新成了 6
,React 将会把前一次渲染时的数组 [5]
和这次渲染的数组 [6]
中的元素进行对比。这次因为 5 !== 6
,React 就会再次调用 effect。
📌 如果数组中有多个元素,即使只有一个元素发生变化,React 也会执行 effect。
对于有清除操作的 effect 同样适用:
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
}, [props.friend.id]); // 仅在 props.friend.id 发生变化时,重新订阅
📐 使用多个 Effect 实现关注点分离
使用 Hook 其中一个目的就是要解决 class 中生命周期函数经常包含不相关的逻辑,但又把相关逻辑分离到了几个不同方法中的问题。下述代码是将前述示例中的计数器和好友在线状态指示器逻辑组合在一起的组件:
function FriendStatusWithCounter(props) {
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);
};
});
// ...
}
📐 完整示例
import React, { useState, Fragment, useEffect } from 'react';
import ReactDOM from 'react-dom';
const App = () => {
const [count, setCount] = useState(0);
useEffect(() => {
let timer = setInterval(() => {
setCount(count => count + 1);
}, 500);
console.log('@@@@');
return () => {
clearInterval(timer);
};
}, [count]);
// 检测count的变化,每次变化,都会输出'@@@@'
// 如果是[],则只会输出一次'@@@@'
const add = () => {
setCount(count => count + 1);
};
const unmount = () => {
ReactDOM.unmountComponentAtNode(document.getElementById('root'));
};
return (
<Fragment>
<h2>当前求和为:{count}</h2>
<button onClick={add}>点我+1</button>
<button onClick={unmount}>卸载组件</button>
</Fragment>
);
};
export default App;
4. useRef
useRef
可以在函数组件中存储 / 查找组件内的标签或任意其它数据。保存标签对象,功能与 React.createRef()
一样。
const refContainer = useRef(initialValue);
useRef
返回一个可变的 ref 对象,其 .current
属性被初始化为传入的参数(initialValue
)。返回的 ref 对象在组件的整个生命周期内保持不变。
📌 当 ref 对象内容发生变化时,useRef
并不会通知你。变更 .current
属性不会引发组件重新渲染。
import React, { Fragment, useRef } from 'react';
const Demo = () => {
const myRef = useRef();
//提示输入的回调
function show() {
console.log(myRef.current.value);
}
return (
<Fragment>
<input type="text" ref={myRef} />
<button onClick={show}>点击显示值</button>
</Fragment>
);
};
export default Demo;
5. Hook规则
Hook 就是 JavaScript 函数,但是使用它们会有两个额外的规则:
只在最顶层使用 Hook
不要在循环,条件或嵌套函数中调用 Hook ,在 React 函数的最顶层调用 Hook。
如果想要有条件地执行一个 effect,可以将判断放到 Hook 的内部:
useEffect(function persistForm() {
// 👍 将条件判断放置在 effect 中
if (name !== '') {
localStorage.setItem('formData', name);
}
});
只在 React 函数中调用 Hook
不要在普通的 JavaScript 函数中调用 Hook。可以:
- 在 React 的函数组件中调用 Hook
- 在自定义 Hook 中调用其他 Hook
链接:https://juejin.cn/post/6992733298493489183