前言

React HookReact 16.8 推出的新特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。没有计划从 React 中移除 class

为什么要有 Hook?

官网 对于动机的解释。

  • 在组件之间复用状态逻辑很难:

    可能需要 render props 和高阶组件(HOC),但是会形成会形成嵌套地狱,而 hooks 支持从组件状态中提取状态逻辑,进行单独的测试和复用,很好的解决这个问题。

  • 复杂组件变得难以理解:

    类组件中,可能需要在不同的生命周期中执行一些逻辑,如事件监听/销毁等需要在不同的生命周期中定义, componentDidMount/componentWillUnMount 中执行,意味着会讲统一逻辑在不同生命周期中去拆分。但是在 hooks 中可以直接在 useEffect 中定义。

  • 难以理解的 class

    this 指向问题,类组件中太多对 this 的使用,如 this.onCLick.bind(this) 等,容易忘记绑定事件处理器。class 不能很好的压缩导致热重载不稳定。

高阶组件 HOC

HOC 的原理其实很简单,它就是一个函数,且它接受一个组件作为参数,并返回一个新的组件,把复用的地方放在高阶组件中,你在使用的时候,只需要在告诫组件内部做不同逻辑处理。

但是容易导致的问题:HOC 的用处不单单是代码复用,还可以做权限控制、打印日志等。但它有也缺陷,例如 HOC 是在原组件上进行包裹或者嵌套,如果大量使用 HOC,将会产生非常多的嵌套,这会让调试变得非常困难;

而且 HOC 可以劫持 props,在不遵守约定的情况下可能造成冲突。

Hook 的设计目标

解决类组件遇到的问题,也就是上面官网的解释,为了解决这三个问题,hooks 应该具备的优点:

  • 复用状态逻辑,支持自定义 hooks
  • 无生命周期的困扰
  • Class 的复杂性,写法简单
  • 对其 Class 组件已经具备的能力

内置 hook

类型 Hook 描述
State Hook
useState ⭐️ 状态:让函数组件具有维持状态的能力。
useReducer ⭐️ 状态:useReducer 是 useState 的一个替代方案,用于在函数组件中管理更复杂的状态逻辑。
Effect Hook
useEffect ⭐️ 执行副作用:允许将组件与外部系统同步,比如数据获取、订阅、手动操作 DOM、定时器等。它通常会在组件渲染后执行。
useLayouEffect 布局副作用:useLayoutEffect 是 useEffect 的一个变换版本,在浏览器重新绘制屏幕之前触发在浏览器重新绘制屏幕前执行。useLayoutEffect 可能会影响性能。
useInsertionEffect 【React 18 新增】在 React 对 DOM 进行更改之前触发,库可以在此处插入动态 CSS。
Context Hook
useContext ⭐️ 上下文:从祖先组件接收信息,而无需将其作为 props 传递。
Ref Hook
useRef ⭐️ ref 允许组件 保存一些不用于渲染的信息,比如 DOM 节点或 timeout ID。
useImpreveHandle ⭐️ 自定义从组件中暴露的 ref handle,如向上暴露子节点的函数和属性。
性能 Hook
useMemo ⭐️ 缓存:每次重新渲染时都能缓存计算的结果。
useCallback ⭐️ 缓存:允许你在多次渲染中缓存函数。
useTransition 【React 18 新增】帮助你在不阻塞 UI 的情况下更新状态
useDeferredValue 【React 18 新增】允许你延迟更新 UI 的某些部分,以让其他部分先更新。
其他 Hook
useDebugValue 自定义在 React 开发者工具中显示的调试信息。
useId 【React 18 新增】用于生成唯一的 ID 标识符,特别适合在无状态的函数组件中为 DOM 元素创建唯一的 ID。
useSyncExternalStore 【React 18 新增】主要用于订阅外部存储(例如 Redux 或其他全局状态管理库)并同步更新组件状态。这个 Hook 解决了在严格模式和并发渲染模式下状态不一致的问题,保证状态始终保持同步。
useActionState 可以根据某个表单动作的结果更新 state 的 Hook。

useState

useState 是一个 React Hook,它允许你向组件添加一个 状态变量

1
2
3
4
5
6
7
8
9
10
function Counter() {
const [count, setCount] = useState(0);

return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}

特点

  • 如果 state 是一个对象,不可局部更新,只能全量替换,如通过展开运算符(...)。
  • 调用 setState 一直会创建新的对象和数组,因为内存地址要变。
  • useStatesetState 都接受函数,如 setState(pre => pre + 1)
  • 自动批处理,且都是异步的。

自动批处理

  • 18 以前,只有在 React 事件处理函数(onChangeonCLick)中进行会批处理(异步)。默认情况下 promisesetTimeoutDOM 原生处理函数(this.click) 都不会进行批处理(同步,这些事件发生在 React 调度流程之外,不会触发批处理更新机制)。

  • React 18 引入了并发渲染的支持,自动批处理。

同步还是异步

  • 批处理与同步异步是两个不同的概念。
  • 通常而言在 17 之前,有同步有异步分情况。不过 18 之后,都是异步的了。
  • 虽然说 setState 在某些情况下是异步的,但实际上它并不是真正意义上的异步,而只是批量更新的一种优化手段。

useReducer

useReducerReact 提供的一个 Hook,用于在函数组件中管理更复杂的状态逻辑。

它是 useState 的一个替代方案,适用于那些有多个子值或者更新逻辑较为复杂的状态。通常情况下,useReducer 被用于管理需要根据不同的动作(actions)来更新的状态,比如你会看到它在 Redux 这样的全局状态管理库中被广泛使用。

基本用法

1
const [state, dispatch] = useReducer(reducer, initialState);
  • useReducer 接受两个参数:

    • reducer 函数:一个纯函数,定义了如何根据 action 来更新 state
    • initialState:初始状态值。
  • useReducer 返回两个值:

    • state:当前的状态值。
    • dispatch:一个函数,用于发送 action 来更新状态。
  • state 是当前的状态。

  • dispatch 是一个函数,用来派发 action 来触发状态更新。

总结起来一句话:我们使用 dispatch 来触发 reducer 纯函数,用 reducer 纯函数中的逻辑修改 initialState,得到一个新的变量,把这个变量赋值给 state,最终返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import { useReducer } from 'react';

function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
return {
age: state.age + 1
};
}
case 'decremented_age': {
return {
age: state.age - 1
};
}
}
throw Error('Unknown action.');
}

export default function Counter() {
const [state, dispatch] = useReducer(reducer, { age: 42 });

return (
<>
<button onClick={() => {
dispatch({ type: 'incremented_age' })
}}>
Increment age
</button>
<button onClick={() => {
dispatch({ type: 'decremented_age' })
}}>
Decrement age
</button>
<p>Hello! You are {state.age}.</p>
</>
);
}

看起来很像 redux 吧?

其实,useReducerReact 的一个 hook,通常用于局部组件状态管理,而 Redux 通常用于跨组件或应用级别的状态管理。

使用场景

  • useReducer 是一个轻量级的状态管理工具,只适用于局部组件(单组件)状态管理,不支持跨组件共享状态。
  • 在需要管理大量数据的场景中,使用 useReducer 更加合适。

useEffect

useEffect 是一个 React Hook,用于在函数组件中执行副作用操作(side effects)。副作用指的是那些对外部世界有影响的操作,比如数据获取、订阅、手动操作 DOM、定时器等。它通常会在组件渲染后执行。

1
useEffect(setup, dependencies?)
  • setup:处理 Effect 的函数。

  • dependencies:可选依赖项,它控制副作用的触发时机。useEffect 会在依赖项发生变化时执行副作用。

特点

  • useEffect 默认会在组件每次渲染后执行。
  • 如果同时存在多个 useEffect,会按照出现次序执行。
  • 可以返回一个清理函数,用于在组件卸载或依赖项更新时清理副作用。
1
2
3
4
5
6
7
8
9
10
useEffect(() => {
const timer = setInterval(() => {
console.log('Count:', count);
}, 1000);

// 返回清理函数
return () => {
clearInterval(timer); // 清理定时器
};
}, [count]); // 每次 count 变化时重新设置定时器

useContext

useContext 是一个 React Hook,可以让你读取和订阅组件中的上下文 context。它使得你能够在组件树中跨层级轻松共享数据,而不需要通过逐层传递 props

1
const value = useContext(ThemeContext)

基本用法

  • 创建 Context: 使用 React.createContext 创建一个上下文对象。
  • 提供 Context: 使用 Context.Provider 组件提供上下文的值,通常在组件树的顶层提供。
  • 消费 Context: 在需要访问上下文值的组件中,使用 useContext 来获取该值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import React, { createContext, useContext, useState } from 'react';

// 创建 Context
const ThemeContext = createContext();

// 提供 Context
function App() {
const [color, setColor] = useState('#123456');

return (
// 使用 Provider 提供 Context 的值
<ThemeContext.Provider value={{ color, setColor }}>
<Component />
</ThemeContext.Provider>
);
}

// 消费
function Component() {
// 使用 useContext 来获取 Context 中的值
const { color, setColor } = useContext(ThemeContext);

return (
<div>
<p>Current color: {color}</p>
<button onClick={() => setColor('#654321')}>Change Color</button>
</div>
);
}

useRef

Ref 提供了一种方式,允许我们访问 DOM 节点或在 render 方法中创建 React 元素。

useRef 返回一个具有单个 current 属性 的 ref 对象,并初始化为你提供的 初始值。在后续的渲染中,useRef 将返回相同的对象。你可以改变它的 current 属性来存储信息,并在之后读取它。

  • state 的区别?

React Hooks 的本质是闭包,闭包是存在内存中的,使用 useRef,可以不通过内存来保存数据,使得这些数据在重渲染时不会被清除。

当改变 ref.current 属性时,React 不会重新渲染组件,但是 state 会触发渲染。

使用场景

  • 缓存值: 使用 ref 缓存一个值或实例对象
  • 操作 DOM:使用 ref 操作 DOM
  • 获取子组件实例

缓存值

比如创建一个定时器

1
2
3
4
5
6
function handleStartClick() {
const intervalId = setInterval(() => {
// ...
}, 1000);
intervalRef.current = intervalId;
}

在之后,从 ref 中读取 interval ID 便可以 清除定时器:

1
2
3
4
function clear() {
const intervalId = intervalRef.current;
clearInterval(intervalId);
}

操作 DOM

使用 ref 操作 DOM 是非常常见的行为。React 内置了对它的支持。

1
2
3
4
5
6
import { useRef } from 'react';

function MyComponent() {
const inputRef = useRef(null);
return <input ref={inputRef} />;
}

React 创建 DOM 节点并将其渲染到屏幕时,React 将会把 DOM 节点设置为 ref 对象的 current 属性。现在可以借助 ref 对象访问 <input>DOM 节点,并访问节点的属性和事件。如获取 inputRef.current.focus();

当组件销毁时,React 会自动将 ref 对象的 current 属性设置为 null。这是 React 的内置机制,确保在组件卸载或节点不再渲染时,引用不会保留对失效 DOM 元素的引用。

获取子组件实例

  • 类组件:直接绑定 ref,就能拿到整个子组件的实例对象。
1
2
3
4
5
6
class Child extends Component {}

const App = () => {
const ref = useRef()
return <Child ref={ref}/>;
}
1
2
3
4
5
6
7
8
9
10
import { forwardRef, useImperativeHandle } from 'react';

const Child = (props, ref) => {
useImperativeHandle(ref, () => {
// 返回要绑定的实例对象
return {};
}, []);
}

const App = forwardRef(Child);

:如果没有通过 forwardRef 包裹,将在控制台得倒错误提示。

img

useImpreveHandle

useImperativeHandleReact 中的一个 Hook,它能让你自定义作为 ref 暴露出来的方法或属性。

1
useImperativeHandle(ref, createHandle, dependencies?)
  • ref: 从 forwardRef 渲染函数 中获得的第二个参数。
  • createHandle:该函数无需参数,它返回你想要暴露的 ref 实例。该实例可以包含任何类型。通常,你会返回一个包含你想暴露的方法的对象。
  • 可选的 dependencies:依赖更新时将重新生成实例分配给 ref

一个完整的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { forwardRef, useRef, useImperativeHandle } from 'react';

const MyInput = forwardRef(function MyInput(props, ref) {
const inputRef = useRef(null);

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

return <input {...props} ref={inputRef} />;
});

默认情况下,组件不会将它们的 DOM 节点暴露给父组件。如果你想要 MyInput 的父组件能访问到 <input> DOM 节点,你必须选择使用 forwardRef

useMemo

useMemo 是一个 React Hook,它在每次重新渲染的时候能够缓存计算的结果。

1
const cachedValue = useMemo(calculateValue, dependencies)
  • calculateValue:要缓存计算值的函数。
  • dependencies:依赖项,依赖发生改变时重新缓存计算值。

跳过代价昂贵的重新计算

1
2
3
4
5
6
import { useMemo } from 'react';

function TodoList({ todos, tab, theme }) {
const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);
// ...
}

跳过组件的重新渲染

如果 List 的所有 props 都与上次渲染时相同,则 List 将跳过重新渲染。

1
2
3
4
5
import { memo } from 'react';

const List = memo(function List({ items }) {
// ...
});

useMemo 和 memo 的区别

  • memo() 是一个高阶组件,我们可以使用它来包装我们不想重新渲染的组件,除非其中的 props 发生变化。
  • useMemo() 是一个 React Hook,我们可以使用它在组件中包装函数。 我们可以使用它来确保该函数中的值仅在其依赖项之一发生变化时才重新计算。

useCallback

useCallback 是一个允许你在多次渲染中缓存函数的 React Hook

1
2
const cachedFn = useCallback(fn, dependencies)

  • fn:想要缓存的函数。
  • dependencies:依赖项,依赖发生改变时缓存函数。

它和 useMemo 出自一脉,useCallback( x => log(x), [m]) 等价于 useMemo(() => x => log(x), [m])

跳过组件的重新渲染

1
2
3
4
5
6
7
8
9
10
11
import { useCallback } from 'react';

function ProductPage({ productId, referrer, theme }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]);
// ...
}

useCallback 与 useMemo 有何区别

useCallback 其实是 useMemo 的另一种实现,如果 useMemo 是个值还好说,如果是返回函数的函数,如 useMmeo(()=>(x) => console.log(x)) 不仅难用,而且难以理解,于是 React 团队就写了语法糖 —— useCallback

  • useMemo 缓存函数调用的结果。
  • useCallback 缓存函数本身

自定义 Hook

自定义 Hook 是一种复用 React 逻辑的方式,允许你将组件中常见的逻辑提取到一个函数中,然后在不同的组件中重用它。自定义 Hookuse 开头,遵循 ReactHook 规则,可以像内置 Hook 一样在函数组件中使用。

特点:

  • Hook 的名称必须永远以 use 开头。
  • 自定义 Hook 共享的是状态逻辑,而不是状态本身。

防抖 useDebounce

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import { useState, useEffect } from 'react';

const useDebounce = (stateValue, wait) => {
const [val, setVal] = useState(stateValue)

useEffect(() => {
const timer = setTimeout(() => {
setVal(stateValue)
}, wait)

return () => {
clearTimeout(timer)
}
}, [stateValue, wait])

return val;
}


const MyComponent = () => {
const [value, setValue] = useState<string>('');
const debouncedValue = useDebounce(value, 500);

return (
<div>
<input
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder="Typed value"
style={{ width: 280 }}/>
<p style={{ marginTop: 16 }}>DebouncedValue: {debouncedValue}</p>
</div>
);
};

节流 useThrottle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import { useRef, useEffect } from 'react';

const useThrootle = (func, wait) => {
const lastCallRef = useRef(0);

const throttledFunction = (...args) => {
const now = Date.now();
if (now - lastCallRef.current >= wait) {
lastCallRef.current = now
func(...args)
}
}
return throttledFunction
}

// 使用
const MyComponent = () => {
const handleScroll = useThrottle(() => {
console.log('Scroll event triggered!');
}, 1000); // 设置 1 秒的节流

useEffect(() => {
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, [handleScroll]);

return (
<div style={{ height: '2000px', background: 'linear-gradient(#fff, #000)' }}>
<h1>Scroll to see throttled events in action!</h1>
</div>
);
};

更多自定义 Hooks自定义 Hook