第一个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()
}