React Hooks
1. React 组件的通信和强化方式
React 组件通信方式:props 和 callback、context(跨层级)、Event 事件、ref传递、状态管理(如:mobx 等) 方式。
强化组件方式:mixin 模式、extends 继承模式、高阶组件模式、自定义 Hooks 模式
2. 10种 React Hooks API 的介绍和使用(V16)
React V16.8 提供的10个API使用方法: useState、useEffect、useContext、useReducer、useMemo、useCallback、useRef、useImperativeHandle、useLayoutEffect、useDebugValue
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 的适用场景
-
动态计算DOM元素的尺寸或位置:如果您需要获取DOM元素的尺寸或位置,并且希望在DOM更新后立即进行操作,可以使用
useLayoutEffect。例如,您可以使用useLayoutEffect来计算元素的宽度和高度,并根据这些值进行后续的布局或动画操作。 -
执行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)
}
}