react学习2:react中常用的hooks

第一个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 编译器:我确定这个值不是 nullundefined

第二个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()
}
相关推荐
南清的coding日记7 小时前
Java 程序员的 Vue 指南 - Vue 万字速览(01)
java·开发语言·前端·javascript·vue.js·css3·html5
Xiaouuuuua7 小时前
2026年计算机毕业设计项目合集
前端·vue.js·课程设计
IT_陈寒7 小时前
React 18并发模式实战:3个优化技巧让你的应用性能提升50%
前端·人工智能·后端
用户761736354017 小时前
CSS重点知识-样式计算
前端
yoyoma7 小时前
object 、 map 、weakmap区别
前端·javascript
shyshi7 小时前
vercel 部署 node 服务和解决 vercel 不可访问的问题
前端·javascript
.生产的驴7 小时前
React 模块化Axios封装请求 统一响应格式 请求统一处理
前端·javascript·react.js·前端框架·json·ecmascript·html5
前端大神之路7 小时前
vue2 响应式原理
前端
一个努力的小码农7 小时前
Rust中if let与while let语法糖的工程哲学
前端·rust