Hook简介
Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。先看两个官方文档中提供的展示Hook用法的例子。
-
State Hook
import React, { useState } from 'react'; function Example() { // useState 是一个hook,声明一个叫 "count" 的 state 变量,初始值为 0。返回当前的值和更改值的函数。 const [count, setCount] = useState(0); return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div> ); } -
Effect Hook
import React, { useState, useEffect } from 'react'; function Example() { const [count, setCount] = useState(0); // Similar to componentDidMount and componentDidUpdate: useEffect(() => { // 组件初始化和更新后设置文档标题 document.title = `You clicked ${count} times`; }); return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div> ); }
Hook 要解决的问题
- 组件间难以复用状态逻辑。
- 复杂组件内逻辑的状态和副作用代码分散难以理解。
- class组件理解不易、不利于工具优化 。
引入 Hook 的影响
- 可选:无需重写现有代码,不破坏现有结构,不取代现有React概念,渐进使用。
- 兼容:100%向后兼容,不包含破坏性改动。
- 自 v16.8.0 可用。
使用 Hook 的限制与检测工具
限制:
- 只在最顶层使用 Hook,不要在循环、条件或嵌套函数中调用 Hook
- 只在 React 函数组件和自定义Hook中调用 Hook,不要在普通的 JavaScript 函数中调用 Hook
React 提供了一个elint插件 eslint-plugin-react-hooks 可用于检测 Hook 被正确使用。
react-hooks/rules-of-hooks: 检查 Hook 的规则react-hooks/exhaustive-deps:检查 effect 的依赖
内置Hook的使用
内置10 个 Hook 分别是:
useState:用于申明和更新组件 state。userEffect:用于传入函数完成Dom变更等副作用操作,该函数会在组件每次渲染结束后执行。userContext:用于接受上层Context对象并返回当前值。useReducer:用于组件复杂状态的处理,可以理解为一个简易的redux。useCallback:用于返回一个 memoized 的回调函数,依赖不改变时返回的函数引用不变,常用于事件绑定或函数props传递。userMemo:返回一个 memoized 值,依赖不改变时不会重新执行函数直接使用上次计算的值。userRef:返回一个 ref 对象,可看作组件的实例变量,ref.current的值变化时不会通知触发重新渲染。userImperativeHandle:配合 ref 使用给父组件暴露实例方法和值。userLayoutEffect:基本等同与userEffect,不过会在绘制前执行。userDebugValue:用于自定义Hook调试,会在React 开发者工具中显示自定义Hook的标签。
以下介绍各Hook详细使用方法。
0. 相关类型
以下为内置Hook函数定义依赖的类型定义。
type Dispatch<A> = A => void;
type BasicStateAction<S> = (S => S) | S;
1. useState
定义
useState<S>(
initialState: (() => S) | S
): [S, Dispatch<BasicStateAction<S>>]
描述
申明一个state并设置初始值,返回一个 state值及更新 state 的函数。
示例
import React, { useState } from "react";
export default function App({ initialCount = 0 }) {
const [count, setCount] = useState(initialCount);
return (
<>
Count: {count}
<button onClick={() => setCount(count + 1)}>+</button>
</>
);
}
points
- useState返回一个包含当前状态和更新状态函数的数组,通过解构自行命名。
- useState 接受一个函数惰性计算初始值,非首次渲染时会跳过该函数执行。eg:
useState(computedFunction)}。 - setCount 更新时行为是覆盖当前值而不是合并到当前值。
- setCount 可接受一个函数,函数接受先前state返回更新后的值。eg:
setCount(prevCount => prevCount + 1)}。
2. userEffect
定义
useEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void
描述
使用 userEffect 传入函数完成副作用操作,该函数会在组件每次渲染结束后执行。
示例
import React, { useState, useEffect } from 'react';
export default function App() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
备注
- 支持返回一个函数用于清除副作用,副作用和清除副作用函数在每次渲染后都会执行。若需要在渲染前执行,可参考 useLayoutEffect。
- 支持传入第二个参数指明副作用函数依赖的数组,依赖未变化时将跳过副作用处理。
- 若需要根据条件动态决定是否执行,需要将条件放在副作用函数内。
3. userContext
定义
useContext<T>(context: ReactContext<T>): T
用途
用于接受上层 Context 对象并返回当前值。
示例
import React, { useContext } from "react";
const Context = React.createContext();
function Counter() {
const count = useContext(Context);
return <div>Current: {count}</div>;
}
export default function App() {
return (
<Context.Provider value={1}>
<Counter />
</Context.Provider>
);
};
备注
- 需要配合上层组件中 Context.Provider 一起使用。
4. useReducer
定义
useReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: I => S,
): [S, Dispatch<A>]
用途
用于组件复杂状态的处理,可以理解为一个简易的 redux。
示例
import React, { useReducer } from "react";
const initialState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case "increment":
return { count: state.count + 1 };
case "decrement":
return { count: state.count - 1 };
default:
throw new Error();
}
}
export default function App() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({ type: "decrement" })}>-</button>
<button onClick={() => dispatch({ type: "increment" })}>+</button>
</>
);
}
备注
- 接受第二个参数
initialArg为初始值,若初始值计算量大,可以通过传入第三个参数init函数来进行初始化。
5. useCallback
定义
useCallback<T>(callback: T, deps: Array<any> | void | null): T
用途
用于返回一个 memoized 的回调函数,依赖不改变时返回的函数引用不变,常用于事件绑定或函数props传递。
示例
import React, { useState, useCallback } from "react";
class Button extends React.PureComponent {
render() {
console.log("rendered ", this.props.text);
return <button onClick={this.props.onClick}>{this.props.text}</button>;
}
}
export default function App() {
const [count1, setCount1] = useState(0);
const memoizedOnClick = useCallback(function onClick() {
setCount1((count1) => count1 + 1);
}, []);
const [count2, setCount2] = useState(0);
const onClick = function onClick() {
setCount2((count2) => count2 + 1);
};
// 点击button1和button2时只有button2会重新渲染
return (
<>
{count1}
<Button text="button1" onClick={memoizedOnClick} />
{count2}
<Button text="button2" onClick={onClick} />
</>
);
}
备注
useCallback(fn, deps)相当于useMemo(() => fn, deps)。
6. userMemo
定义
useMemo<T>(create: () => T, deps: Array<any> | void | null): T
用途
返回一个 memoized 值,依赖不改变时不会重新执行函数直接使用上次计算的值
示例
import React, { useState, useMemo } from "react";
export default function App() {
const [count1, setCount1] = useState(2);
// 点击 button2 触发重新渲染时不会重新计算result
const result = useMemo(
function () {
console.log("computed pow");
return Math.pow(count1, 2);
},
[count1]
);
const [count2, setCount2] = useState(2);
return (
<>
pow: {result} <br />
count2: {count2} <br />
<button onClick={() => setCount1(count1 + 1)}>button1</button>
<button onClick={() => setCount2(count2 + 1)}>button2</button>
</>
);
}
7. userRef
定义
useRef<T>(initialValue: T): {current: T}
用途
返回一个 ref 对象,可看作组件的实例变量,ref.current的值变化时不会通知触发重新渲染
示例
// 示例1: 获取dom节点
import React, { useRef } from "react";
export default function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// 获取dom节点
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
// 示例2: 组件实例变量
import React, { useEffect, useRef, useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
// intervalId 用于组件的实例变量
let intervalId = useRef(null);
useEffect(() => {
intervalId.current = setInterval(() => {
setCount((c) => c + 1);
}, 1000);
console.log("create ", intervalId.current);
return () => {
console.log("clear ", intervalId.current);
clearInterval(intervalId.current);
};
}, []);
return (
<>
count: {count}; intervalId.current: {intervalId.current}
<button
onClick={() => {
// 示例3: 点击stop后更改 intervalId 的值界面不会重新渲染
clearInterval(intervalId.current);
intervalId.current = Math.random();
}}
>
stop
</button>
</>
);
}
export default function App() {
const [visible, setVisible] = useState(true);
return (
<>
{visible && <Counter />}
<button onClick={() => setVisible((v) => !v)}>Toggle</button>
</>
);
}
备注
- 可用于获取dom节点
- 可用于组件实例变量
- ref.current更新不会引发组件重新渲染
8. userImperativeHandle
定义
useImperativeHandle<T>(
ref: {current: T | null} | ((inst: T | null) => mixed) | null | void,
create: () => T,
deps: Array<any> | void | null,
): void
用途
配合 ref 暴露实例方法和值给父组件使用
示例
import React, {
forwardRef,
useRef,
useState,
useImperativeHandle
} from "react";
const Counter = forwardRef(function Counter(props, ref) {
const [count, setCount] = useState(0);
useImperativeHandle(ref, () => ({
increment: () => { // 暴露increment方法
setCount((c) => c + 1);
}
}));
return <>count: {count}</>;
});
export default function App() {
const counterRef = useRef();
return (
<>
<Counter ref={counterRef} />
<button onClick={() => counterRef.current.increment()}>increment</button>
</>
);
}
备注
- 尽量避免使用
- 应当与 forwardRef 一起使用
9. userLayoutEffect
定义
useLayoutEffect(
create: () => (() => void) | void,
deps: Array<any> | void | null,
): void
用途
基本等同与 userEffect,不过会在dom变更后绘制前执行
示例
// styles.css
.container {
background: green;
height: 200px;
position: relative;
margin-bottom: 10px;
}
.inner {
display: block;
width: 130px;
height: 50px;
background: red;
position: absolute;
left: 0;
top: 0;
transition: all 3s;
}
.active {
left: 100px;
}
// App.js
import React, { useLayoutEffect, useRef, useEffect } from "react";
import "./styles.css";
export default function App() {
const ref1 = useRef();
const ref2 = useRef();
useLayoutEffect(() => {
// 1. 初次执行 render 渲染成 HTML
// 2. 执行 useEffect 增加类 active
// 3. 初次绘制 left:100,看不到动画
ref1.current.className = "inner active";
}, []);
useEffect(() => {
// 1. 初次执行 render 渲染成 HTML
// 2. 初次绘制 left:0
// 3. 执行 useEffect 增加类 active
// 4. 再次绘制 left:100 触发 transition,可以看到动画
ref2.current.className = "inner active";
}, []);
return (
<div>
<div className="container">
<div ref={ref1} className="inner">
useLayoutEffect
</div>
</div>
<div className="container">
<div ref={ref2} className="inner">
useEffect
</div>
</div>
</div>
);
}
10. userDebugValue
定义
useDebugValue<T>(value: T, formatterFn: ?(value: T) => any): void
用途
用于自定义Hook调试,会在React 开发者工具中显示自定义Hook的标签。
示例
import React, { useDebugValue, useState } from "react";
function useFriendStatus() {
const [isOnline] = useState("1");
// 在开发者工具中的这个 Hook 旁边显示标签
// e.g. "FriendStatus: Online"
useDebugValue(isOnline ? "Online" : "Offline");
return isOnline;
}
export default function App() {
const isOnline = useFriendStatus();
return <>isOnline: {isOnline}</>;
}
备注
- 接受一个格式化函数作为可选的第二个参数。该函数只有在 Hook 被检查时才会被调用。它接受 debug 值作为参数,并且会返回一个格式化的显示值。eg:
useDebugValue(date, date => date.toDateString());
自定义Hook
- 自定义 Hook 就是调用了 Hook 的普通函数,强烈推荐名称以“use” 开头命名。这是一个非强制约定,不准守不影响功能运行,但是影响阅读体验和工具识别和检测Hook 规则。
- 可以在函数中调用其他内置或自定义 Hook,同样需要准守Hook的限制如只能在顶层调用。
- 通过自定义 Hook可以将组件逻辑提取到单独的函数实现复用,若目的不是重用组件逻辑且未调用任何Hook不要将函数命名为“use”开头。
以下是一个自定义Hook的例子,用于重用聊天和评论消息送达时间的格式化和随时间更新逻辑。
import React, { useRef, useEffect, useState } from "react";
function formatTime(time) {
const now = +new Date();
const offsetSeconds = Math.round((now - time) / 1000);
const Minute = 60;
const Hour = Minute * 60;
const Day = Hour * 24;
if (offsetSeconds < Minute) {
return `${offsetSeconds}秒前`;
} else if (offsetSeconds < Hour) {
return `${Math.round(offsetSeconds / Minute)}分前`;
} else if (offsetSeconds < Day) {
return `${Math.round(offsetSeconds / Hour)}小时前`;
} else {
return `${Math.round(offsetSeconds / Day)}天前`;
}
}
function useFormatMessageTime(arrivedTime) {
const [formattedTime, setFormattedTime] = useState(() =>
formatTime(arrivedTime)
);
const ref = useRef();
useEffect(() => {
console.log("setInterval");
ref.current = setInterval(() => {
setFormattedTime(formatTime(arrivedTime));
});
return () => {
console.log("clearInterval");
clearInterval(ref.current);
ref.current = null;
};
}, [arrivedTime]);
return formattedTime;
}
export default function App() {
const [time, setTime] = useState(new Date());
const formattedTime = useFormatMessageTime(time);
return (
<>
message arrived at: {formattedTime}
<button onClick={() => setTime(new Date())}>set now</button>
</>
);
}
实现原理
推荐阅读:
- 译 React hooks: 不是魔法,只是数组:利用闭包数组保存hook状态
- Fiber架构的简单理解与实现:利用 requestIdleCallback 和保存当前任务实现渲染任务细化、hook以链表形式挂在fiber上、简单的dom diff
第二个文章中的代码运行有一点错误,修正后如下:codesandbox.io/s/react-fib…
对比Vue
文章 对比 React Hooks 和 Vue Composition API 已对此有较为详细的对比。简单列举其中讲诉的异同点。
相同点:
- 解决状态复用和逻辑代码点分散的问题
- API有相似之处
不同点:
- React 基于链表实现有调用顺序的限制,Vue基于Proxy实现没有此限制。
- React 需要手动定义hook 依赖但提供了优化跳过执行的手段,Vue可以自动收集依赖。
- React 的hook需要在每次渲染时都会执行,vue会放在setup里执行一次。
- React 提供的useEffect处理副作用类似组件执行时相当于多个生命周期,Vue为不同的选项式生命周期提供不同的API。