React Hook和一些踩坑

React Hook是React 16.8的新增特性,它可以让我们在不编写class组件的情况下使用state以及生命周期函数等其他React特性。

State Hook

声明State

函数组件中,由于没有this,所以不能使用this.state来分配或读取State,我们要在组件中调用useState Hook:

1
2
// 声明一个count变量以及一个更新count的函数
const [count, setCount] = useState(0)

调用useState时,定义了一个State变量,这个变量叫做count,以及一个更新count的函数setCountuseState函数的唯一参数是初始State。

读取State

在函数组件中,我们直接用变量名就可以读取State:

1
<p>You clicked {count} times</p>

更新State

更新State需要用useState返回的函数:

1
2
3
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>

Effect Hook

Effect Hook用于在函数组件中执行副作用操作:

1
2
3
4
// 类似于componentDidMount和componentDidUpdate和componentWillUnmount三个函数的组合
useEffect(() => {
console.log("useEffectTriggerd");
})

无需清除的Effect

有时候我们想在React更新DOM之后运行一些额外的代码,这些都是无需清除的操作:

1
2
3
4
// 每次DOM更新的时候更新标题
useEffect(() => {
document.title = `You clicked ${count} times`;
});

通过useEffect这个Hook,我们告诉React组件需要在渲染后执行某些操作。React会保存传递的函数,称之为Effect,并在DOM更新后调用它。在默认情况下,Effect在第一次渲染后都会执行,不用再去考虑挂载还是更新,React保证了每次运行Effect的同时,DOM都已更新完成。

需要清除的Effect

如果Effect返回一个函数,React将会在卸载组件时调用它,(类似于componentWillUnmount):

1
2
3
4
// 组件卸载时会打印unmount
useEffect(() => {
return () => { console.log("unmount") }
})

Effect的清除阶段在每次重新渲染时都会执行,而不是只在卸载组件时执行一次。

跳过Effect进行性能优化

如果每次渲染后都执行清理或者执行Effect可能会导致性能问题,因此我们可以跳过Effect:如果某些特定的值在两次渲染之间没有发生变化,我们可以跳过对Effect的调用,只要传递数组做为useEffect的第二个参数:

1
2
3
4
// 只有count变化了才会执行
useEffect(() => {
console.log("update");
}, [count])

如果想要执行一个只运行一次的Effect,只需要将一个空数组做为第二个参数即可:

1
2
3
4
// 只会运行一次
useEffect(() => {
console.log("update");
}, [])

Hook规则

只在最顶层使用Hook

不要在循环、条件或嵌套函数中使用Hook,确保在React函数的最顶层和return之前调用他们。

只在React函数中调用Hook

不要在普通的JavaScript函数中调用Hook。

Hook踩坑

在组件加载时发起异步请求

在组件加载时发起异步请求是很常见的,但是不可以这样写:

1
2
3
4
  useEffect(async () => {
await fetch("http://127.0.0.1:10000/")
}, [])
// 报错信息:Warning: useEffect must not return anything besides a function, which is used for clean-up.

因为useEffect的第一个参数需要是一个函数,而async函数返回的是一个Promise,因此会报错。

我们可以在useEffect里的函数写一个IIFE函数:

1
2
3
4
5
useEffect(() => {
(async () => {
await fetch("http://127.0.0.1:10000/")
})()
}, [])

又或者:

1
2
3
4
5
6
useEffect(() => {
var getData = async () => {
await fetch("http://127.0.0.1:10000/")
}
getData()
}, [])

设置State是异步的

1
2
3
4
5
6
const [count, setCount] = useState(0)

var counterAdd = () => {
setCount(count + 1)
console.log(count); // 0
}

状态修改时异步的,所以马上读取count,还是得到原始的值。如果在修改了State之后要马上使用它的值,可以在设置State的函数中返回一个函数,在函数中进行操作:

1
2
3
4
5
6
7
8
9
const [count, setCount] = useState(0)

var counterAdd = () => {
setCount(() => {
var newCount = count + 1
console.log(newCount) //1
return newCount
})
}

又或者利用useEffect做监听:

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

useEffect(() => { console.log(count); }, [count])

return (
<button onClick={() => { setCount(count + 1) }}>add</button>
);
}

当State为数组时

下面是一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function App() {
const [arr, setArr] = useState([1])

var arrAdd = () => {
arr.push(arr[arr.length - 1] + 1)
console.log(arr);
setArr(Array.from(arr))
}


return (
<div className="App">
{arr.map(v => (
<div key={v}>{v}</div>
))}
<button onClick={arrAdd}>
add
</button>
</div>
)
}

虽然控制台中打印出了新的函数,但是页面没有更新新的元素。这是因为React组件的更新机制对State只进行浅对比,因此我们在setArr里传入原数组是不会更新页面的,我们需要传入一个新的数组:

1
2
3
4
5
var arrAdd = () => {
arr.push(arr[arr.length - 1] + 1)
console.log(arr);
setArr(Array.from(arr)) // 或者setArr(arr.slice())
}