react的15个hook汇总

1,905 阅读10分钟

react的hooks只要包括下面15个:

react16有10个

image.png

react18是5个

image.png

hooks解决的问题是:

  1. 为函数组件提供生命周期
  2. 为函数组件创建状态
  3. 提取函数组件的重复逻辑

根据作用划分

我们可以根据具体作用对他进行重新划分:处理状态的有8个,处理副作用的有3个,优化组件的有2个,工具hooks有2个。

image.png

一:状态hooks

1.1 useState ---v16

函数组件里面,只要props和state发生变化都会触发组件更新,useState是批量更新的,如果同一个状态,修改多次,只有最后一次起作用。

基本使用

const DemoState = (props) => {
   /* number为此时state读取值 ,setNumber为派发更新的函数 */
   let [number, setNumber] = useState(0) /* 0为初始值 */
   return (<div>
       <span>{ number }</span>
       <button onClick={ ()=> {
         setNumber(number+1)
         console.log(number) /* 这里的number是不能够即使改变的  */
       } } ></button>
   </div>)
}

1.2 useReducer ---v16

1.必须在一个组件里面使用,不能跨组件。

2.适用于当修改state前需要执行大量重复逻辑的场景。

3.相当于是一个小型的redux

const DemoUseReducer = ()=>{
    /* number为更新后的state值,  dispatchNumbner 为当前的派发函数 */
   const [ number , dispatchNumbner ] = useReducer((state,action)=>{
       const { payload , name  } = action
       /* return的值为新的state */
       switch(name){
           case 'add':
               return state + 1
           case 'sub':
               return state - 1 
           case 'reset':
             return payload       
       }
       return state
   },0)
   return <div>
      当前值:{ number }
      { /* 派发更新 */ }
      <button onClick={()=>dispatchNumbner({ name:'add' })} >增加</button>
      <button onClick={()=>dispatchNumbner({ name:'sub' })} >减少</button>
      <button onClick={()=>dispatchNumbner({ name:'reset' ,payload:666 })} >赋值</button>
      { /* 把dispatch 和 state 传递给子组件  */ }
      <MyChildren  dispatch={ dispatchNumbner } State={{ number }} />
   </div>
}

1.3 useContext

1.子组件用useContext获取父组件传过来的context,主要的应用场景就是跨级传值。

2.它的弊端:如果在context里面存储了a,b两个变量,和setA,setB的两个方法,把asetA传入A组件,把bsetB传入B组件,当我用setA修改a变量的时候,即便B组件没有使用 a变量,但是B组件也会被重新渲染。解决这个问题的办法就是把a变量和b变量拆分到不同的context里面,这样又会出现大量的context.provide的嵌套。陷入嵌套地狱!

3.综上所述,context只适用于场景比较简单的嵌套组件里面。关键时刻还得专门的状态管理器上场,比如redux,zustand,jotai。

/* 用useContext方式 */
const DemoContext = ()=> {
    const value:any = useContext(Context)
    /* my name is alien */
return <div> my name is { value.name }</div>
}

/* 用Context.Consumer 方式 */
const DemoContext1 = ()=>{
    return <Context.Consumer>
         {/*  my name is alien  */}
        { (value)=> <div> my name is { value.name }</div> }
    </Context.Consumer>
}

export default ()=>{
    return <div>
        <Context.Provider value={{ name:'alien' , age:18 }} >
            <DemoContext />
            <DemoContext1 />
        </Context.Provider>
    </div>
}

1.4 useRef

它用来存储dom,也可以用来存储那些不会引发重渲染的值,也就是说如果用ref存储了值,即使这个值发生了改变,他也不会导致组件重渲染。

const DemoUseRef = ()=>{
    const dom= useRef(null)
    const handerSubmit = ()=>{
        /*  <div >表单组件</div>  dom 节点 */
        console.log(dom.current)
    }
    return <div>
        {/* ref 标记当前dom节点 */}
        <div ref={dom} >表单组件</div>
        <button onClick={()=>handerSubmit()} >提交</button> 
    </div>
}

1.5 useImperativeHandle

如果父组件给子组件传了一个ref,在子组件里面可以将这个ref指定给某个元素,这样父组件就能够轻松操控这个子组件里面的元素了。但是如果父组件想要操纵子组件的一些值或者是方法呢?此时就可以用useImperativeHandle将子组件里面的一些值或者方法暴露出去,供父组件使用,这个hook常常在组件封装的时候用到。

他接受三个参数,具体是:

-   ① 第一个参数ref: 接受 forWardRef 传递过来的 ref。
-   ② 第二个参数 createHandle :处理函数,返回值作为暴露给父组件的 ref 对象。
-   ③ 第三个参数 deps : 依赖项 deps ,依赖项更改形成新的 ref 对象。

具体用法

function Son (props,ref) {
    console.log(props)
    const inputRef = useRef(null)
    const [ inputValue , setInputValue ] = useState('')
    useImperativeHandle(ref,()=>{
       const handleRefs = {
           /* 声明方法用于聚焦input框 */
           onFocus(){
              inputRef.current.focus()
           },
           /* 声明方法用于改变input的值 */
           onChangeValue(value){
               setInputValue(value)
           }
       }
       return handleRefs
    },[])
    return <div>
        <input
            placeholder="请输入内容"
            ref={inputRef}
            value={inputValue}
        />
    </div>
}

const ForwarSon = forwardRef(Son)

class Index extends React.Component{
    inputRef = null
    handerClick(){
       const { onFocus , onChangeValue } =this.cur
       onFocus()
       onChangeValue('let us learn React!')
    }
    render(){
        return <div style={{ marginTop:'50px' }} >
            <ForwarSon ref={node => (this.inputRef = node)} />
            <button onClick={this.handerClick.bind(this)} >操控子组件</button>
        </div>
    }
}

1.6 useSyncExternalStore

它主要是给redux和mobox这种状态管理器使用的,目的是要组件在concurrent模式下也能安全准确的拿到state。所以这个hook一般在开发的时候也用不到。

import { useSyncExternalStore } from "react";
import { Button } from "antd";
import { combineReducers, createStore } from "redux";

const reducer = (state: number = 1, action: any) => {
  switch (action.type) {
    case "ADD":
      return state + 1;
    case "DEL":
      return state - 1;
    default:
      return state;
  }
};

/* 注册reducer,并创建store */
const rootReducer = combineReducers({ count: reducer });
const store = createStore(rootReducer, { count: 1 });

const Index: React.FC<any> = () => {
  //订阅
  const state = useSyncExternalStore(
    store.subscribe,
    () => store.getState().count
  );

  return (
    <>
      <div>大家好,我是小杜杜,一起玩转Hooks吧!</div>
      <div>数据源: {state}</div>
      <Button type="primary" onClick={() => store.dispatch({ type: "ADD" })}>
        加1
      </Button>
      <Button
        style={{ marginLeft: 8 }}
        onClick={() => store.dispatch({ type: "DEL" })}
      >
        减1
      </Button>
    </>
  );
};

export default Index;


1.7 useTransition

启用过度任务,现实使用场景是:输入搜索框,当我们在input里面输入文本,然后在onchange的时候搜索数据的时候,一般会先用state创建一个loading设置loading是true,在页面上要他显示组件,等数据加载好以后就将loading改成false,然后驱动页面更新,显示加载后的数据。

有了useTransition以后,上面的loading就不要了,它会返回2个参数,一个是loading,一个是延迟执行函数。我们只需要将要做的事情加入延迟执行函数,他就能自己驱动loading发生改变。

import { useState, useTransition } from "react";
import { Input } from "antd";

const Index: React.FC<any> = () => {
  const [isPending, startTransition] = useTransition();
  const [input, setInput] = useState("");
  const [list, setList] = useState<string[]>([]);

  return (
    <>
      <div>大家好,我是小杜杜,一起玩转Hooks吧!</div>
      <Input
        value={input}
        onChange={(e) => {
          setInput(e.target.value);
          startTransition(() => {
            const res: string[] = [];
            for (let i = 0; i < 10000; i++) {
              res.push(e.target.value);
            }
            setList(res);
          });
        }}
      />
      {isPending ? (
        <div>加载中...</div>
      ) : (
        list.map((item, index) => <div key={index}>{item}</div>)
      )}
    </>
  );
};

export default Index;

1.8 useDeferredValue

useDeferredValue 用来包裹延迟的值,作用和useTransion一样,但是用法不同,一个是延迟执行函数,一个是延迟渲染值。

import { useState, useDeferredValue } from "react";
import { Input } from "antd";

const getList = (key: any) => {
  const arr = [];
  for (let i = 0; i < 10000; i++) {
    if (String(i).includes(key)) {
      arr.push(<li key={i}>{i}</li>);
    }
  }
  return arr;
};

const Index: React.FC<any> = () => {
  //订阅
  const [input, setInput] = useState("");
  const deferredValue = useDeferredValue(input);
  console.log("value:", input);
  console.log("deferredValue:", deferredValue);

  return (
    <>
      <div>大家好,我是小杜杜,一起玩转Hooks吧!</div>
      <Input value={input} onChange={(e: any) => setInput(e.target.value)} />
      <div>
        <ul>{deferredValue ? getList(deferredValue) : null}</ul>
      </div>
    </>
  );
};

export default Index;


二. 副作用

2.1 useEffect

它里面的代码异步执行。

如上在 useEffect 中做的功能如下:

  • ① 请求数据。
  • ② 设置定时器,延时器等。
  • ③ 操作 dom , 在 React Native 中可以通过 ref 获取元素位置信息等内容。
  • ④ 注册事件监听器, 事件绑定,在 React Native 中可以注册 NativeEventEmitter 。
  • ⑤ 还可以清除定时器,延时器,解绑事件监听器等。

知识点汇总

1.如果没有依赖值那就每次刷新都执行

2.如果依赖是[],只有页面挂载的时候执行一次

3.如果依赖数组里面有值,那么每次依赖值发生变化的时候才更新

4.它里面有个return出去的函数是组件卸载前要执行的

    /* 模拟数据交互 */
function getUserInfo(a){
    return new Promise((resolve)=>{
        setTimeout(()=>{ 
           resolve({
               name:a,
               age:16,
           }) 
        },500)
    })
}

const Demo = ({ a }) => {
    const [ userMessage , setUserMessage ] :any= useState({})
    const div= useRef()
    const [number, setNumber] = useState(0)
    /* 模拟事件监听处理函数 */
    const handleResize =()=>{}
    /* useEffect使用 ,这里如果不加限制 ,会是函数重复执行,陷入死循环*/
    useEffect(()=>{
       /* 请求数据 */
       getUserInfo(a).then(res=>{
           setUserMessage(res)
       })
       /* 定时器 延时器等 */
       const timer = setInterval(()=>console.log(666),1000)
       /* 操作dom  */
       console.log(div.current) /* div */
       /* 事件监听等 */
       window.addEventListener('resize', handleResize)
         /* 此函数用于清除副作用 */
       return function(){
           clearInterval(timer) 
           window.removeEventListener('resize', handleResize)
       }
    /* 只有当props->a和state->number改变的时候 ,useEffect副作用函数重新执行 ,如果此时数组为空[],证明函数只有在初始化的时候执行一次相当于componentDidMount */
    },[ a ,number ])
    return (<div ref={div} >
        <span>{ userMessage.name }</span>
        <span>{ userMessage.age }</span>
        <div onClick={ ()=> setNumber(1) } >{ number }</div>
    </div>)
}

2.2 useLayoutEffect

它里面的代码同步执行。所以不会出现闪屏

① 首先 useLayoutEffect 是在 DOM 更新之后,浏览器绘制之前,这样可以方便修改 DOM,获取 DOM 信息,这样浏览器只会绘制一次,如果修改 DOM 布局放在 useEffect ,那 useEffect 执行是在浏览器绘制视图之后,接下来又改 DOM ,就可能会导致浏览器再次回流和重绘。而且由于两次绘制,视图上可能会造成闪现突兀的效果。

② useLayoutEffect 的回调函数中代码执行会阻塞浏览器绘制。

如果你使用useEffect去操作state更新页面,发现页面有闪动,你就可以使用useLayoutEffect进行优化,但是useLayoutEffect会阻塞页面更新。因为他是同步执行的。

image.png

    const DemoUseLayoutEffect = () => {
    const target = useRef()
    useLayoutEffect(() => {
        /*我们需要在dom绘制之前,移动dom到制定位置*/
        const { x ,y } = getPositon() /* 获取要移动的 x,y坐标 */
        animate(target.current,{ x,y })
    }, []);
    return (
        <div >
            <span ref={ target } className="animate"></span>
        </div>
    )
}

2.3 useInsertionEffect

它的执行时机是dom更新前,一般我们都不会使用它,具体的使用场景是:为了解决css-in-js在渲染的过程中注入样式,会导致页面重绘问题。所以有了这个hook,我们就可以在dom更新前,把它的样式全部注入进去,就不会出现页面重绘问题了。

image.png

import { useInsertionEffect } from "react";

const Index: React.FC<any> = () => {
  useInsertionEffect(() => {
    const style = document.createElement("style");
    style.innerHTML = `
      .css-in-js{
        color: blue;
      }
    `;
    document.head.appendChild(style);
  }, []);

  return (
    <div>
      <div className="css-in-js">大家好,我是小杜杜,一起玩转Hooks吧!</div>
    </div>
  );
};

export default Index;


总结看看他们的真实的效果

  import { useEffect, useLayoutEffect, useInsertionEffect } from "react";

const Index: React.FC<any> = () => {
  useEffect(() => console.log("useEffect"), []);

  useLayoutEffect(() => console.log("useLayoutEffect"), []);

  useInsertionEffect(() => console.log("useInsertionEffect"), []);

  return <div>大家好,我来了!</div>;
};

export default Index;

image.png

三. 性能优化

3.1 useMemo对比useCallback

useMemo是用来缓存值的hook,useCallback是用来缓存函数的hook。

一般在父组件里面嵌套子组件,如果父组件更新了,子组件就会自动更新。

为了节能,我们想要父组件更新了,如果子组件的props没有发生变化,他就可以不用更新。

此时你可以用React.memo()包裹组件。但是如果父组件给子组件传入的是函数或者是一个对象,你会发现在父组件每刷新一次,子组件也会刷新一次,即使函数表面上并没有发生变化。

这是因为当父组件刷新的时候,函数在内存里面的地址发生了变化,导致React.memo()的比较结果是值发生了变化。

如何解决这个问题呢?就需要useMemouseCallback上场了,用他们把值和函数包裹住,保证,父组件刷新了,只要依赖值不变,函数就不会发生变化,传入子组件以后,子组件就不会更新。

    import { memo, useCallback, useEffect, useMemo, useState } from "react";

function Aaa() {

const [,setNum] = useState(1);

const [count, setCount] = useState(2);

useEffect(() => {

setInterval(()=> {

setNum(Math.random());

}, 2000)

},[]);

const bbbCallback = useCallback(function () {

// xxx

}, []);

const count2 = useMemo(() => {

return count * 10;

}, [count]);

return <div>

<MemoBbb count={count2} callback={bbbCallback}></MemoBbb>

</div>

}

interface BbbProps {

count: number;

callback: Function

}

function Bbb(props: BbbProps) {

console.log('bbb render');

return <h2>{props.count}</h2>

}

const MemoBbb = memo(Bbb);

export default Aaa;

四.工具 hooks

4.1 useDebugValue

useDebugValue 可用于在 React 开发者工具中显示自定义 hook 的标签。这个hooks目的就是检查自定义hooks。 你可以理解成他就是一个用来替换console.log帮你调试的hook。

用法:

import { useState, useDebugValue } from 'react';

function useTestDebug(data: any) {
  useDebugValue("我是snow来了");
  useDebugValue(data);
  return data;
}

export default function MyApp() {
  const [count, setCount] = useState(0);

  function countClick() {
    setCount(c => c + 1);
  }

  useTestDebug(count);

  return <div>
    count: {count}
    <button onClick={countClick}>count + 1</button>
  </div>;
}

测试:

image.png

4.2 useId

当ReactSSR重构项目以后,启动项目,你会发现当服务端渲染一次以后,由于有些组件的id是用随机数获取到的,导致服务端渲染和浏览器渲染产生的id不同,即使浏览器加载的组件其他地方和服务器加载的组件一样,它依旧会因为id不同而重新渲染。这就会造成性能的消耗。为了解决这个问题,react专门提供了一个hooks,帮助组件不管是服务器渲染,还是浏览器渲染,都只有一个id。

const id = useId()

return (
  <>
    <label htmlFor={id}>名字:</label>
    <input id={id} type="text" />
  </>
)