react-hook

在react 16.8的版本中,react推出了hook这一个新特性,推出这一特性的原因就是因为在组件间去复用状态逻辑非常复杂。在之前我们可能会使用render prop或者高阶组件去解决这一问题。例如render prop:

1
2
3
<DataProvider render={data => (
<ChildItem data={data} />
)}/>

高阶组件hoc:

1
2
3
4
5
6
7
8
9
10
11
12
const withData = WrappedComponent => {
const data = {}
return props => <WrappedComponent data={data} {...props} />;
};

const ChildItem = props => (
<div>
{data.name}
</div>
);

export default withData(ChildItem);

这就是原先在组件间去复用状态逻辑的几种方法。但是这样的操作就会导致react内部去使用的时候不停的去嵌套组件,形成”wrapper hell“。阅读代码的时候逻辑经常需要去其他地方查找,代码逻辑会让人难以理。而且想要用到state及生命周期方法的话就必须使用类组件,而类组件在书写的时候经常会落到一些不大好的实践中,使代码变慢,而且类组件不能很好的压缩,并且它们使得热加载变得不可靠。

这时候hook的好处就凸显出来了,它能够在不更改组件层次结构的情况下重用有状态逻辑,并根据相关的部分(例如设置订阅或获取数据)将一个组件拆分为较小的功能,并且能使你在不是类组件的情况下使用react的很多功能,至关重要的是,Hooks能与现有代码并行工作,所以在不改动原来代码的基础上,我们也可以在新的代码中去尝试和使用hook。

一个使用hook的简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React, { useState } from 'react';

function Example() {
// Declare a new state variable, which we'll call "count"
const [count, setCount] = useState(0);

return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}

使用hook的一些注意事项:

  • 只能在顶层调用Hooks。不要在循环,条件或嵌套函数中调用Hook。
  • 仅从React功能组件调用Hooks。不要从常规JavaScript函数调用Hook。
1
2
3
4
useState('Mary')           // 1. 使用 'Mary' 初始化变量名为 name 的 state
useEffect(persistForm) // 2. 添加 effect 以保存 form 操作
useState('Poppins') // 3. 使用 'Poppins' 初始化变量名为 surname 的 state
useEffect(updateTitle) // 4. 添加 effect 以更新标题
1
2
3
4
5
6
7
useState('Mary')           // 1. 读取变量名为 name 的 state(参数被忽略)
// 🔴 在条件语句中使用 Hook 违反了不要在普通的 JavaScript 函数中调用 Hook
if (name !== '') {
useEffect(persistForm) // 2. 替换保存 form 的 effect
}
useState('Poppins') // 3. 读取变量名为 surname 的 state(参数被忽略)
useEffect(updateTitle) // 4. 替换更新标题的 effect
1
2
3
4
5
6
useEffect(function persistForm() {
// 👍 将条件判断放置在 effect 中
if (name !== '') {
localStorage.setItem('formData', name);
}
});

hook的几个重点的api

useState

我们现在想要去写一个存在state变化的组件的普通方法的话一般就是去建一个类组件。比如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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>
);
}
}

但如果使用useState

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React, { useState } from 'react';

function Example() {
// Declare a new state variable, which we'll call "count"
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}

const [fruit, setFruit] = useState('banana');

===
var fruitStateVariable = useState('banana'); // Returns a pair
var fruit = fruitStateVariable[0]; // First item in a pair
var setFruit = fruitStateVariable[1]; // Second item in a pair

setState 函数用于更新 state。它接收一个新的 state 值并将组件的一次重新渲染加入队列。在每次渲染开始的时候state值是已定的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function Counter () {
const [count, setCount] = useState(0)

function onClick () {
setTimeout(() => {
setCount(count + 1)
}, 2000)
}

return <p onClick={onClick}>You clicked {count} times</p>
}
// 代码等价于
class Counter extends Component {
state = {
count: 0
}

onClick = () => {
setTimeout(() => {
this.setState({
count: this.state.count + 1
})
}, 2000)
}

render () {
return <p onClick={this.onClick}>You clicked {this.state.count} times</p>
}
}

2e98c82b39ae4eea7e47e6e42a861551.png
51a802faf298a2113623ef21921a79f1.png

但是如果在2秒内连续点击由hook写成的组件。无论几次,count值也只是加一
函数式更新:如果新的 state 需要通过使用先前的 state 计算得出,那么可以将函数传递给 setState。该函数将接收先前的 state,并返回一个更新后的值。

1
2
3
4
5
6
7
8
9
10
11
12
function Counter3 () {
const [count, setCount] = useState(0)

function onClick () {
setTimeout(() => {
console.log(count,'hook2-out')
setCount(count => { console.log(count,'hook2-in') ; return count + 1})
}, 2000)
}

return <p onClick={onClick}>You clicked {count} times</p>
}

c87401077c87ed8482c8dba62219ee5b.png
2ba15244fbfbc37616caf09bc6401f6a.png

useEffect

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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>
);
}
}

import React, { useState, useEffect } from 'react';

function Example() {
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>
);
}

这段代码实现的效果是,刚开始渲染和点击按钮的时候,网页的title会变化,显示当时count的值。

所以我们其实可以将useEffect Hook视为componentDidMount,componentDidUpdate和componentWillUnmount的组合。在每一次组件重新render的时候useEffect都会重新执行。

如何执行一些需要清空,取消订阅的函数

1
2
3
4
5
6
7
8
9
10
11
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);
};
});

在useEffect中最后返回的函数,是useEffect的可选清理机制,每一个useEffect都可以再最后返回一个去执行清理的函数,这使我们可以将添加和删除这个订阅的逻辑都放在一起,易于理解和查找。

当组件卸载时,React执行清理。但是,正如我们之前所了解的那样,效果会针对每个渲染而不仅仅运行一次。这就是React在下次运行效果之前还清除前一渲染效果的原因。这样可以避免忘记处理componentDidUpdate时出现bug的情况,也可以防止多次注册某一事件

1
2
3
4
5
6
7
8
9
10
11
12
13
// Mount with { friend: { id: 100 } } props
ChatAPI.subscribeToFriendStatus(100, handleStatusChange); // Run first effect

// Update with { friend: { id: 200 } } props
ChatAPI.unsubscribeFromFriendStatus(100, handleStatusChange); // Clean up previous effect
ChatAPI.subscribeToFriendStatus(200, handleStatusChange); // Run next effect

// Update with { friend: { id: 300 } } props
ChatAPI.unsubscribeFromFriendStatus(200, handleStatusChange); // Clean up previous effect
ChatAPI.subscribeToFriendStatus(300, handleStatusChange); // Run next effect

// Unmount
ChatAPI.unsubscribeFromFriendStatus(300, handleStatusChange); // Clean up last effect

通过跳过效果优化性能

useEffect也可以根据传入参数的变化来执行:

1
2
3
4
5
6
7
8
9
componentDidUpdate(prevProps, prevState) {
if (prevState.count !== this.state.count) {
document.title = `You clicked ${this.state.count} times`;
}
}

useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); //

只要是 useEffect() 中用到的,都要在依赖数组中申明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const Example = (props) => {
const [total, setTotal] = useState(0)
const [foo, setFoo] = useState(0)
const [bar, setBar] = useState(0)

useEffect(() => {
setInterval(() => {
console.log(total)
}, 1000)
}, [total])//如果不声明打出来的将永远是0

function updateTotal () {
setTotal(t => t + 1)
}

function addFoo () {
setFoo(f => f + 1)
updateTotal()
}

function addBar () {
setBar(b => b + 1)
updateTotal()
}

return <>
<button onClick={addFoo}>{foo}</button>
+
<button onClick={addBar}>{bar}</button>
=
<span>{total}</span>
</>
}

如果要运行效果并仅将其清理一次(在mount和unmount时),则可以将空数组([])作为第二个参数传递。

这告诉React你的效果不依赖于来自props或state的任何值,所以它永远不需要重新运行。

这不作为特殊情况处理 - 它直接遵循输入数组的工作方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
componentDidMount() {
window.addEventListener('mousemove', () => {})
}

componentWillUnmount() {
window.removeEventListener('mousemove', () => {})
}

useEffect(() => {
window.addEventListener('mousemove', () => {});

// returned function will be called on component unmount
return () => {
window.removeEventListener('mousemove', () => {})
}
}, [])

自定义hook

自定义Hook是一个JavaScript函数,其名称以“use”开头,可以调用其他Hook。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React, { 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

1
2
3
4
5
6
7
8
function FriendStatus(props) {
const isOnline = useFriendStatus(props.friend.id);

if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}

自定义hook是一种重用有状态逻辑的机制(例如设置订阅并记住当前值),但每次使用自定义hook时,其中的所有状态和效果都是完全隔离的。
hook和setinterval

  • 基础的hook
    • [useState](https://reactjs.org/docs/hooks-reference.html#usestate)
    • [useEffect](https://reactjs.org/docs/hooks-reference.html#useeffect)
    • [useContext](https://reactjs.org/docs/hooks-reference.html#usecontext)
  • 其他的hook
    • [useReducer](https://reactjs.org/docs/hooks-reference.html#usereducer)
    • [useCallback](https://reactjs.org/docs/hooks-reference.html#usecallback)
    • [useMemo](https://reactjs.org/docs/hooks-reference.html#usememo)
    • [useRef](https://reactjs.org/docs/hooks-reference.html#useref)
    • [useImperativeHandle](https://reactjs.org/docs/hooks-reference.html#useimperativehandle)
    • [useLayoutEffect](https://reactjs.org/docs/hooks-reference.html#uselayouteffect)
    • [useDebugValue](https://reactjs.org/docs/hooks-reference.html#usedebugvalue)