10.React-组件-组件封装
# 0.前言
本篇博客代码仓库
开一个新坑,希望以示例讲解知识点,talk is cheap,show me code
# 组件:受控与非受控
在 React
中的组件受控与非受控是一个非常基础的概念,但是却是初学者经常容易犯错的地方。
# 问题 1:受控与非受控基础使用
受控组件用法:
/* 受控组件示例用法 */
const ControlInput: React.FC = () => {
/* 注:此时 value 不能为 undefined */
const [value, setValue] = useState("");
const onChange = useCallback((e) => {
console.log("e.target.value", e.target.value);
setValue(e.target.value);
}, []);
return <input value={value} onChange={onChange} />;
};
2
3
4
5
6
7
8
9
10
11
非受控组件用法:
/* 非受控组件使用 */
const UnControlInput: React.FC = () => {
const inputRef = useRef<HTMLInputElement>(null);
const onChange = useCallback((e) => {
console.log("e.target.value", e.target.value);
}, []);
const getInstanceValue = () => {
if (inputRef.current) {
alert(inputRef.current.value);
}
};
return (
<div>
<input ref={inputRef} onChange={onChange} defaultValue={"hello world"} />
<button onClick={() => getInstanceValue()}>获取input中的值</button>
</div>
);
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 问题 2:如何获取非受控组件内部状态?
对于非受控组件,需要通过 ref
绑定 dom
后获取:ref.current.value
/* 非受控组件使用 */
const UnControlInput: React.FC = () => {
const inputRef = useRef<HTMLInputElement>(null);
const getInstanceValue = () => {
if (inputRef.current) {
alert(inputRef.current.value);
}
};
return (
<div>
<input ref={inputRef} />
<button onClick={() => getInstanceValue()}>获取input中的值</button>
</div>
);
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 问题 3:常见初学者犯的 BUG
?
受控 与 非受控 主要有以下区别:
- 非受控模式:
value = "undefined"
+defaultValue = ""
- 受控模式:
value !== "undefined"
开发隐式约束为 value
和 defaultValue
无法同时输入。若同时输入,则会如下报错。
浏览器无法同时处理
value
和defaultValue
属性。
❎ 错误示例代码:
const AllInput: React.FC<any> = (props) => {
const [value, setValue] = useState("");
const onChange = useCallback((e) => {
console.log("e.target.value", e.target.value);
}, []);
return (
<input value={value} onChange={onChange} defaultValue={"hello world"} />
);
};
2
3
4
5
6
7
8
9
10
11
# 问题 4:封装组件库——如何处理内部状态?
组件库的封装和直接功能实践区别还是很大的,组件的类型一般分为两种:
stateless
组件【无状态组件】此类组件在
Class Component
时期,也被称为 函数组件,不存在内部变量,组件的显示仅通过props
传递。function PureFunctionComp(props) { return <div>{props.title}</div>; }
1
2
3组件内部存在状态维护逻辑【受控模式】
function Input(props) { const [value, setValue] = useState(""); return <input onChange={(e) => setValue(e.target.value)} value={value} />; }
1
2
3
4
实际开发过程中,对于第二种封装形式往往会新增一个需求,Input
组件存在两个控制来源:
value
←props.value
发生变化当外部传入
value
,props.value
改变时,input
发生变化【父组件受控】value
←onChange
变化组件内存在
onChange
发生变化时,input
也能改变【子组件内部受控】
此时,传统的做法是将第二种组件封装方式改写为第一种组件封装方式,即 【受控模式】→ 【stateless
组件】,示例如下:
function App(){
const [value, setValue] = useState("");
/* 将内部 val 状态通过回调 callback 获取 */
const onChange = (val) => setValue(val);
return <Input value={value} onChange={onChange}>;
};
function Input(props){
return <input onChange={props.onChange} value={props.value} />
}
2
3
4
5
6
7
8
9
10
综上,当封装组件库给项目组使用时,上面这套改造方案就不合适了,需要做到以下两点:
当没有传递
value
属性时,子组件内部维护一套状态,以及状态的修改机制。【非受控】典型的如
antd
的form
组件,通过formRef.current.getFieldsValue()
来实时获取字段值。当传递
value
属性时,子组件又可切换为受控模式,完全取决于父组件的属性传递。
解决方案
- 使用
innerValue
让<input />
组件完全受控,难点在状态的初始化。 - 处理
innerValue
受控条件。
完整代码:
/* 同时兼容: defaultValue + value + onChange 属性 */
const Input = React.memo((props: any) => {
const {
/* controlled attribute*/
value,
/* uncontrolled attributes */
defaultValue,
onChange,
...rest
} = props;
/**
* 初始化:维护内部 innerValue 状态,input 采用完全受控模式
*/
const [innerValue, setInnerValue] = useState(() => {
if (typeof value !== "undefined") {
return value;
} else {
// 当 value 为 undefined 时,返回 defaultValue 值
return defaultValue;
}
});
const _onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const inputValue = e.target.value;
/* 非受控状态:取决内部组件 onChange 回调得到的值 */
if (typeof value === "undefined") {
setInnerValue(inputValue);
}
onChange && onChange(e);
};
/* 受控状态:取决于外部 props.value 值 */
useEffectUpdate(() => {
setInnerValue(value);
}, [value]);
/* 保证最后的状态始终受控 */
function fixControlledValue<T>(value: T): string {
if (typeof value === "undefined" || value === null) {
return "";
}
return String(value);
}
/* 改造思路:完全采用受控模式 */
return (
<input
value={fixControlledValue(innerValue)}
onChange={_onChange}
{...rest}
/>
);
});
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
# 问题 5:如何让保证组件完全受控?
需保证:value !== undefined
const [innerValue, setInnerValue] = useState(()=>{
.....省略部分逻辑代码
})
/* 保证最后的状态始终受控 */
function fixControlledValue<T>(value: T): string {
if (typeof value === "undefined" || value === null) {
return "";
}
return String(value);
}
<input value={fixControlledValue(innerValue)} />
2
3
4
5
6
7
8
9
10
11
12
13
# 问题 6:如何处理 defaultValue
? 如何对 innerValue
维护?
让
input
组件完全受控使用
innerValue
维护,核心在于初始化。const [innerValue, setInnerValue] = useState(() => { if (typeof value !== "undefined") { return value; } else { // 当 value 为 undefined 时,返回 defaultValue 值 return defaultValue; } });
1
2
3
4
5
6
7
8绑定变量时保证状态始终受控,即
value
值不允许为undefined
/* 保证最后的状态始终受控 */ function fixControlledValue<T>(value: T): string { if (typeof value === "undefined" || value === null) { return ""; } return String(value); }
1
2
3
4
5
6
7如何更新
innerValue
对于 受控模式下 ,需监听
props.value
的变化。useEffectUpdate(() => { setInnerValue(props.value); }, [props.value]);
1
2
3对于 非受控模式下 ,通过
onChange
属性改变const onChange = (e) => { const inputValue = e.target.value; if (typeof value === "undefined") { setInnerValue(inputValue); } };
1
2
3
4
5
6
# 问题 7:使用 useMergeState
自定义钩子
在 rc-util
工具函数中,提供了 useMergeState
自定义钩子,已对上述逻辑进行了抽离和简化。
useMergeState
的使用方式也很简单
原先的直接透传给 input
:<input value={value} defaultValue={defaultValue}>
先透传给 useMergeState
,再绑定在 input
上
const [innerValue, setInnerValue] = useMergedState("defaultValue", {
value,
onChange, /* innerValue 变化时触发 */
defaultValue,
});
<input value={innerValue}>
2
3
4
5
6
7
其中:useMergedState
的第一个参数为 defaultValue
,初始化时,优先级如下:
props.value => props.defaultValue => 自身的 defaultValue(支持传递 null)
完整代码如下:
import { useMergedState } from "rc-util";
/* 同时兼容: defaultValue + value + onChange 属性 */
const Input = React.memo((props: any) => {
const {
/* controlled attribute*/
value,
/* uncontrolled attribute */
defaultValue,
onChange,
...rest
} = props;
/* 使用自定义钩子 useMergeState */
const [innerValue, setInnerValue] = useMergedState(null, {
value,
onChange,
defaultValue,
});
/* 更新内部变量 */
const _onChange = (e) => {
setInnerValue(e.target.value);
};
return <input value={innerValue} onChange={_onChange} />;
});
export default () => {
const [value, setValue] = useState("");
return (
<Input
value={value}
onChange={(e) => {
console.log("非受控内部状态", e);
}}
defaultValue={"hello world"}
/>
);
};
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
# Stale-closure 陈旧闭包示例
js
闭包示例:
❎ 错误案例 1:setTimeout
/* 陈旧闭包 */
for (var i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i);
});
}
/* 解决方案1: var → let */
for (let i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i);
});
}
/* 解决方案2: 立即执行函数包裹 */
for (var i = 0; i < 5; i++) {
(() => {
setTimeout(() => {
console.log(i);
});
})(i);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
示例案例 2:
function outerTest() {
var num = 0; // 闭包变量(内存变量)
function innetTest() {
++num;
console.log(num); // num 在函数定义时就已经决定了
}
return innetTest;
}
const fn1 = outerTest(); // 闭包1
const fn2 = outerTest(); // 闭包2
fn1(); // 1
fn1(); // 2
fn1(); // 3
fn2(); // 1
fn2(); // 2
fn2(); // 3
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
特性:
- 一个闭包内对变量的修改,不会影响到另外一个闭包中的变量
- 在内存中保持变量数据一直不丢失
具体见另一篇博客:《函数闭包与 this 指针》
React
闭包示例:
❎ 错误案例1
:在 setTimeout
或者 setInterval
中引用外部 state
import React, { useState, useEffect } from "react";
function Counter() {
// 闭包变量
const [count, setCount] = useState(0);
useEffect(() => {
const intervalId = setInterval(() => {
console.log(count);
setCount(count + 1); // count 永远为 0
}, 1000);
return () => {
clearInterval(intervalId);
};
}, []);
return <div>{count}</div>;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
❎ 错误案例2
:react
点击按钮自增三次
import { useState } from "react";
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button
onClick={() => {
setNumber(number + 1); // number 永远为 0
setNumber(number + 1);
setNumber(number + 1);
}}
>
+3
</button>
</>
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
典型官网示例:https://zh-hans.react.dev/learn/state-as-a-snapshot
官方解释:
state as a snapshot
jsx
生成的时候,每次相当于生成一张快照。