react hooks 详解

Snipaste_2021-11-29_23-27-35

官方文档


useState

使用状态:
  • const[n, setN] = React.useState(0)
  • const[user, setUser] = React.useState({name:'frank})
注意事项1:不可局部更新

如果 state 是一个对象,能否只更新部分?

答案是不行,示例代码

因为 setState 不会帮我们合并属性。

注意事项2:对象地址要变

setState (obj) 如果 obj 地址不变,即使数据变了,那么 React 认为 数据没有变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import React, {useState} from "react";
import ReactDOM from "react-dom";
function App() {
const [user,setUser] = useState({name:'Frank', age: 18})
const onClick = ()=>{
// 这么修改没有用
user.name = 'java'
setUser(user)
// 正确y
setUser({
...user,
name: 'java'
})
}
return (
<div className="App">
<h1>{user.name}</h1>
<h2>{user.age}</h2>
<button onClick={onClick}>Click</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

useState 接收函数

1
2
const [state, setState] = useState(()=>{return initialState})
该函数返回初始 state, 且只执行一次

setState 接收函数

1
setN(i => i + 1)

优先使用函数,demo

自定义 useState


useReducer

用来践行 Flux/Redux 的思想

共四步:

  1. 创建初始值 initialState
  2. 创建所有操作 reduce(state, actioin)
  3. 传给 useReducer, 得到读和写 API
  4. 调用写 ({type: ‘操作类型’})

总的来说 useReducer 是 useState 的复杂版。

demo

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
// 创建初始值
const initial = {
n: 0
};
// 创建所有操作
const reducer = (state, action) => {
if (action.type === "add") {
return { n: state.n + action.number };
} else if (action.type === "multi") {
return { n: state.n * 2 };
} else {
throw new Error("unknown type");
}
};
function App() {
// useReducer
const [state, dispatch] = useReducer(reducer, initial);
const { n } = state;
const onClick = () => {
// 调用 写 ({type: ‘操作类型’})
dispatch({ type: "add", number: 1 });
};
const onClick2 = () => {
dispatch({ type: "add", number: 2 });
};
return (
<div className="App">
<h1>n: {n}</h1>

<button onClick={onClick}>+1</button>
<button onClick={onClick2}>+2</button>
</div>
);
}

useReducer 表单例子

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
import React, { useReducer } from "react";
import ReactDOM from "react-dom";

const initFormData = {
name: "",
age: 18,
nationality: "汉族",
};

const reducer = (state, action) => {
switch (action.type) {
case "patch":
return { ...state, ...action.formData };
case "reset":
return initFormData;
default:
throw new Error();
}
};

const App = () => {
console.log("aaa");
const [formData, dispatch] = useReducer(reducer, initFormData);
const onSubmit = () => {};
const onReset = () => {
dispatch({ type: "reset" });
};
return (
<form onSubmit={onSubmit} onReset={onReset}>
<div>
<label>
姓名
<input
value={formData.name}
onChange={(e) =>
dispatch({
type: "patch",
formData: { name: e.target.value },
})
}
/>
</label>
</div>
<div>
<label>
年龄
<input
value={formData.age}
onChange={(e) =>
dispatch({
type: "reset",
formData: { age: e.target.name },
})
}
/>
</label>
</div>
<div>
<label>
民族
<input
value={formData.nationality}
onChange={(e) =>
dispatch({
type: "patch",
formData: { nationality: e.target.value },
})
}
/>
</label>
</div>
<div>
<button type="submit">提交</button>
<button type="reset">重置</button>
</div>
<hr />
{JSON.stringify(formData)}
</form>
);
};

ReactDOM.render(<App />, document.getElementById("root"));


如何代替 Redux

步骤:

  1. 将数据集中在一个 store 对象
  2. 将所有操作集中在 reducer
  3. 创建一个 Context
  4. 创建对数据的读写 API
  5. 将第四步的内容放到第三步的 Context
  6. 用 Context.Provider 将 Context 提供给所有组件
  7. 各个组件用 useContext 获取读写 API

demo ,模块化后 demo


useContext

上下文

全局变量是全局的上下文

上下文是局部的全局变量

使用:

  1. 使用 C = createContext(initial) 创建上下文
  2. 使用 <C.provider> 圈定作用域
  3. 在作用域内使用 useContext(C) 来使用上下文

demo

注意:

useContext 不是响应式的。

在一个模块将 C 里的值改变,另一个模块不会感知到这个变化。


useEffect

副作用

  • 对环境的改变即为副作用,如修改 document.title
  • 但我们不一定非要把副作用放在 useEffect 里
  • 实际上叫做 afterRender 更好,因为每次 render 后会执行

用途:

  • 作为 componentDidMount 使用, [] 作第二个参数
  • 作为 componentDidUpdate 使用,可指定依赖
  • 作为 componentWillUnmount 使用,通过 return
  • 以上三种用途可同时存在

特点:如果同时存在多个 useEffect,会按照出现次序执行。

demo

useLayoutEffect

布局副作用

useEffect 在浏览器渲染完成后执行,demo

useLayoutEffect 在浏览器渲染前执行,通过时间点来侧面证明

特点:

useLayoutEffect 总是比 useEffect 先执行,demo

useLayoutEffect 里的任务最好影响了 Layout,不然没必要用 useLayoutEffect。

经验:

为了用户体验,优先使用 useEffect(优先渲染)


useMemo

要理解 React.useMemo,需要先了解 React.memo,React 默认有多余的 render,demo,代码中的 Child 用 React.memo(Child)代替,如果 props 不变,就不会再次执行函数组件。

useMemo 的作用就是提供了一个 memorize 值,在依赖项改变之后,该值才会改变才会被重新计算。demo

特点:

  • 第一个参数是 ()=>value

  • 第二个参数是依赖 [m, n]

只有当依赖变化时,才会计算出新的 value,如果依赖不变,那么就重用之前的 value。类似于 vue2 的 computed。

注意:如果 value 是个函数,那么就要写成 useMemo( ()=> (x)=>console.log('1') ),这是一个返回函数的函数,

于是就有了 useCallback,useCallback( (x)=>console.log('1') )


useCallback

用法:

1
2
3
useCallback(x => console.log(x), [m])
等价于
useMemo( () => x=>console.log(x), [m])

useRef

目的:

如果你需要一个值,在组件不断 render 时保持不变

初始化:const count = useRef(0)code

读取:count.current

为什么需要 current ?

为了保证两次 useRef 是同一个值(只有引用能做到)

useRef 不会自动更新 UI。

拓展:

vue3 的 ref,

初始化:const count = ref(0)

读取:count.value

不同点:Vue3 会自动 render。

forwardRef

demo1:props 无法传递 ref 属性

demo2:实现 ref 的传递

demo3:两次 ref 传递得到 button 的引用

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
const App = () => {
const buttonRef = useRef(null);
console.log("buttonRef", buttonRef);
return (
<div>
<h1>React.forwardRef</h1>
<hr />
<div>
<Button3 ref={buttonRef}>button</Button3>
</div>
</div>
);
};
const Button2 = (props, ref) => {
console.log(props);
console.log(ref);
return <button className="red" ref={ref} {...props} />;
};
// 实现 ref 的传递
const Button3 = forwardRef(Button2);

// const Button4 = forwardRef((props, ref) => {
// console.log(props);
// console.log(ref);
// return <button className="red" ref={ref} {...props} />;
// });

useRef:

  • 可以用来引用 DOM 对象
  • 也可以用来引用 普通对象

forwardRef:

  • 由于 props 不包含 ref,所以需要 forwardRef
  • 为生命 props 不包含 ref ?以为大部分时候不需要。

useImperativeHandle

用于自定义 ref 的属性。推荐阅读

demo1 使用 useImperativeHandle

demo2 不使用 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
37
38
// 子组件
const ChildComponent = forwardRef((props, ref) => {
const [count, setCount] = useState(0); //子组件定义内部变量count
//子组件定义内部函数 addCount
const addCount = () => {
setCount(count + 1);
};
//子组件通过useImperativeHandle函数,将addCount函数添加到父组件中的ref.current中
useImperativeHandle(ref, () => ({ addCount }));
return (
<div>
{count}
<button onClick={addCount}>child</button>
</div>
);
});

// 父组件
function Imperative() {
const childRef = useRef(null); //父组件定义一个对子组件的引用

const clickHandle = () => {
childRef.current.addCount(); //父组件调用子组件内部 addCount函数
};

return (
<div>
{/* 父组件通过给子组件添加 ref 属性,将childRef传递给子组件,
子组件获得该引用即可将内部函数添加到childRef中 */}
<ChildComponent ref={childRef} />
<button onClick={clickHandle}>child component do something</button>
</div>
);
}

function App() {
return <Imperative />;
}

自定义 Hook

封装数据操作

简单例子,贴心例子

还可以在自定义 Hook 里使用 Context

useState 只说了不能在 if 里,没说不能在函数里运行,只要这个函数在函数组件里运行即可。

Stale Closure

过时闭包

参考文章链接


Snipaste_2021-11-29_23-27-35