React Hook
Hook 是 React 16.8 的新增特性。✌️
Hook 是一些可以让你在函数组件里“钩入” React state 及生命周期等特性的函数。
它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
它可以从组件中提取状态逻辑 使这些逻辑可以单独测试和复用。(自定义hook)
基础Hook
useState
作用是设置初始state,返回以及更新state
import React, { useState } from 'react’;
function ExampleWithManyStates() {
// initialState具有惰性,只在初始渲染时调用
// useState 会返回一对值:当前状态(count)和一个让你更新它的函数(setCount)
const [count, setCount] = useState(initialCount);
// 可以声明多个 state 变量😯
const [age, setAge] = useState(42);
return (
<>
<!-- 直接使用count 无需this.state -->
Count: {count}
<!-- 更新 state的方法 -->
<button onClick={() => setCount(initialCount)}>Reset</button>
<button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
<button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
<button onClick={() => setCount(setAge => setAge + 1)}>{age}</button>
</>
);
}
-
useState后的参数 (initialState) 是state的初始值,它是惰性调用,只在初始渲染时起效
-
useState会返回一对值:当前状态和一个让你更新它的函数,这个第二个返回值类似 class 组件的
this.setState
,但是它不会把新的 state 和旧的 state 进行合并
useEffect
useEffect用来执行副作用操作;
它会发生在每轮渲染结束后执行;
它用来解决函数组件主体内(React 渲染阶段)无法执行副作用操作(改变 DOM、添加订阅、设置定时器、记录日志等等)的问题。
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新
// count是可选参数,当两次渲染没有变化发生时就跳过对effect的调用
- 可以把 useEffect Hook 看做 componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个函数的组合。
- 与 componentDidMount 或 componentDidUpdate 不同的是使用 useEffect 调度的 effect 不会阻塞浏览器更新屏幕,这让应用看起来响应更快。
- 两种常见的副作用操作分为: 需要清除和不需要清除的(即执行操作后就可以忽略)
不需要清除的副作用操作
替代componentDidMount 和 componentDidUpdate
拿class组件的写法和使用hook的函数组件的写法做比较:
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>
);
}
}
可以改造成下面这样,优势在于,本来需要在组件加载和更新时执行相同的代码,使用useEffect Hook后只需写一遍
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
// 这样每次更新时都能在effect中取得最新的值
useEffect(() => {
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
需要清除的副作用操作
替代componentDidMount 和 componentWillUnmount;
要在 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 () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
注意⚠️:
- Hook可以在无需修改组件结构的情况下复用状态逻辑。
- Hook 将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据),而并非强制按照生命周期划分。
- 你只能在React函数最顶层使用Hook
- Hook 是一种复用状态逻辑的方式,它不复用 state 本身
- 不要在循环,条件或嵌套函数中调用 Hook(原因见后)
useContext
const value = useContext(MyContext);
- 接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。
- 调用了 useContext 的组件总会在 context 值变化时重新渲染。
自定义Hook
使用hook时,当我们想在两个函数之间共享逻辑时,我们会把它提取到第三个函数中,这就是一个自定义hook;
约定函数名以use打头
内部可以使用其他Hook
多个组件使用共同的Hook不会共享state,它们的state是完全独立的
import { useState, useEffect } from 'react';
// useFriendStatus就是一个自定义hook
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(useFriendStatus)?
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
- useReducer
- useCallback
- useMemo
- useRef
- useImperativeHandle
- useLayoutEffect
- useDebugValue
React hooks——not magic, just Arrays;
上边提到,不要在循环,条件或嵌套函数中调用 Hook,原因如下,我们可以先从hooks的原理入手;
- 当我们调用 useState 的时候,会返回形如 (变量, 函数) 的一个元祖。并且 state 的初始值就是外部调用 useState 的时候,传入的参数。
- 然后,useState 返回的用于更改状态的函数,自动调用了render方法来触发视图更新。
- 可以思考,首先需要一个全局变量记录hooks初始状态,然后在方法内部调用一个类似于setState的方法来更新状态,最后调用render方法来更新视图,返回[state, setState]这个元祖。具体代码如下所示:
function render() {
ReactDOM.render(<App />, document.getElementById("root"));
}
let state: any;
function useState<T>(initialState: T): [T, (newState: T) => void] {
state = state || initialState;
function setState(newState: T) {
state = newState;
render();
}
return [state, setState];
}
render(); // 首次渲染
但是现实中,我们的 useState是支持多个 state的。它的本质上是通过一个朴实无华的 Array 对象来实现的。具体过程如下(需要一个全局数组states[]和一个记录位置的cursor):
- 第一次渲染时候,根据 useState 顺序,逐个声明 state 并且将其放入全局 Array 中。每次声明 state,都要将 cursor 增加 1。
- 更新 state,触发再次渲染的时候。cursor 被重置为 0。按照 useState 的声明顺序,依次拿出最新的 state 的值,视图更新。
import React from "react";
import ReactDOM from "react-dom";
const states: any[] = [];
let cursor: number = 0;
function useState<T>(initialState: T): [T, (newState: T) => void] {
const currenCursor = cursor;
states[currenCursor] = states[currenCursor] || initialState; // 检查是否渲染过
function setState(newState: T) {
states[currenCursor] = newState;
render();
}
++cursor; // update: cursor
return [states[currenCursor], setState];
}
function App() {
const [num, setNum] = useState < number > 0;
const [num2, setNum2] = useState < number > 1;
return (
<div>
<div>num: {num}</div>
<div>
<button onClick={() => setNum(num + 1)}>加 1</button>
<button onClick={() => setNum(num - 1)}>减 1</button>
</div>
<hr />
<div>num2: {num2}</div>
<div>
<button onClick={() => setNum2(num2 * 2)}>扩大一倍</button>
<button onClick={() => setNum2(num2 / 2)}>缩小一倍</button>
</div>
</div>
);
}
function render() {
ReactDOM.render(<App />, document.getElementById("root"));
cursor = 0; // 重置cursor
}
render(); // 首次渲染
那为什么不能在循环,条件或嵌套函数中调用 Hook呢?
这与它的cursor相关:由于 useState 是基于 Array+Cursor 来实现的,第一次渲染时候,state 和 cursor 的对应关系在条件改变时可能发生变化,但是函数并不能知道这种变化,所以可能会造成错位,从而导致一些问题。如下例:
// ------------
// 第一次渲染----满足条件
// ------------
useState('1') // cursor为1
useState('2') // cursor为2
// 我们在条件语句中使用Hook
if (case) {
useState('3') // cursor为3
}
useState('4') // cursor为4
// -------------
// 第二次渲染---不满足条件
// -------------
useState('1') // cursor为1
useState('2') // cursor为2
// 我们在条件语句中使用Hook
if (case) {
useState('3')
}
useState('4') // cursor为3❌ 造成接受的变量赋值错位
最后:
总的来说,Hooks 对代码编写的要求较高,在没有有效机制保证代码可读性、规避风险的情况下,Class 依然是首选