第一个Hook: useState
state
state 是组件内部的状态,那什么是状态呢?
状态一定是可变化的,也就是从一个状态变化为另一个状态。如果一个变量是不变的,那么不能称为状态。
props 是父组件传递过来的状态。
那么状态有那些特征呢?
它有三大特性,异步更新、合并更新、不可变数据。
异步更新
js
const StateDemo: FC = () => {
const [count, setCount] = useState(0)
const add = () => {
setCount(count + 1)
console.log('count', count)
}
return (
<>
<button onClick={add}>增加{count}</button>
</>
)
}
当执行完setCount(count + 1)之后打印count,此时count的值仍然是原来的值。这就是因为count的值是异步更新的
其实,当你修改某个值的时候,你也没有必要去打印,因为大多数情况下它会显示在页面中,如果不显示在页面中,完全不需要用useState,可以使用 useRef。
可能会被合并
js
const add = () => {
setCount(count + 1)
setCount(count + 1)
setCount(count + 1)
setCount(count + 1)
console.log('count', count)
}
当多次执行setCount(count + 1),count并不会每次都加1,所以点击一次还是只增加1,而不是4。
这也是因为异步更新的原因,当执行后面的setCount(count + 1),此时count并没有更新,仍然是0。
那怎么办呢?可以使用函数的方式进行更新。
js
const add = () => {
setCount(count => count + 1)
setCount(count => count + 1)
setCount(count => count + 1)
setCount(count => count + 1)
setCount(count => count + 1)
}
不可变数据(重要!!!)
不是修改state的值,而是传入一个新的值,或者传入一个函数,这个函数返回一个值。
js
const [userInfo, setUserInfo] = useState({ name: 'pcj', age: 35 })
const changeAge = () => {
setUserInfo({ ...userInfo, age: 36 })
}
修改年龄时必须传入一个新的值。
如果要修改的对象很复杂,嵌套了很多层,那么用...就很麻烦了,这个时候就可以用到immer这个库了,首先要安装这个库。
js
import { produce } from 'immer'
const [userInfo, setUserInfo] = useState({ name: 'pcj', age: 35 })
const changeAge = () => {
// setUserInfo({ ...userInfo, age: 36 })
setUserInfo(
produce(userInfo, draft => {
draft.age = 36
})
)
}
对于数组的修改如下:
js
const add = () => {
setQuestionList(
// questionList.concat({
// id: questionList.length + 1,
// title: `问卷${questionList.length + 1}`,
// isPublish: false,
// })
// immer的方式
produce(questionList, draft => {
draft.push({
id: questionList.length + 1,
title: `问卷${questionList.length + 1}`,
isPublish: false,
})
})
)
}
新增一项原来只能使用concat,现在通过immer之后就可以使用push方法了。
那么数组删除呢?
js
const deleteQuestion = (id: number) => {
// setQuestionList(questionList.filter(question => question.id !== id))
// immer的方式
setQuestionList(
produce(questionList, draft => {
draft.splice(
questionList.findIndex(question => question.id === id),
1
)
})
)
}
数组修改呢?
js
const publishQuestion = (id: number) => {
// setQuestionList(
// questionList.map(question => {
// if (question.id === id) {
// return {
// ...question,
// isPublish: true,
// }
// }
// return question
// })
// )
// immer的方式
setQuestionList(
produce(questionList, draft => {
draft.find(question => question.id === id)!.isPublish = true
})
)
}
!主要用于非空断言 ,告诉 TypeScript 编译器:我确定这个值不是null或undefined。
第二个Hook: useEffect
在讲useEffect,首先要明白一点:
- 组件是一个函数,当初次渲染就是执行这个函数
- 当state更新时,会触发组件的更新,也就是又执行了这个函数
js
const List1: FC = () => {
console.log('每次渲染都会执行这个打印')
return <></>
}
如果我们当组件渲染完成时,想执行一些其他操作,比如发送ajax请求,如果我们直接写在函数里面,那么每次更新都会执行,但是我们想当某个 state 更新时才发送请求,怎么办呢
副作用
- 当组件初次渲染完成时,发送ajax请求
js
useEffect(() => {
console.log('初次渲染')
}, [])
- 当某个 state 变化时,发送ajax请求
js
useEffect(() => {
console.log('组件更新')
}, [questionList])
useEffect第二个参数是依赖项,也就是第一个参数函数执行需要依赖的状态,只有依赖的状态变化时函数才会执行。
需要注意的是,当组件首次渲染时所有的useEffect都会执行。
组件销毁时,怎么执行副作用呢?
js
useEffect(() => {
console.log('组件渲染')
return () => {
console.log('组件销毁', id)
}
}, [])
在 useEffect 中返回一个函数,当组件销毁时就执行这个函数。
通过上面的学习,useEffect其实就相当于vue中的生命周期和watch的结合体。在 react 中,useEffect 既可以充当生命周期,又可以完成 vue 中的 watch 功能。
第三个Hook: useRef
-
一般用于操作DOM;
-
可以传入普通的js 变量,但是变化时不会触发组件的 rerender,但是会保存修改的值,如果只是用一个普通变量才保存修改后的值,当组件更新时普通变量又恢复到初始值,这一点要记住。
js
const UseRefDemo: FC = () => {
const inputRef = useRef<HTMLInputElement>(null)
const nameRef = useRef('pcj')
const changeName = () => {
nameRef.current = 'pcj2'
console.log('nameRef.current ', nameRef.current)
}
return (
<div>
<input ref={inputRef} type="text" defaultValue="hello world" />
<div>{nameRef.current}</div>
<div>
<button onClick={() => inputRef.current?.select()}>选中</button>
<button onClick={changeName}>change</button>
</div>
</div>
)
第四个Hook: memo + useMemo + useCallback
memo
有两个组件 Aaa、Bbb,Aaa 是 Bbb 的父组件:
js
import { memo, useEffect, useState } from "react";
function Aaa() {
const [,setNum] = useState(1);
useEffect(() => {
setInterval(()=> {
setNum(Math.random());
}, 2000)
},[]);
return <div>
<Bbb count={2}></Bbb>
</div>
}
interface BbbProps {
count: number;
}
function Bbb(props: BbbProps) {
console.log('bbb render');
return <h2>{props.count}</h2>
}
export default Aaa;
在 Aaa 里面不断 setState 触发重新渲染,问:console.log('bbb render') 打印几次?
答案是每 2s 都会打印。
也就是说,每次都会触发 Bbb 组件的重新渲染。但很明显,这里 Bbb 并不需要再次渲染,因为传入的是一个不变的数字2。
那怎么办呢?
这时可以加上 memo:
js
import { memo, useEffect, useState } from "react";
function Aaa() {
const [,setNum] = useState(1);
useEffect(() => {
setInterval(()=> {
setNum(Math.random());
}, 2000)
},[]);
return <div>
<MemoBbb count={2}></MemoBbb>
</div>
}
interface BbbProps {
count: number;
}
function Bbb(props: BbbProps) {
console.log('bbb render');
return <h2>{props.count}</h2>
}
const MemoBbb = memo(Bbb);
export default Aaa;
memo 的作用是只有 props 变的时候,才会重新渲染被包裹的组件。
这样就只会打印一次了。
我们让 2s 后 props 变了呢?
js
import { memo, useEffect, useState } from "react";
function Aaa() {
const [,setNum] = useState(1);
const [count, setCount] = useState(2);
useEffect(() => {
setInterval(()=> {
setNum(Math.random());
}, 2000)
},[]);
useEffect(() => {
setTimeout(()=> {
setCount(Math.random());
}, 2000)
},[]);
return <div>
<MemoBbb count={count}></MemoBbb>
</div>
}
interface BbbProps {
count: number;
}
function Bbb(props: BbbProps) {
console.log('bbb render');
return <h2>{props.count}</h2>
}
const MemoBbb = memo(Bbb);
export default Aaa;
props 变了会触发 memo 的重新渲染。
用 memo 的话,一般还会结合两个 hook:useMemo 和 useCallback。
useCallback
给 Bbb 加一个 callback 的参数:

参数传了一个 function,你会发现 memo 失效了。
因为每次 function 都是新创建的,也就是每次 props 都会变,这样 memo 就没用了。
这时候就需要 useCallback:
js
const bbbCallback = useCallback(function () { // xxx }, []);
它的作用就是当 deps 数组不变的时候,始终返回同一个 function,当 deps 变的时候,才把 function 改为新传入的。
这时候你会发现,memo 又生效了。
useMemo
useMemo 也是和 memo 打配合的,只不过它保存的不是函数,而是值。
js
const count2 = useMemo(() => {
return count * 10;
}, [count]);
它是在 deps 数组变化的时候,计算新的值返回。

useMemo 和 Vue 中的 computed 有一些相似,两者的核心作用都是缓存计算结果,避免在依赖项未发生变化时进行重复计算,从而优化性能,都需要指定依赖项(Vue 的 computed 会自动追踪依赖,React 的 useMemo 需要显式声明依赖数组)。
所以说,如果子组件用了 memo,那给它传递的对象、函数类的 props 就需要用 useMemo、useCallback 包裹,否则,每次 props 都会变,memo 就没用了。
反之,如果 props 使用 useMemo、useCallback,但是子组件没有被 memo 包裹,那也没意义,因为不管 props 变没变都会重新渲染,只是做了无用功。
memo + useCallback + useMemo 是搭配着来的,少了任何一方,都会使优化失效。
但 useMemo 和 useCallback 也不只是配合 memo 用的:
比如有个值的计算,需要很大的计算量,你不想每次都算,这时候也可以用 useMemo 来缓存。
总结:memo 是防止 props 没变时的重新渲染,useMemo 和 useCallback 是防止 props 的不必要变化。
自定义 Hooks
自定义一个异步请求数据的 hooks
代码如下:
js
import { useState, useEffect } from 'react'
function getInfo(): Promise<string> {
return new Promise(resolve => {
setTimeout(() => {
resolve('123')
}, 2000)
})
}
const useGetInfo = () => {
const [loading, setLoading] = useState(false)
const [info, setInfo] = useState('')
useEffect(() => {
setLoading(true)
getInfo().then(res => {
setInfo(res)
setLoading(false)
})
}, [])
return { loading, info }
}
export default useGetInfo
// 在组件中使用
import useGetInfo from './hooks/useGetInfo'
function App() {
const { loading, info } = useGetInfo()
useTitle('app title')
return (
<>
<div>{loading ? 'loading' : info}</div>
<>
)
}
第三方Hooks
国内用的比较多的是阿里的ahooks,国外用的比较多的是react-use。
hooks 使用规则
-
必须使用
useXxxx来命名 -
只能在两个地方调用,一个是组件内,一个其他
hooks内 -
必须保证每次调用顺序一致,不能放在if for 内部,只能放在组件的第一层
js
# 下面写法是错误的
if (true) {
useEffect()
}
for() {
useTitle()
}