React-hooks

什么是 hooks?

Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。

我们先来看一个例子,这是传统的 class 组件的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React from "react";

class index extends React.component {
state = {
msg: "你好",
};
updateMsg = () => {
this.setState({
msg: "真好",
});
};
render() {
<div>
<span>{this.state.msg}</span>
<button onClick={this.updateMsg}>点击修改值</button>
</div>;
}
}
export default index;

将上面的例子转换成 hooks 的写法

1
2
3
4
5
6
7
8
9
10
import { useState } from "react";
function index() {
let [msg,setMsg] = useState("你好)
return (
<div>
<span>{msg}</span>
<button onClick={()=>updateMsg("真好")}>点击修改值</button>
</div>
);
}

仔细感受一下异同

解决的问题

  • 在组件之间复用状态逻辑很难,可能要用到 render props 和高阶组件,React 需要为共享状态逻辑提供更好的原生途径,Hook 使你在无需修改组件结构的情况下复用状态逻辑
  • 复杂组件变得难以理解,Hook 将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据)
  • 难以理解的 class,包括难以捉摸的this

注意事项

  • 只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。
  • 只能在 React 的函数组件中调用 Hook。不要在其他 JavaScript 函数中调用

hooks 规则校验插件

eslint-plugin-react-hooks - npm (npmjs.com)

1
2
3
4
5
6
7
8
9
10
11
12
// 你的 ESLint 配置
{
"plugins": [
// ...
"react-hooks"
],
"rules": {
// ...
"react-hooks/rules-of-hooks": "error", // 检查 Hook 的规则
"react-hooks/exhaustive-deps": "warn" // 检查 effect 的依赖
}
}

useState 声明状态和设置状态

state 只在组件首次渲染的时候被创建。在下一次重新渲染时,useState 返回给我们当前的 state。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React, { useState } from "react";

function UseState() {
// [state, setState]
// 创建一个名为 count 的 state 并且设置初始化的值为 0,通过 setCount(值) 来修改 count 的值
let [count, setCount] = useState(0);

return (
<div className="use-state" style={{ border: "1px solid pink" }}>
<h1>这里是 UseState 组件</h1>
<div>{count}</div>
<button onClick={() => setCount(count + 1)}>点击累加</button>
</div>
);
}

export default UseState;

useReducer 替代 useState

  • useState 的替代方案。它接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法
  • 在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 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
import React, { useReducer } from "react";

const initialState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case "add":
return { count: state.count + 1 };
case "sub":
return { count: state.count - 1 };
default:
return { ...state };
}
}
export default function UseReducer() {
// reducer 函数返回值 initialState 初始化值
const [state, dispatch] = useReducer(reducer, initialState);

return (
<div>
<h1>useReducer</h1>
<span>{state.count}</span>
<br />
<button onClick={() => dispatch({ type: "add" })}>+</button>
<button onClick={() => dispatch({ type: "sub" })}>-</button>
</div>
);
}

惰性初始化

{17,1-3}
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
function init() {
return initialState;
}
const initialState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case "add":
return { count: state.count + 1 };
case "sub":
return { count: state.count - 1 };
default:
return { ...state };
}
}
export default function UseReducer() {
// reducer 函数返回值 initialState 初始化值
const [state, dispatch] = useReducer(reducer, initialState, init);

return (
<div>
<h1>useReducer</h1>
<span>{state.count}</span>
<br />
<button onClick={() => dispatch({ type: "add" })}>+</button>
<button onClick={() => dispatch({ type: "sub" })}>-</button>
</div>
);
}

useContext 跨越关系的传值

  • 接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值
  • 当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定
  • 当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值
  • useContext(MyContext) 相当于 class 组件中的 static contextType = MyContext 或者 <MyContext.Consumer>
  • useContext(MyContext) 只是让你能够读取 context 的值以及订阅 context 的变化。你仍然需要在上层组件树中使用 <MyContext.Provider> 来为下层组件提供 context
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
import React, { useContext } from "react";
// 1
const MsgContext = React.createContext("你好");
export default function UseContext() {
return (
<div>
<h1>useContext</h1>
{/* 2 */}
<MsgContext.Provider value={"你好"}>
<Son></Son>
</MsgContext.Provider>
</div>
);
}

function Son() {
// 3
const context = useContext(MsgContext);
return (
<div>
<h1>子组件</h1>
{/* 4 */}
<span>{context}</span>
</div>
);
}

useEffect 模拟生命周期函数

跟 class 组件中的 componentDidMount、componentDidUpdate 和 componentWillUnmount 具有相同的用途

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
import { useState, useEffect } from "react";

const UseEffect = () => {
let [count, setCount] = useState(0);

useEffect(() => {
// componentDidMont
console.log("数据渲染了");
// componentDidUpdate
console.log("数据更新了");
// componentWillUmount
return () => {
console.log("组件卸载了");
};
});
return (
<div className="use-state" style={{ border: "1px solid pink" }}>
<h1>这里是 UseEffect 组件</h1>
<div>{count}</div>
<button onClick={() => setCount(count + 1)}>点击累加</button>
</div>
);
};

export default UseEffect;

useEffect 会在每次渲染都执行.默认情况下,它在第一次渲染之后和每次更新之后都会执行。React 保证了每次运行 effect 的同时,DOM 都已经更新完毕。

useEffect 监听 state 的变化

如同 vue 里面的 watch 一样 react hooks 也提供了监听变量变化的功能

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 { useState, useEffect } from "react";

const UseEffect = () => {
let [count, setCount] = useState(0);

useEffect(() => {
// componentDidMont
console.log("数据渲染了");
// componentDidUpdate
console.log("数据更新了");
// componentWillUmount
return () => {
console.log("组件卸载了");
};
});
// 监听 count 更新
useEffect(() => {
console.log("count 更新了", count);
}, [count]);

return (
<div className="use-state" style={{ border: "1px solid pink" }}>
<h1>这里是 UseEffect 组件</h1>
<div>{count}</div>
<button onClick={() => setCount(count + 1)}>点击累加</button>
</div>
);
};

export default UseEffect;

useEffect 模拟 componentDidMount

1
2
3
4
useEffect(() => {
console.log("UseEffect.jsx 初始化");
// 没有监听的 state
}, []);

useEffect 模拟 componentDidUpdate

1
2
3
4
5
6
useEffect(() => {
console.log("UseEffect.jsx conut数据变化了");
}, [count]);
useEffect(() => {
console.log("UseEffect.jsx 数据变化了");
});

useEffect 模拟 componentWillUnmount

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
import React, { useState, useEffect } from "react";

export default function UseEffect() {
let [count, setCount] = useState(0);
let [flag, setFlag] = useState(true);

return (
<div>
<h1>useEffect</h1>
<div>{count}</div>
<button onClick={() => setCount(count + 1)}>点击累加</button>
<button onClick={() => setFlag(!flag)}>显示/隐藏</button>
{flag && <Son></Son>}
</div>
);
}

function Son() {
useEffect(() => {
return () => {
console.log("useEffect.jsx 组件卸载了");
};
}, []);
return (
<div>
<h1>子组件</h1>
</div>
);
}

useLaoutEffect 在视图更新前执行

useLayoutEffectuseEffect 两个方法的作用和使用方式都是一样的,都是用来处理副作用代码的,它们之间唯一的区别就是回调函数的执行时机不同。

qJ9LwV.png

useEffect 在组件视图更新完成后执行,组件状态发生变化 -> 比较状态差异 -> 视图更新 -> useEffect

qJ9qe0.png

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
import { useLayoutEffect, useRef, useState } from "react";

function LayoutEffect() {
const [isShow, setIsShow] = useState(false);
const divRef = useRef<HTMLDivElement>(null);
// 此处如果使用 useEffect 就是出现元素闪烁问题
// 就是元素先出现在原始位置, 再出现在目标位置
// useEffect 是先渲染DOM 再执行
// useLayoutEffect 是先执行, 再渲染DOM
useLayoutEffect(() => {
if (!divRef.current) return;
divRef.current.style.top = "100px";
}, [isShow]);
return (
<>
<h1>useLayoutEffect</h1>
<button onClick={() => setIsShow(!isShow)}>button</button>
{isShow ? (
<div ref={divRef} style={{ position: "relative" }}>
useLayoutEffect
</div>
) : null}
</>
);
}
export default LayoutEffect;

useRef 获取 Dom 元素

  • useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)
  • 返回的 ref 对象在组件的整个生命周期内保持不变
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React, { useRef } from "react";

export default function UseRec() {
const h1Ref = useRef();
const test = () => {
console.dir(h1Ref.current);
h1Ref.current.innerText = "useRef 被修改了";
};
return (
<div>
<h1 ref={h1Ref}>useRef</h1>
<button onClick={test}>获取 h1</button>
</div>
);
}

ref 转发 让父元素可以使用子元素的 ref

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
import React, { forwardRef, useRef } from "react";
const ForwardRef = () => {
return (
<div>
<h1>ForwardRef</h1>
<Father></Father>
</div>
);
};
const Father = () => {
let inputRef = useRef();
const changeInputValue = () => {
inputRef.current.value = "Hello";
};
return (
<div>
<button onClick={changeInputValue}>我是 Father </button>
<Son ref={inputRef}></Son>
</div>
);
};
const Son = forwardRef((props, ref) => {
// let inputRef = useRef();
const changeInputValue = () => {
ref.current.value = "Hello";
};
return (
<div>
<input type="text" ref={ref} />
<button onClick={changeInputValue}>我是 Son </button>
</div>
);
});

export default ForwardRef;

memo 优化组件

父组件更新引起了子组件的不必要更新,因为子组件本身是没有任何变化的是没有必要更新的。

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
// src/App.js
import { useEffect, useState, memo } from "react";

function ShowName({ name }: any) {
useEffect(() => {
console.log("ShowName rendered");
});
return <div>{name}</div>;
}
// memo 方法内部采用的是浅层比较,比较基本数据类型的值是否相同,比较引用类型是否为相同的引用地址。
// memo 方法的第二个参数即为比较函数,可以通过它解决以上问题。比较函数的第一个参数为 prevProps,比较函数的第二个参数为 nextProps, 比较函数返回 true 不进行渲染,比较函数返回 false 组件重新渲染。
function compare(prev: any, cur: any) {
return prev.obj.sex === cur.obj.sex;
}

const MemoShowName = memo(ShowName, compare);

function Memo() {
const [index, setIndex] = useState(0);
const [name] = useState("张三");
useEffect(() => {
const timer = setInterval(() => {
setIndex((prev) => prev + 1);
}, 1000);
return () => clearInterval(timer);
}, []);
return (
<>
<h1>Memo</h1>
<p>{index}</p>
<MemoShowName name={name} obj={{ sex: "" }} />
</>
);
}
export default Memo;

useMemo 对值进行缓存

通过 useMemo 方法可以对组件中的值进行缓存,就是说在每次组件重新渲染时都返回相同的值,也可以指定哪些状态发生改变时重新计算该值。

useMemo 有助于避免在每个渲染上进行昂贵的计算,提升组件性能。

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
import { useEffect, useMemo, useState } from "react";

function App() {
const [number, setNumber] = useState(0);
const [dark, setDark] = useState(false);
const double = useMemo(() => {
return slowFunction(number);
}, [number]);
const styles = useMemo(() => {
return {
background: dark ? "black" : "white",
color: dark ? "white" : "black",
};
}, [dark]);
useEffect(() => {
console.log("styles");
}, [styles]);
return (
<div>
<input
type="number"
value={number}
onChange={(event) => setNumber(event.target.value)}
/>
<div style={styles} onClick={() => setDark(!dark)}>
{double}
</div>
</div>
);
}

function slowFunction(n) {
for (let i = 0; i < 1000000000; i++) {}
return n * 2;
}

export default App;

useCallback 缓存函数

通过 useCallback 方法可以缓存函数,使用组件每次重新渲染都返回相同的函数实例,也可以指定当某个状态发生变化后返回新的函数实例。

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
39
40
41
42
43
import { useCallback, useEffect, useState } from "react";

export default function UseCallBack() {
const [number, setNumber] = useState(1);
const [dark, setDark] = useState(false);
const styles = {
background: dark ? "black" : "white",
color: dark ? "white" : "black",
};
// 缓存
const getItems = useCallback(() => {
return [number, number + 1, number + 2];
}, [number]);
return (
<div style={styles}>
<h1>useCallback</h1>
<input
type="number"
value={number}
onChange={() => setNumber((prev) => prev + 1)}
/>
<button onClick={() => setDark((dark) => !dark)}>button</button>
<List getItems={getItems} />
</div>
);
}

function List({ getItems }) {
const [items, setItems] = useState([]);

useEffect(() => {
setItems(getItems());
console.log("update items");
}, [getItems]);

return (
<div>
{items.map((item) => (
<p key={item}>{item}</p>
))}
</div>
);
}

useImperativeHandle 子向父传值

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
import { useRef, forwardRef, useImperativeHandle, useState } from "react";

let Message = forwardRef((props: any, ref: any) => {
const [text, setText] = useState("");
const inputRef = useRef<HTMLInputElement>(null);
useImperativeHandle(ref, () => {
return {
getText() {
return text;
},
input: inputRef.current,
};
});
return (
<input
ref={inputRef}
type="text"
value={text}
onChange={(event) => setText(event.target.value)}
/>
);
});
export default function UseImperativeHandle() {
const messageRef = useRef<any>(null);
const onClickHandler = () => {
console.log(messageRef.current.getText());
console.log(messageRef.current.input);
};
return (
<>
<h1>UseImperativeHandle</h1>
<Message ref={messageRef}></Message>
<button onClick={onClickHandler}>button</button>
</>
);
}

一些小技巧

手动更新组件

1
2
3
4
import { useCallback, useState } from "react";
let [, updateState] = useState({});
const forceUpdate = useCallback(() => updateState({}), []);
<button onClick={() => forceUpdate}>更新组件</button>;