likes
comments
collection
share

React hooks备忘录

作者站长头像
站长
· 阅读数 29

前言

废话不多说,本文主要是记录日常经常使用的hooks。

hooks的使用规则

  1. 只能在函数式组件中使用。
  2. 只能在最顶层使用hooks,无法在判断、循环等等中使用。
  3. 只能在自定义的hooks中使用其他hooks。

useState

useState可以创建一个状态数据。

什么是状态数据?

视图是由状态组成的,但是并不是所有的数据改变都会触发重新渲染完成驱动视图更新,普通的数据不会触发更新,只有状态数据才会触发更新。在react中通过useState创建状态数据。

const [num, setNum] = useState(0);

const handleClick = () => {
  setNum(num + 1)
}

num就是状态数据,在handleClick事件中通过setNum可以更新num数据,从而重新更新页面。

useState中不仅可以传入一个普通的数据,而且还可以传入一个函数。

const [numList, setNumList] = useState(() => {
  const a = [1,2]
  const b = [3,4]
  return [...a,...b]
});

那么什么时候需要传入函数呢?当useState创建出的状态数据的初始值需要经过复杂计算时传入一个函数。(不常用)

num是状态数据这个没啥好说的,setNum可以改变状态数据,不仅可以传入一个新的值,而且还可以传入一个函数,这个函数的参数是上一次的state,而且这个函数要返回一个值,目的是作为state的新值。

const handleClick = () => {
  setNum((preNum) => preNum + 1)
}

useEffect

useEffect在react组件中可以用来处理副作用,它会在dom渲染完成后执行。一般在useEffect中可以用来发请求获取服务端数据,操作dom。

useEffect(() => {
  // 获取服务器端数据
  const getRemoteData = async () => {
      const data = await fetch('/json')
      console.log(data);
  }
  getRemoteData()
}, [])

useEffect接受的第二个参数被称为依赖,上面的例子是一个空数组。

useEffect第二个参数有下面这几种情况

  • 空数组

    useEffect(() => {
      console.log('执行useEffect,只执行一次');
    }, [])
    

    只在组件渲染完成后执行一次。

  • 不传

    useEffect(() => {
      console.log('执行useEffect,每次渲染,都会执行');
    })
    

    在每次组件渲染后都会执行。

  • 传递一个或多个依赖

    useEffect(() => {
      console.log('执行useEffect,根据条件执行');
    }, [num, count])
    

    第一次组件渲染完成后执行,而且当数组中的值发生变化时,也会执行。

在useEffect接收的回调中,可以返回一个函数,该函数会在组件卸载或者依赖发生变化时执行,当依赖发生变化时,会先执行返回的清理函数,然后再重新执行回调函数。

useEffect(() => {
  console.log('执行 effect')
  const timer = setInterval(() => {
    console.log(num);
  }, 1000);

  return () => {
    console.log('清理 定时器 or其他')
    clearInterval(timer);
  }
}, [num]);

清理函数可以用来清理定时器,销毁实例,移除事件监听或者订阅。

useReducer

useReducer和useState有一样的作用,都是用来管理状态数据,但是useReducer更适合管理复杂逻辑的状态。

useReducer接收两个参数,第一个是reducer函数,用来处理逻辑,另一个是initialState表示初始数据,返回两个参数,第一个是状态数据,另外一个是改变状态数据的函数。

const [result, dispatch] = useReducer(reducer, { num: 0 });

dispatch函数会派发一个action到reducer函数中,reducer函数是一个纯函数,接收一个状态和一个action,dispatch的时候只需要传入action即可,state会自动获取。

interface Data {
  num: number;
}

interface Action {
  type: "INCREMENT" | "DECREMENT";
  payload: number;
}
function App() {
  function reducer(state: Data, action: Action) {
    switch (action.type) {
      case "INCREMENT":
        return {
          num: state.num + action.payload,
        };
      case "DECREMENT":
        return {
          num: state.num - action.payload,
        };
    }
    return state;
  }

  const [result, dispatch] = useReducer(reducer, { num: 0 });

  return (
    <div>
      <button onClick={() => dispatch({ type: "INCREMENT", payload: 2 })}>
        {result.num}
      </button>
    </div>
  );
}

useReducer还有另外一种写法:

const [result, dispatch] = useReducer<Reducer<Data, Action>, string>(
  reducer,
  "zero",
  (param) => {
    return {
      num: param === "zero" ? 0 : 1,
    };
  },
);

useReducer可以传入三个参数,传入的第二个参数就是第三个函数的参数。第三个参数的返回值就是该useReducer返回的初始数据。

这种写法的灵活性增加了。但是并不常用,一般的足以满足需要了。

useReducer相比较useState而言, 把处理逻辑和数据通过reducer组合,不像useState这么单一,可以满足复杂逻辑的使用场景,但是在一般的业务逻辑下,useState就够用了。

useRef

useRef主要有两个作用

  1. 在组件中持久性保存数据
  2. 可以保存DOM引用

持久化保存数据:

React在组件就是一个函数,在组件重新渲染时会重新执行这个函数,如果是普通的变量则会重新赋值,但是如果是useState的变量会在上下文中保存,useRef产生的变量也会被保存,与useState不同的是,用setState修改state的值会重新渲染组件,但是修改useRef的值不会重新保存数据。

所以useRef一般是用来存一些不是用于渲染的内容的,useRef类似于这个组件的全局变量。

useRef获取最新的state的值

 const [count, setCount] = useState(0);
  const countRef = useRef(0);

  const handleIncrement = () => {
    setCount(count + 1);
    countRef.current++;

    console.log("State:", count); // 打印0
    console.log("Ref:", countRef.current); // 打印1
  };

  return (
    <div className="tutorial">
      Count: {count}
      <button onClick={handleIncrement}>Increment</button>
    </div>
  );

点击Increment

  1. 执行setCount(count + 1); 更新state,将count的值变为1
  2. countRef.current++;countRef.current的值赋为1
  3. 打印state的值为0, 最新的state需要下次渲染前才能拿到
  4. 打印countRef.current的值为1,这是最新的。
  5. 页面渲染Count: 1, 但是此时打印count的log语句已经执行完了。

保存DOM引用:

import { useEffect, useRef } from "react";

function App() {
    const inputRef = useRef<HTMLInputElement>(null);
	
  	// 进入页面时input输入框聚焦
    useEffect(() => {
        inputRef.current?.focus();
    },[]);

    return (
        <div>
            <input ref={inputRef}></input>
        </div>
    );
}

export default App;

在dom上通过ref属性保存dom的引用。可以通过inputRef.current来获取到dom引用,然后就可以dom上的方法了。比如上面就是调用了input元素的focus属性。

forwardRef

useRef更多的使用场景还是保存DOM引用,但是父组件怎么获取到子组件的dom呢?通过forwardRef。

forwardRef可以在组件之间传递 ref,这个功能就很强了,可以在父组件拿到子组件的dom。

const ChildComp: React.ForwardRefRenderFunction<HTMLInputElement> = (
  props,
  ref,
) => {
  return (
    <div>
      <input ref={ref}></input>
    </div>
  );
};

const WrapedChild = React.forwardRef(ChildComp);

function App() {
  const ref = useRef<HTMLInputElement>(null);

  useEffect(() => {
    console.log("ref", ref.current);
    ref.current?.focus();
  }, []);

  return (
    <div className="App">
      <WrapedChild ref={ref} />
    </div>
  );
}

这是固定写法,给子组件绑定ref,这个子组件通过forwardRef包裹起来。在子组件内部在通过参数的ref绑定到dom上。这个时候父组件就可以通过ref.current获取到子组件中绑定的dom了。

useImperativeHandle

通过forwardRef可以获取到子组件的dom,但是如果想要获取子组件中的方法或者属性呢?这个时候就要用到useImperativeHandleuseImperativeHandle通常与forwardRef一起配合使用。向父组件自定义暴露属性和方法。(有没有像vue3中的defineExpose)

import { useRef, useState } from "react";
import { useEffect } from "react";
import React from "react";
import { useImperativeHandle } from "react";

interface RefProps {
  getFocus: () => void;
  increment: (num: number) => void;
}

const Child: React.ForwardRefRenderFunction<RefProps> = (props, ref) => {
  const inputRef = useRef<HTMLInputElement>(null);

  const [num, setNum] = useState(0);
  const increment = (n) => {
    setNum(num + n);
  };

  useImperativeHandle(
    ref,
    () => {
      return {
        getFocus() {
          inputRef.current?.focus();
        },
        increment,
      };
    },
    [inputRef, increment],
  );

  return (
    <div>
      <input ref={inputRef}></input>
      <div> child: {num}</div>
    </div>
  );
};

const WrapedChild = React.forwardRef(Child);

function App() {
  const ref = useRef<RefProps>(null);

  useEffect(() => {
    console.log("ref", ref.current);
    ref.current?.getFocus();
  }, []);

  return (
    <div className="App">
      <WrapedChild ref={ref} />
      <button onClick={() => ref.current?.increment(2)}>
        调用子组件的方法
      </button>
    </div>
  );
}

export default App;

而且在上面的例子中,把inputRef放在了子组件内部,这样父组件中不用定义ref了。完成了解藕。

useContext

父子组件之间传入数据一般使用props,但是如果多层级组件或者任意层级组件之间传递用props就太麻烦了,这个时候可以使用Context上下文进行传递数据。

使用Context一般分为3步

  1. 创建上下文

    通过createContext创建上下文对象和并提供默认值。

    const countContext = createContext({
      num: 10,
    });
    
  2. 使用上下文对象的Provider

    在应用的顶层使用Provider

    function App() {
      return (
        <div>
          <countContext.Provider value={{ num: 20 }}>
            <Bbb></Bbb>
          </countContext.Provider>
        </div>
      );
    }
    
  3. 使用useContext

    useContext的作用就是在函数组件中获取上下文对象提供的数据。

    const count = useContext(countContext); // { num: 20 }
    

完整示例

import { createContext, useContext } from "react";

const countContext = createContext({ num: 1 });

function App() {
  return (
    <div>
      <countContext.Provider value={{ num: 20 }}>
          <Bbb></Bbb>
      </countContext.Provider>
    </div>
  );
}

function Bbb() {
  return (
    <div>
      <Ccc></Ccc>
    </div>
  );
}

function Ccc() {
  const count = useContext(countContext);
  return <h2>context 的值为:{count.num}</h2>
}

export default App;

配置一般用context传递

Context对象也可以嵌套使用, 下面countContext与msgContext进行嵌套

import { createContext, useContext } from "react";

const countContext = createContext({ num: 1 });
const msgContext = createContext({ msg: "" });

function App() {
  return (
    <div>
      <countContext.Provider value={{ num: 20 }}>
        <msgContext.Provider value={{ msg: "test" }}>
          <Bbb></Bbb>
        </msgContext.Provider>
      </countContext.Provider>
    </div>
  );
}

function Bbb() {
  return (
    <div>
      <Ccc></Ccc>
    </div>
  );
}

function Ccc() {
  const count = useContext(countContext);
  const msgData = useContext(msgContext);
  return (
    <>
      <h2>context 的值为:{count.num}</h2>
      <h2>msg 的值为:{msgData.msg}</h2>
    </>
  );
}

export default App;

memo

父组件状态数据发生变化时,会重新渲染子组件,但是如果父组件状态的变化对子组件没有影响,那么子组件还继续渲染,那么就会造成性能上的浪费。

react推出了memo api用来包裹函数式组件,对传入子组件的props进行Object.is算法比较,如果props变了,就会重新渲染子组件,如果没变则不会重新渲染。

import { useState, memo } from "react";

function ChildComponent(props) {
  console.log("ChildComponent 渲染" + props.msg);
  return <p>{props.msg}</p>;
}

const MemoizedChildComponent = memo(ChildComponent);

function ParentComponent() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Current count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>

      <ChildComponent msg="非memo组件" />
      <MemoizedChildComponent msg="memo组件" />
    </div>
  );
}

export default ParentComponent;

点击increment按钮,状态数据count改变,ParentComponent组件重新渲染, 这个时候ChildComponent会重新渲染,但是MemoizedChildComponent组件不会重新渲染,因为组件被memo包裹了 。

但是如果给MemoizedChildComponent组件传入count,那么当count发生改变时,也会重新渲染这个组件。

<MemoizedChildComponent msg="memo组件" count={count} />

memo 是用来防止 props 没变的时候造成的重新渲染

useMemo

useMemo也是用来做性能优化的,它的作用是在组件重新渲染时,可以缓存该组件中的属性的值。可以避免昂贵的计算。

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

function App() {
  // 复杂的计算
  const computedValue = () => {
    console.log("jisuan....");

    const arr = [];
    for (let i = 0; i < 10000000; i++) {
      arr.push(i);
    }
    return arr;
  };
  const startTime = Date.now();
  useEffect(() => {
    const endTime = Date.now();
    console.log(endTime - startTime);
  });

  const value = computedValue();

  const [state, setState] = useState(0);
  return (
    <div>
      <div>
        <button onClick={() => setState(Math.random())}>{state}</button>
      </div>
    </div>
  );
}

export default App;

多次点击按钮在控制台打印的结果是:

React hooks备忘录

如果把 const value = computedValue();这个耗时计算使用useMemo进行包裹之后。

 const value = useMemo(() => computedValue(), []);

打印的结果是:

React hooks备忘录

除了第一次慢点之外,后面几次都是几毫秒,说明使用了缓存结果,这样就避免了每次重新渲染的时候都会执行类似的昂贵的计算。

使用useMemo缓存属性还可以避免子组件的重新渲染。

当在组件中需要定义一个普通的对象数据,这个普通的对象数据需要传递给子组件,父组件重新渲染, 子组件的props没有改变,而且子组件使用了memo进行包裹,子组件仍然会重新渲染,这是定义的普通数据每次渲染都会生成新的对象。

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

function Child(props) {
  console.log("组件重新渲染");
  return <div>{props.info.a}</div>;
}

const MemoChild = memo(Child);

function App() {
  const info = { a: 1, b: 2 };

  const [state, setState] = useState(0);
  return (
    <div>
      <MemoChild info={info}></MemoChild>
      <button onClick={() => setState(state + 1)}>{state}</button>
    </div>
  );
}
export default App;

点击按钮,发现每次子组件都会重新渲染。

React hooks备忘录

将info使用useMemo包裹之后

const info = useMemo(() => ({ a: 1, b: 2 }), []);

不会造成重新渲染了。

React hooks备忘录

useMemo有点像vue中的computed。

useMemo的第二个参数就是依赖,当依赖发生改变的时候(也是Object.is算法比较),就会重新执行useMemo的回调函数,产生新的值。

useCallback

useCallback的用法和useMemo是一样的,而且useCallback和useMemo一样也是做性能优化的,不同的是useCallback缓存的是函数,useMemo缓存的是值。还记得上面例子定义的info变量吗?我们使用useMemo成功缓存了info的值,避免了子组件的重新渲染,但是如果info变量是一个函数呢?

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

function Child(props) {
  console.log("组件重新渲染");
  return <div>111</div>;
}

const MemoChild = memo(Child);

function App() {
  const info = () => {
    console.log("info函数");
  };

  const [state, setState] = useState(0);
  return (
    <div>
      <MemoChild info={info}></MemoChild>
      <button onClick={() => setState(state + 1)}>{state}</button>
    </div>
  );
}
export default App;

按钮点击,子组件还是会重新渲染。

将函数使用useCallback缓存起来。

const info = useCallback(() => {
  console.log("info函数");
}, []);

按钮点击,子组件就不会重新渲染了。

React hooks备忘录

useMemo和useCallback都是用来做性能优化的,但是如果为了避免子组件重新渲染的性能优化,那么一定要和memo配合。

useCallback的第二个参数也是一个依赖数组,Object.is算法比较数组中值的变化,如果发生改变那么就会重新执行,产生一个新的函数。

useLayoutEffect

useLayoutEffect的功能与useEffect功能一样,但是这个hooks不太常用,通常用来解决useEffect会造成页面闪烁问题,这与浏览器绘制有关。为了简单的理解它,看一个例子:

import { useState, useEffect } from "react";

function App() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    if (count === 0) {
      const randomNum = 1 + Math.random() * 1000;
      setCount(randomNum);
    }
  }, [count]);

  return <button onClick={() => setCount(0)}>{count}</button>;
}
export default App;
  1. 启动项目
  2. 连续点击button按钮
  3. 发现页面闪烁

为什么会出现页面闪烁呢?

useEffect会在页面渲染完成后执行。

  1. 点击按钮后,setCount(0)会触发页面重新渲染,将页面上的数字改为0
  2. 渲染完成后,执行useEffect方法, 将页面上的数字改为一个随机数,页面重新渲染
  3. 两次页面渲染的间隔太短,但是还是需要一定时间去绘制的,所以就会出现页面闪烁的情况。

使用useLayoutEffect来解决,这个hooks会在浏览器重新绘制屏幕之前触发,也就是会先执行useLayoutEffect然后在去重新渲染页面。

useLayoutEffect(() => {
    if (count === 0) {
      const randomNum = 1 + Math.random() * 1000;
      setCount(randomNum);
    }
}, [count]);
  1. 点击按钮后,setCount(0)将count的值改为0,但是不会重新渲染页面,这是会先等待useLayoutEffect执行
  2. 执行useLayoutEffect,将count的值改为一个随机数。
  3. 页面重新渲染

页面不会闪烁了。

因为页面渲染会等待useLayoutEffect的执行,所以可能会降低页面渲染的性能。谨慎使用

那么什么时候会使用呢?

当出现页面渲染发生闪烁的场景时,尝试使用useLayoutEffect是否能够解决。

比如当A元素需要依赖X元素的位置信息时,X元素是时常变动的,如果使用useEffect,会造成A元素的闪烁,这个时候使用useLayoutEffect计算X元素的位置信息后,在显示A元素。

总结:

  • useState:可以定义和改变状态数据,返回两个值,第一个是当前state 的值,第二个是一个改变state值的函数,通过setState修改state的值会重新渲染该组件。
  • useEffect:执行副作用(side effect),如:发送请求、记录日志、定时器等。有两个参数,第一个是想在useEffect中执行什么代码逻辑,返回一个函数,返回的函数会在会在组件卸载或者依赖发生变化时执行,用来清理副作用; 第二个参数依赖,当依赖发生变化时会重新执行useEffect。
  • useLayoutEffect:与useEffect是一样的使用方式,useLayoutEffect会阻塞页面渲染,但是能解决页面闪烁问题,它可能会造成性能损耗,用的比较少。
  • useReducer:useState功能上一样,但是可以用来处理更复杂的逻辑。有两个参数,第一个是reducer,第二个为初始值。返回两个值,state 的值和dispatch 方法。dispatch方法会触发reducer函数,reducer中改变数据重新触发页面渲染。
  • useCallback:性能优化的hooks,用来缓存函数,第一个参数是要缓存的函数,第二个参数是一个依赖数组依赖数变了,会重新执行,产生一个新的函数,如果useCallback缓存的函数要传入子组件,为了防止子组件重新渲染,需要memo进行配合。
  • useMemo:性能优化的hooks,用来缓存值,第一个参数是要缓存的值,第二个参数是一个依赖数组依赖数变了,会重新执行,产生一个新的值,如果useMemo缓存的函数要传入子组件,为了防止子组件重新渲染,需要memo进行配合。
  • useRef:可以把useRef看成组件的全局变量, 可以储存不需要渲染但是需要保存的值。useRef也可以保存DOM引用,与ref关键字配合使用。
  • useImperativeHandleuseImperativeHandle通常与forwardRef一起配合使用。子组件向父组件自定义暴露属性和方法。父组件中可以调用。
  • useContext: 父子组件之间传入数据一般使用props,多层级组件或者任意层级组件传递数据用Context上下文进行传递数据。createContext创建上下文,useContext使用上下文数据。
转载自:https://juejin.cn/post/7386969940940767266
评论
请登录