跳到主要内容

Hooks

关于 Hooks 相关知识点总结, 持续更新中……

Hooks 解决了什么问题

首先,hooks 使用场景是函数式组件中,如果没有 hooks, 那么函数组件能做的只有接收 props、渲染,外加触发父组件传过来的事件。那么导致的所有的逻辑都只能在 class 组件中来定义实现,这样就会使得 class 组件内部错综复杂。

class 组件是一种面向对象的体现,class 组件之间的状态会随着功能、逻辑的复杂度变得越来越臃肿,维护起来不是很高效,而且不利于后期的 tree shaking

综上所述,就出现了 hooks, 所以出现的本质原因是:

  • 可以让函数组件做类组建可以做的事情,可以管理自己的状态,处理一些副作用,获取 ref,同时还能做数据缓存。
  • 解决逻辑复用难的问题。
  • 放弃面向对象编程,拥抱函数式编程。

hooks 只能在顶层,且不能在条件,函数,循环,嵌套中使用

具体地说,不要在循环、嵌套、条件语句中使用 Hook——因为这些动态的语句很有可能会导致每次执行组件函数时调用 Hook 的顺序不能完全一致,导致 Hook 链表记录的数据失效。

基础 hooks

useState

useState 可以使函数组件像类组件一样拥有 state,函数组件通过 useState 可以让组件重新渲染,更新视图。

// 使用
const [ ①state , ②dispatch ] = useState(③initData)
  • 在初次渲染时,我们通过 useState 定义了多个状态;
  • 每调用一次 useState ,都会在组件之外生成一条 Hook 记录,同时包括状态值(用 useState 给定的初始值初始化)和修改状态的 Setter 函数;
  • 多次调用 useState 生成的 Hook 记录形成了一条链表;
    • 例如 触发 onClick 回调函数,调用 ②dispatch 函数修改 ①state 的状态,不仅修改了 Hook 记录中的状态值,还即将触发重渲染。

useEffect

用于弥补函数组件没有生命周期的缺陷。其本质主要是运用了 hooks 里面的 useEffectuseLayoutEffect,还有 useInsertionEffect。其中最常用的就是 useEffect 。我们首先来看一下 useEffect 的使用。

useEffect(
() => {
// todo somthing

return () => {
// 清除定时,滞空变量,销毁等操作
};
},
[
// deps 依赖数组
]
);

第一个参数可以有返回值,触发时机是 componentWillUnmount 卸载组件之前执行,常用于清理变量,定时事件绑定等等;

第二个参数为依赖数组,用来控制执行,只有依赖数组中的每一项与上一次相比发生变化时,才会执行,我们可以指定 deps 为空数组 ,这样可以确保 Effect 只会在组件初次渲染后执行

useLayoutEffect

useLayoutEffect 同样是 React 中的副作用。

对于 React 的函数组件来说,其更新过程大致分为以下步骤:

  1. 因为某个事件 state 发生变化。
  2. React 内部更新 state 变量。
  3. React 处理更新组件中 return 出来的 DOM 节点(进行一系列 dom diff 、调度等流程)。
  4. 将更新过后的 DOM 数据绘制到浏览器中。
  5. 用户看到新的页面。

useEffect 在第 4 步之后执行,且是异步的,保证了不会阻塞浏览器进程。 useLayoutEffect 在第 3 步至第 4 步之间执行,且是同步代码,所以会阻塞后面代码的执行。

useRef

useRef 可以用来获取元素,缓存状态,接受一个状态 initState 作为初始值,返回一个 ref 对象 cur, cur 上有一个 current 属性就是 ref 对象需要获取的内容。 可以用于:

  • useRef 获取 DOM 元素
  • useRef 保存状态, 返回值相当于是一个组件内部的全局变量
const cur = useRef(initState);
console.log(cur.current);

useMemo

useMemo 可以在函数组件 render 上下文中同步执行一个函数逻辑,这个函数的返回值可以作为一个新的状态缓存起来。

使用场景有:

  • 函数组件中进行大量的逻辑计算
// 缓存计算结果
export function Scope(props) {
const style = useMemo(() => {
let computedStyle = {};
// 经过大量的计算
return computedStyle;
}, []);

return <div style={style}>内容</div>;
}

// 缓存组件
export function Scope({ children }) {
const renderChild = useMemo(() => {
children();
}, [children]);
return <div>{renderChild} </div>;
}

useCallback

不废话,直接上例子

/* 用react.memo */
const DemoChildren = React.memo((props) => {
/* 只有初始化的时候打印了 子组件更新 */
console.log("子组件更新");
useEffect(() => {
props.getInfo("子组件");
}, []);
return <div>子组件</div>;
});

const DemoUseCallback = ({ id }) => {
const [number, setNumber] = useState(1);
/* 此时usecallback的第一参数 (sonName)=>{ console.log(sonName) }
经过处理赋值给 getInfo */
const getInfo = useCallback(
(sonName) => {
console.log(sonName);
},
[id]
);
return (
<div>
{/* 点击按钮触发父组件更新 ,但是子组件没有更新 */}
<button onClick={() => setNumber(number + 1)}>增加</button>
<DemoChildren getInfo={getInfo} />
</div>
);
};

useCallback() 和 useMemo() 的区别

  • 两者都是为了减少组件的更新次数,优化组件性能
  • useCallback 可缓存函数,其实就是避免每次重新渲染后都去重新执行一个新的函数。
  • useMemo 可缓存值。

有很多时候,我们在 useEffect 中使用某个定义的外部函数,是要添加到 deps 数组中的,如果不用 useCallback 缓存,这个函数在每次重新渲染时都是一个完全新的函数,也就是引用地址发生了变化,这就会导致 useEffect 总会无意义的执行。

useContext

可以使用 useContext ,来获取父级组件传递过来的 context 值,这个当前值就是最近的父级组件 Provider 设置的 value 值,useContext 参数一般是由 createContext 方式创建的 ,也可以父级上下文 context 传递的 ( 参数为 context )。useContext 可以代替 context.Consumer 来获取 Provider 中保存的 value 值。

useContext 接受一个参数,一般都是 context 对象,返回值为 context 对象内部保存的 value 值。

/* 用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>
);
};

useReducer

useReducerreact-hooks 提供的能够在无状态组件中运行的类似 redux 的功能 api 。

// 使用
const [ ①state , ②dispatch ] = useReducer(③reducer)

const reducer = ((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);

const DemoUseReducer = () => {
/* number为更新后的state值, dispatchNumbner 为当前的派发函数 */
const [number, dispatchNumbner] = useReducer(reducer);

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>
);
};

useImperativeHandle

useImperativeHandle 可以配合 forwardRef 自定义暴露给父组件的实例值。这个很有用,我们知道,对于子组件,如果是 class 类组件,我们可以通过 ref 获取类组件的实例,但是在子组件是函数组件的情况,如果我们不能直接通过 ref 的,那么此时 useImperativeHandleforwardRef 配合就能达到效果。

直接上例子:

// 用useImperativeHandle,使得父组件能让子组件中的input自动赋值并聚焦
function Son(props, ref) {
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);

export export function Index (props) {
const inputRef = useRef(null);
const handerClick = () => {
const { onFocus, onChangeValue } = this.cur;
onFocus();
onChangeValue("let us learn React!");
}

return (
<>
<ForwarSon ref={inputRef} />
<Button onClick={handerClick}>操控子组件</Button>
</>
);
}

在 React v18 新增 hooks

useInsertionEffect

useInsertionEffect 是在 React v18 新添加的 hooks ,它的用法和 useEffectuseLayoutEffect 一样。 不过它的执行要在 useLayoutEffect 之前, 也就是 在它执行时 DOM 还没更新。useInsertionEffect 主要是解决 CSS-in-JS 在渲染中注入样式的性能问题。这个 hooks 主要是应用于这个场景,在其他场景下 React 不期望用这个 hooks

export default function Index() {
useInsertionEffect(() => {
/* 动态创建 style 标签插入到 head 中 */
const style = document.createElement("style");
style.innerHTML = `
.css-in-js{
color: red;
font-size: 20px;
}
`;
document.head.appendChild(style);
}, []);

return <div className="css-in-js"> hello , useInsertionEffect </div>;
}

useId

用于生成唯一 ID。

function Demo() {
const rid = useId(); // 生成稳定的 id

return <div id={rid}></div>;
}

这里有一个问题, 在 CSR 普通单页应用中, 使用 useId 确实是稳定的, 但是如果是 SSR 服务端渲染的应用, useId 又是怎么确保它的唯一性的呢?

这里就要解释下 useId 的原理, 不管是在 CSR 还是 SSR 中, 元素最稳定也是最唯一确定的, 应该就是元素 Dom 所处的层级, 那么按照元素层级, 第一层是 1-XXX, 第二层是 2-XXX, 第三层是 3-XXX, 依次往下, 如下图:

那么, 这样生成的 ID 不管是在客户端还是服务端, 他们的层级结构是不变的,所以层级本身就能作为服务端、客户端之间不变的标识。

useId 的实际实现中,层级被表示为 32 进制的数。之所以选择 32 进制,是因为选择尽可能大的进制会让生成的字符串尽可能紧凑。

useTransition

在 React v18 中,有叫做过渡任务的新的概念,大致意思就是不是那么紧急需要更新的任务,就比如:

  • tab 的切换 从 tab1 切换到 tab2 可能中间过程需要一些其他的交互动作。
  • 按钮提交时候,不希望在提交过程中客户重复点击按钮,中间过程的时间差,就需要按钮 loading
/* 使用 */
import { useTransition } from "react";

export default function Index() {
const [data, setData] = useState(null);
const [isPending, startTransition] = useTransition();

const submit = () => {
startTransition(async () => {
const result = await postData(...);

setData(result);
})
}

return (
<>
{/* 一系列表单。。。 */}
<Button loading={isPending}>提交</Button>
</>
)
}

useDeferredValue

useDeferredValue 是一个用于控制组件更新优先级的 Hook。它可以让我们将某个值的更新推迟到更合适的时机,从而避免在高优先级任务执行期间进行不必要的渲染。

使用

import { useState, useEffect, useDeferredValue } from "react";

function LiveSearchComponent() {
const [query, setQuery] = useState("");
const [results, setResults] = useState([]);
const deferredQuery = useDeferredValue(query, { timeoutMs: 200 });

useEffect(() => {
if (deferredQuery !== "") {
// 模拟 API 请求
const fetchData = async () => {
const response = await fetch(`https://api.example.com/search?q=${deferredQuery}`);
const data = await response.json();
return data;
};

fetchData().then((data) => {
setResults(data);
});
} else {
setResults([]);
}
}, [deferredQuery]);

return (
<div>
<input type="text" value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Search..." />
<ul>
{results.map((result) => (
<li key={result.id}>{result.name}</li>
))}
</ul>
</div>
);
}

使用场景

  • 用户输入:在处理实时搜索、自动完成等与用户输入相关的功能时,我们可以使用 useDeferredValue 来确保输入框在用户输入过程中保持流畅,同时在合适的时机更新相关组件。
  • 列表和大型数据集:当需要处理大量数据时,useDeferredValue 可以帮助我们控制渲染的优先级,从而避免阻塞用户界面。例如,在分页加载数据的情况下,我们可以使用 useDeferredValue 在高优先级任务完成后再更新数据列表。

useSyncExternalStore

useSyncExternalStore 能够让 React 组件在 concurrent 模式下安全地有效地读取外接数据源,在组件渲染过程中能够检测到变化,并且在数据源发生变化的时候,能够调度更新。当读取到外部状态发生了变化,会触发一个强制更新,来保证结果的一致性。

import { combineReducers, createStore } from "redux";

/* number Reducer */
function numberReducer(state = 1, action) {
switch (action.type) {
case "ADD":
return state + 1;
case "DEL":
return state - 1;
default:
return state;
}
}

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

function Index() {
/* 订阅外部数据源 */
const state = useSyncExternalStore(store.subscribe, () => store.getState().number);
console.log(state);
return (
<div>
{state}
<button onClick={() => store.dispatch({ type: "ADD" })}>点击</button>
</div>
);
}

// 点击按钮,会触发 reducer ,
// 然后会触发 store.subscribe 订阅函数,
// 执行 getSnapshot 得到新的 number ,
// 判断 number 是否发生变化,如果变化,触发更新。

自定义 hooks

自定义 hooks 是在 React Hooks 基础上的一个拓展,可以根据业务需求制定满足业务需要的组合 hooks ,更注重的是逻辑单元。通过业务场景不同,到底需要 React Hooks 做什么,怎么样把一段逻辑封装起来,做到复用,这是自定义 hooks 产生的初衷。

自定义 hooks 也可以说是 React Hooks 聚合而成的产物,其内部可以有一个或者多个 React Hooks 组成,用于解决一些真实业务中的复杂逻辑。

下面来几个自定义的 hooks

useDebounce 防抖

import { useCallback, useRef } from 'react';

const useDebounce = (fn: Function, delay = 100) => {
const time1 = useRef<any>();

return useCallback((...args) => {
if (time1.current) {
clearTimeout(time1.current);
}
time1.current = setTimeout(() => {
fn(...args);
}, delay);
}, [delay]);
};

export default useDebounce;

useThrottle 节流

import { useCallback, useRef } from 'react';

const useThrottle = (fn: Function, delay = 100) => {
const time1 = useRef<any>();

return useCallback((...args) => {
if (time1.current) {
return;
}
time1.current = setTimeout(() => {
fn(...args);
time1.current = null;
}, delay);
}, [delay]);
};

export default useThrottle;

useUpdateEffect

这个 hooks 主要是为了在组件首次加载时不想执行,只有 update 状态的时候才去执行方法。使用场景

  • 监听状态改变
  • 避免初次渲染执行副作用
import { useEffect, useRef } from "react";

function useUpdateEffect(effect, dependencies) {
const isMounted = useRef(false);

useEffect(() => {
if (isMounted.current) {
effect();
} else {
isMounted.current = true;
}
}, dependencies);
}

useTitle 控制浏览器标题

import { useState, useEffect } from "react";

function useTitle(initialTitle) {
const [title, setTitle] = useState(initialTitle);

useEffect(() => {
document.title = title;
}, [title]);

return setTitle;
}

// 使用
const setTitle = useTitle("This is title");

setTitle("new title");

useForceUpdate

模拟组件强制更新

import { useState } from "react";

function useForceUpdate() {
const [value, setValue] = useState(0);

return () => {
setValue((value) => value + 1);
};
}
// 使用

const forceUpdate = useForceUpdate();

// 模拟更新组件
forceUpdate();