1. React 组件的通信和强化方式

React 组件通信方式:props 和 callback、context(跨层级)、Event 事件、ref传递、状态管理(如:mobx 等) 方式。

强化组件方式:mixin 模式、extends 继承模式、高阶组件模式、自定义 Hooks 模式

2. 10种 React Hooks API 的介绍和使用(V16)

React V16.8 提供的10个API使用方法: useStateuseEffectuseContextuseReduceruseMemouseCallbackuseRefuseImperativeHandleuseLayoutEffectuseDebugValue

2.1 useState

React会使用浅比较来判断新旧状态是否相等,由于浅比较的限制,当你更新状态时,应该始终返回一个新的对象或数组,而不是修改原始对象或数组。这样React才能正确地检测到状态的变化,并触发重新渲染。

2.2 useEffect

副作用,这个钩子成功弥补了函数式组件没有生命周期的缺陷,是我们最常用的钩子之一。

Params:

  • callback:useEffect 的第一个入参,最终返回 destory,它会在下一次 callback 执行之前调用,其作用是清除上次的 callback 产生的副作用;

  • deps:依赖项,可选参数,是一个数组,可以有多个依赖项,通过依赖去改变,执行上一次的 callback 返回的 destory 和新的 effect 第一个参数 callback。

2.3 useContext

上下文,类似于 Context,其本意就是设置全局共享数据,使所有组件可跨层级实现共享。

useContext 的参数一般是由 createContext 创建,或者是父级上下文 context传递的,通过 CountContext.Provider 包裹的组件,才能通过 useContext 获取对应的值。我们可以简单理解为 useContext 代替 context.Consumer 来获取 Provider 中保存的 value 值。

arams:

  • context:一般而言保存的是 context 对象。

Result:

  • contextValue:返回的数据,也就是context对象内保存的value值。

2.4 useReducer

功能类似于 redux,与 redux 最大的不同点在于它是单个组件的状态管理,组件通讯还是要通过 props。简单地说,useReducer 相当于是 useState 的升级版,用来处理复杂的 state 变化。

const [state, dispatch] = useReducer(
    (state, action) => {}, 
    initialArg,
    init
);

Params:

  • reducer:函数,可以理解为 redux 中的 reducer,最终返回的值就是新的数据源 state;

  • initialArg:初始默认值;

  • init:惰性初始化,可选值。

Result:

  • state:更新之后的数据源;

  • dispatch:用于派发更新的dispatchAction,可以认为是useState中的setState

示例:

import { useReducer } from "react";
import { Button } from "antd";

const Index: React.FC<any> = () => {
  const [count, dispatch] = useReducer((state: number, action: any) => {
    switch (action?.type) {
      case "add":
        return state + action?.payload;
      case "sub":
        return state - action?.payload;
      default:
        return state;
    }
  }, 0);

  return (
    <>
      <div>count:{count}</div>
      <Button
        type="primary"
        onClick={() => dispatch({ type: "add", payload: 1 })}
      >
        加1
      </Button>
      <Button
        type="primary"
        style={{ marginLeft: 10 }}
        onClick={() => dispatch({ type: "sub", payload: 1 })}
      >
        减1
      </Button>
    </>
  );
};

export default Index;

特别注意: 在 reducer 中,如果返回的 state 和之前的 state 值相同,那么组件将不会更新。

2.5 useMemo

理念与 memo 相同,都是判断是否满足当前的限定条件来决定是否执行callback 函数。它之所以能带来提升,是因为在依赖不变的情况下,会返回相同的值,避免子组件进行无意义的重复渲染。

可以减少函数的重新计算,不用每次都执行函数计算新的值了。

Params:

  • fn:函数,函数的返回值会作为缓存值;

  • deps:依赖项,数组,会通过数组里的值来判断是否进行 fn 的调用,如果发生了改变,则会得到新的缓存值

Result:

  • cacheData:更新之后的数据源,即 fn 函数的返回值,如果 deps 中的依赖值发生改变,将重新执行 fn,否则取上一次的缓存值。

示例

没有使用 useMemo 时

// bad case
const [count, setCount] = useState(0);
const userInfo = {
  // ...
  age: count,
  name: 'Jace'
}
return <UserCard userInfo={userInfo}>

很明显的上面的 userInfo 每次都将是一个新的对象,无论 count 发生改变没,都会导致 UserCard 重新渲染。

// good case
const [count, setCount] = useState(0);
const userInfo = useMemo(() => {
  return {
    // ...
    name: "Jace",
    age: count
  };
}, [count]);

return <UserCard userInfo={userInfo}>

使用 useMemo后,只有当count改变后才会生成新的对象,如果UserCard组件使用了React.memo那么就不会每次都渲染。

2.6 useCallback

与 useMemo 极其类似,甚至可以说一模一样,唯一不同的点在于,useMemo 返回的是值,而 useCallback 返回的是函数。

注意使用 useCallback 时只缓存函数是没有意义的,当缓存的函数作为属性传递给子组件时才有意义,并且子组件要使用 React.memo 才有意义。 因为无论怎样 useCallback 都会重新定义传递进来的函数,只是如果依赖没有变化的话就会使用之前缓存的函数实例,而不是使用新定义的函数实例,由于使用的是缓存的函数实例,而且子组件使用了 React.momo,所以子组件比较props 发现没有变化那么也就不会触发重新渲染。

关于 useCallback 和 useMemo 的详细比较,可以参考这篇文章 https://juejin.cn/post/6844904101445124110。

2.7 useRef

用于获取当前元素的所有属性,除此之外,还可以缓存数据。

基本使用

const ref = useRef(initialValue);

Params:

  • initialValue:初始值,默认值。

Result:

  • ref:返回的是一个 current 对象,这个 current 属性就是 ref 对象需要获取的内容。

2.8 useImperativeHandle

可以通过 ref 或 forwardRef 暴露给父组件的实例值,所谓的实例值是指值和函数。这个钩子可以让不同的模块关联起来,让父组件调用子组件的方法。在不使用状态管理框架的情况下,这个钩子还是很有用的。

useImperativeHandle(ref, createHandle, deps)

Params:

  • ref:接受 useRef 或 forwardRef 传递过来的 ref;

  • createHandle:处理函数,返回值作为暴露给父组件的 ref 对象;

  • deps:依赖项,依赖项如果更改,会形成新的 ref 对象。

函数式组件的用法

const Child = ({cRef}:any) => {
  const [count, setCount] = useState(0)
  useImperativeHandle(cRef, () => ({
    add
  }))

  const add = () => {
    setCount((v) => v + 1)
  }
  return <div>
    <p>点击次数:{count}</p>
    <Button onClick={() => add()}> 子组件的按钮,点击+1</Button>
  </div>
}

类组件时的用法

如果父组件是类组件,此时不能使用useRef,可以使用 forwardRef 来协助我们处理。

forwardRef: 引用传递,是一种向子组件自动传递引用ref的技术。

讲到这里是不是对 forwardRef 感觉云里雾里的,先来考虑下面这个问题。

组件中允许使用 ref 通过 props 传参吗?答案是不允许,不仅是 ref,key也是不允许的,原因是在 React 内部中,ref 和 key 会形成单独的 key 名。

其实 forwardRef 就是为了解决无法传递 ref 的问题。

经过 forwardRef 包裹后,会将 props(其余参数)和 ref 拆分出来,ref 会作为第二个参数进行传递。如:

import { useState, useRef, useImperativeHandle, Component, forwardRef } from "react";
import { Button } from "antd";

// props 和 ref 分开
const Child = (props:any, ref:any) => {
  const [count, setCount] = useState(0)
  useImperativeHandle(ref, () => ({
    add
  }))
  const add = () => {
    setCount((v) => v + 1)
  }
  return <div>
    <p>点击次数:{count}</p>
    <Button onClick={() => add()}> 子组件的按钮,点击+1</Button>
  </div>
}
const ForwardChild = forwardRef(Child)

class Index extends Component{
  countRef:any = null
  render(){
    return   <>
      <Button
        type="primary"
        onClick={() => this.countRef.add()}
      >
        父组件上的按钮,点击+1
      </Button>
      {/* 使用 forwardRef 包裹后就可以使用 ref 传递了 */}
      <ForwardChild ref={node => this.countRef = node} />
    </>
  }
}
export default Index;

2.9 useLayoutEffect

与 useEffect 基本一致,不同点在于它是同步执行的。简要说明:

  • 执行顺序:useLayoutEffect 是在 DOM 更新之后,浏览器绘制之前的操作,这样可以更加方便地修改 DOM,获取 DOM 信息,这样浏览器只会绘制一次,所以 useLayoutEffect 的执行顺序在 useEffect 之前;

  • useLayoutEffect 相当于有一层防抖效果;

  • useLayoutEffect 的 callback 是同步执行的,因此会阻塞浏览器绘制。

useLayoutEffect 的适用场景

  1. 动态计算DOM元素的尺寸或位置:如果您需要获取DOM元素的尺寸或位置,并且希望在DOM更新后立即进行操作,可以使用useLayoutEffect。例如,您可以使用useLayoutEffect来计算元素的宽度和高度,并根据这些值进行后续的布局或动画操作。

  2. 执行DOM操作后立即触发浏览器重绘:有时,您可能需要在执行DOM操作后立即触发浏览器重绘,以确保用户能够看到更新的结果。useLayoutEffect可以在DOM更新后立即执行,从而确保浏览器重绘。

请注意,由于useLayoutEffect会在DOM更新后立即执行,因此需要谨慎使用,以避免性能问题。如果没有必要立即执行,可以优先考虑使用useEffect,因为它会在浏览器绘制之后异步执行,对性能更友好。

以下是一个示例,展示了如何使用useLayoutEffect来计算元素的尺寸:

import React, { useLayoutEffect, useRef } from 'react';

function Component() {
  const elementRef = useRef(null);

  useLayoutEffect(() => {
    const element = elementRef.current;
    if (element) {
      const width = element.offsetWidth;
      const height = element.offsetHeight;
      // 在这里可以使用获取到的宽度和高度进行后续操作
    }
  }, []);

  return <div ref={elementRef}>Hello, World!</div>;
}

2.10 useDebugValue

useDebugValue可以帮助我们在React开发工具中显示有用的调试信息。它接受一个值和一个格式化函数作为参数,并将它们传递给React开发工具,以便在调试时显示。

以下是一个示例,展示了如何使用useDebugValue

import React, { useDebugValue, useState } from 'react';

function useCounter(initialValue) {
  const [count, setCount] = useState(initialValue);
  useDebugValue(count, count => `Count: ${count}`);
  const increment = () => setCount(count + 1);
  const decrement = () => setCount(count - 1);
  return { count, increment, decrement };
}

function Component() {
  const { count, increment, decrement } = useCounter(0);
  return (
    <div>
      <button onClick={decrement}>-</button>
      <span>{count}</span>
      <button onClick={increment}>+</button>
    </div>
  );
}

3. 5种 React Hooks(V18)

4. 自定义Hooks

4.1 useMount

const useMount = (fn: () => void) => {
  useEffect(() => {
    fn?.();
  }, []);
};

4.2 useUnmount

const useUnmount = (fn: () => void) => {
  const fnRef = useRef(fn);

  useEffect(
    () => () => {
      fnRef.current();
    },
    []
  );
};

4.3 useUnmountedRef

获取当前组件是否卸载。

const useUnmountedRef = (): { readonly current: boolean } => {
  const unmountedRef = useRef<boolean>(false);
  useEffect(() => {
    unmountedRef.current = false;
    return () => {
      unmountedRef.current = true;
    };
  }, []);
  return unmountedRef;
};

4.4 useSafeState

在组件卸载后异步回调内的 setState 不再执行,这样可以避免因组件卸载后更新状态而导致的内存泄漏。

function useSafeState<S>(initialState?: S | (() => S)) {
  const unmountedRef: { current: boolean } = useUnmountedRef();
  const [state, setState] = useState(initialState);
  const setCurrentState = useCallback((currentState: any) => {
    // 如果组件已经卸载了,那么不再更新
    if (unmountedRef.current) return;
    setState(currentState);
  }, []);
  return [state, setCurrentState] as const;
}

4.5 useUpdate

强制组件重新渲染,最终返回一个函数。

实现方式有很多种,可以搞个累加器,每触发一次,就累加 1,这样就会强制刷新。

function useUpdate() {
    const [, update] = useReducer(num => num + 1 , 0)
    
    return update
}

4.6 useDebounceFn

用来处理防抖函数的 Hooks.

export const useDebounceFn = (fn, delay) => {
  const timerRef = useRef(null)

  return (...args) => {
    clearTimeout(timerRef.current)

    timerRef.current = setTimeout(() => {
      fn(...args)
    }, delay)
  }
}

4.7 useDebounce

4.8 useThrottleFn

4.9 useThrottle

4.10 useLockFn

4.11 useLatest