如何写好一个交互组件
2022-11-29
几个月前刷微博,看到老赵的一条,感同身受。
老赵也给出了评分标准,满分10分:
- 基本实现无误(4分)
- 显示 loading 字样(1分)
- 合理错误捕捉(1分)
- 知道在切换选择后取消未完成的请求(2分)
- 做 debounce 或 trottle(1分)
- 处理好各种情况之间的时序问题(1分)
基本实现无误比较笼统,具体还有几点要考虑
- 初始值
- 选中状态
小白或者初级工程师基本上能写出来下拉、发请求、展示请求结果,会忽略 选中状态、loading、取消请求等。
中级工程师大多也会遗漏取消请求、debounce、时序问题。
评分标准其实就对应了一次完整交互动作的 n 个状态,实现一个好的交互组件,我们需要仔细想一下交互过程中的状态,才能完整的处理每个过程。
以题目来说,这个组件是
// Component.tsx
const Component: React.FC = () => {
const [userInfo, setUserInfo] = React.useState('无数据');
return <>
<select>
{new Array(10).fill(1).map((, index) => {
return <option
key={index} // 很多人也容易忘记设置 key
value={`${index}`}>
{index}
</option>
})}
</select>
{userInfo}
</>;
};
接下来我们往组件上添加状态
加初始值
我们需要定义好当前的选中值,才能依此来发请求、标记选中状态、取消其它发出的请求。
注意 html option tag 的 value 只能是字符串。
// Component.tsx
const Component: React.FC = () => {
const [userInfo, setUserInfo] = React.useState('无数据');
+ const [value, setValue] = React.useState<string>();
+ const onChange = event => {
+ setValue(event.target.value);
+ };
+
return <>
- <select>
+ <select onChange={onChange}>
加 disabled、loading 状态、选中状态
disabled 状态就是 loading=true 的情况。
// Component.tsx
const Component: React.FC = () => {
+ const [loading, setLoading] = React.useState(false);
const [userInfo, setUserInfo] = React.useState('无数据');
const [value, setValue] = React.useState<string>();
const onChange = event => {
setValue(event.target.value);
};
return <>
选中状态就是当前值等于 option value,当前值是string,index是number,判断相等要做类型转换。
return <option
key={index} // 很多人也容易忘记设置 key
+ selected={+value === index}
value={`${index}`}>
接下来就是发请求了,要注意 loading、abort 状态
// Component.tsx
const Component: React.FC = () => {
const [loading, setLoading] = React.useState(false);
const [userInfo, setUserInfo] = React.useState('无数据');
const [value, setValue] = React.useState<string>();
const onChange = event => {
setValue(event.target.value);
};
+ const doFetch = (userId: string) => {
+ const abort = new AbortController();
+ setLoading(true);
+ // 传递 abort 信号
+ fetch(`url?userId=${userId}`, { signal: abort.signal })
+ .then(res => {
+ setUserInfo(res.data);
+ })
+ .catch(err => {
+ setUserInfo(err.message);
+ })
+ .finally(() => {
+ setLoading(false);
+ });
+ // 最后将 abort 控制器返回出去,以便取消请求
+ return abort;
+ };
+
+ // 使用 useEffect 监听 value 变化发送请求
+ // 同时取消上一次正在发送的请求
+ React.useEffect(() => {
+ const abort = value && doFetch(value);
+ return () => {
+ // 若请求很迅速,abort 不会做任何事
+ if (typeof abort === 'function') abort();
+ };
+ }, [value]);
return <>
<select onChange={onChange}>
再给展示内容加上 loading 信息
</select>
- {userInfo}
+ {loading ? '正在加载...' : userInfo}
</>;
至此这个定制组件就很完整了,其中忽略了 userInfo 的类型,当它是 string,若实际情况是 object 的话,也要做相应处理。
想要进一步抽象为通用组件,还需要将
- option 列表
- fetch url 甚至是 doFetch function
等作为 props 传递给组件,还要考虑 option 的内容溢出、title 等。
完整代码
// Component.tsx
const Component: React.FC = () => {
const [loading, setLoading] = React.useState(false);
const [userInfo, setUserInfo] = React.useState('无数据');
const [value, setValue] = React.useState<string>();
const onChange = event => {
setValue(event.target.value);
};
const doFetch = (userId: string) => {
const abort = new AbortController();
setLoading(true);
// 传递 abort 信号
fetch(`url?userId=${userId}`, { signal: abort.signal })
.then(res => {
setUserInfo(res.data);
})
.catch(err => {
setUserInfo(err.message);
})
.finally(() => {
setLoading(false);
});
// 最后将 abort 控制器返回出去,以便取消请求
return abort;
};
// 使用 useEffect 监听 value 变化发送请求
// 同时取消上一次正在发送的请求
React.useEffect(() => {
const abort = value && doFetch(value);
return () => {
// 若请求很迅速,abort 不会做任何事
if (typeof abort === 'function') abort();
};
}, [value]);
return <>
<select>
{new Array(10).fill(1).map((, index) => {
return <option
key={index} // 很多人也容易忘记设置 key
selected={+value === index}
value={`${index}`}>
{index}
</option>
})}
</select>
{loading ? '正在加载...' : userInfo}
</>;
};
希望本文能帮助你写出更完整的交互组件。