Skip to content

如何写出更优雅的 React 组件 - 代码结构篇

https://juejin.cn/post/7038810014584143909

创建 React 组件的10条准则

版本稳定

当创建一个 API 的时候,需要考虑的最重要的一件事是尽可能地保持 API 的稳定。 这意味着需要最大限度地减少重大变化的数量。 如果 API 真的有较大的变化,也请确保撰写了详细的升级指南,并尽可能提供一份代码模块,可以让用户自动完成升级过程。

如果正在发布 API,请确保遵循了语义版本规范。这可以让用户轻松地决定所需的版本。

提供错误描述信息

每当调用 API 发生错误时,你需要尽可能地去解释发生了什么问题,以及如何去修复错误。

在没有任何提醒或信息的情况下,直接抛出一个“错误使用”来羞辱调用者貌似不是一种良好的用户体验。

相反,撰写描述性错误信息可以帮助调用者来修改他们调用 API 的方式。

别让程序猿犯嘀咕

程序猿是脆弱的,而且你也不希望他们在使用 API 的时候吓到他们。

也就是说,API 应该尽可能直观。可以通过遵循最佳实践和现有的命名习惯来实现这一点。

另外还需要注意一点:保持代码风格的连贯。

如果在布尔属性的名称前加上了 is 或者 has 作为前缀,但是接下来却又不这么做了,这就会让人感到费解。

精简 API 结构

当我们在讨论做减法的时候,同样也包括减少 API 。 功能多了固然很好,但是 API 的结构越简单,调用者的学习成本就越小。 反过来讲,这会被认为是一个简单易用的 API 。

总有办法来控制 API 的大小,其中的一个办法是,从旧的 API 中重构出一个新的API。

写文档

如果没有文档来记录组件是如何使用的,好吧,虽然大多数开发者会随时查看你的代码,但这不能说是一种良好的用户体验。 无论选择哪个,都请确保在文档中记录了 API 的用法 ,以及组件的用法和使用时机。 后者在共享组件库中尤为重要。

允许上下文语义

HTML 是一种通过语义化的方式来组织信息的语言。 大多数组件是使用 <div />标签来构建的。 这在某种程度上是有道理的——因为通用组件不清楚它到底应该是一个 <article /> 还是 <section /> 或者是 <aside /> ,尽管如此,但只用<div/>来构建也并不完美。

相反,我们建议允许组件接受一个 as 属性,它将始终覆盖正在呈现的DOM 元素。下面是一个实现它的例子:

tsx
function Grid({ as: Element, ...props }) {
  return <Element className="grid" {...props} />;
}

Grid.defaultProps = {
  as: 'div',
};

我们将 as 属性重命名为局部变量 Element,并在 JSX 中使用它。当不需要更多语义化的 HTML 标签时,我们也提供了普通的默认值来传给组件。

当使用 组件的时候,你可以传入合适的标签:

tsx
function App() {
  return (
    <Grid as="main">
      <MoreContent />
    </Grid>
  );
}

请注意上面的代码在 React 组件中同样有效。下面是一个很好的例子,展示了如果想让一个<Button />组件呈现一个 React Router <Link />

text
<Button as={Link} to="/profile">
  Go to Profile
</Button>

使用 props.children

React 中有几个特殊的属性,他们的处理方式与其他属性不太一样。其中一个就是key,用来在有序列表中追踪列表项的,另一个就是children。

在一个开始标签和结束标签之间的任何东西都被放置在props.children属性中,推荐尽量多使用这个属性。

推荐的原因是使用props.children属性比起使用content属性,或者其他只接受类似文本的简单值的属性来说,要简便的多。

text
<TableCell content="Some text" /> // vs <TableCell>Some text</TableCell>

使用 props.children还有几个好处。首先,它的写法和普通的 HTML 是一样的。 第二,你可以向组件传递任何想要的东西,而不是向组件中添加 leftIcon 和 rightIcon 属性,把他们作为 props.children 的一部分传递给组件即可。

text
<TableCell> <ImportantIcon /> Some text </TableCell>

你可能会说,我的组件只会渲染普通的文本,不需要渲染其他东西。 在某些情况下可能是正确的,至少现在是没问题的。而在未来需求变化的时候,你就会发现使用 props.children 的好处。

扩散剩余属性

每当创建一个新的组件的时候,请确保将剩余的属性也扩散到有意义的元素上。

如果有某些属性仅需传递给子组件或子元素而组件自身并不需要这个属性),那就不必添加到你的组件中,这么做可以让组件 API 更加稳定,即使当下一个开发者需要新事件监听器的时候,也无需发布新版本的组件。

例如:

tsx
function ToolTip({ isVisible, ...rest }) {
  return isVisible ? <span role="tooltip" {...rest} /> : null;
}

你的组件可以向底层组件或元素传递属性,比如 className 或者 onClick 的监听函数,一定要确保外部的调用者一样可以这样做。 比如在 class 这种情况中,你可以使用 npm 上的 classname 包来方便地添加 class 属性(或者干脆直接用简单的 string 字符串)。

tsx
import classNames from 'classnames';

function ToolTip(props) {
  return (
    <span
      {...props}
      className={classNames('tooltip', props.tooltip)}
    />
  );
}

在事件监听回调的情况下,可以用一个小工具函数将它们合并成单个函数。

例如:

tsx
function combine(...functions) {
  return (...args) =>
    functions
      .filter(func => typeof func === 'function')
      .forEach(func => func(...args));
}

现在,我们创建了一个以函数数组为参数的函数,它返回一个新的回调函数,该回调函数会向各个函数传入相同的参数,并依次调用各个函数。

示例代码:

tsx
function ToolTip(props) {
  const [isVisible, setVisible] = React.useState(false);
  return (
    <span
      {...props}
      className={classNames('tooltip', props.className)}
      onMouseIn={combine(() => setVisible(true), props.onMouseIn)}
      onMouseOut={combine(() => setVisible(false), props.onMouseOut)}
    />
  );
}

充分提供默认值

请确保为属性提供了充分的默认值,这样做可以最大限度地减少必传值的数量,而且也大大简化了代码实现。

以 onClick 处理函数为例,如果它不是必需的,就可以提供一个空函数来作为默认值。这样,你就可以在代码中随时调用它,就好像组件总是被提供了回调函数一样。

另一个例子是自定义输入。 除非明确提供,否则假设输入的字符串是空字符串。 这将使你确保始终处理字符串对象,而不是 undefined 或 null 。

使用自定义钩子实现逻辑可复用

在使用 React 时,我们经常使用由 react-router-dom、Next.js 或 react-navigation (适用于 React Native) 等库提供的导航钩子。

但是,这些通用的导航钩子缺乏对应用程序中特定页面的了解,这会导致限制。一个有效的解决方案是创建自定义导航钩子,这些钩子了解应用程序中的所有页面,使它们之间的导航更加容易。

以下是创建自定义导航钩子的示例:

ts
import { useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import type { Routes } from '@/types';

export function useRouter() {
  const navigate = useCallback((path: Routes) => goTo(path), [goTo]);

  return { navigate };
}

这种方法可能一开始会让人觉得有点复杂,但从长远来看,它有几个优点。它简化了钩子的调用,并为函数提供了自动完成功能,使代码更简洁易懂。

此外,它简化了维护,因为如果将来需要更改导航库,你只需要更改自定义钩子,而不是更改所有使用导航钩子的地方。

创建自定义钩子的这种思路可以应用于应用程序的其他方面,例如管理 cookie、本地存储 (localStorage)、API 调用等等。

这种方法允许你在项目的多个地方轻松地重用逻辑,从而提高模块化,简化代码维护。

Contributors

作者:Long Mo
字数统计:2.1k 字
阅读时长:6 分钟
Long Mo
文章作者:Long Mo
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Longmo Docs