如何写好一个交互组件

M1cPDldO7
几个月前刷微博,看到老赵的一条,感同身受。

老赵也给出了评分标准,满分10分:

  1. 基本实现无误(4分)
  2. 显示 loading 字样(1分)
  3. 合理错误捕捉(1分)
  4. 知道在切换选择后取消未完成的请求(2分)
  5. 做 debounce 或 trottle(1分)
  6. 处理好各种情况之间的时序问题(1分)

基本实现无误比较笼统,具体还有几点要考虑

  1. 初始值
  2. 选中状态

小白或者初级工程师基本上能写出来下拉、发请求、展示请求结果,会忽略 选中状态、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 的话,也要做相应处理。

想要进一步抽象为通用组件,还需要将

  1. option 列表
  2. 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}
  </>;
};

希望本文能帮助你写出更完整的交互组件。