原文链接:Separation of concerns with React hooks 原文作者:Felix Gerschau

如果你已经使用了 React 一段时间了,你可能听说过 containerpresentational components,或者 smart components 和 dumb components。刚提到的这几个东西的目的是实现一种模式——将 React 组件的逻辑与 UI 分层。

译者注: 什么是 containerpresentational components ? 出自 dan 的博客——Presentational and Container Components

将 UI 和 业务逻辑分离并不是 React 特有的方式:关注点分离(separation of concerns)在 70 年代就已经成为了一个设计准则。比如说,将数据库和后端业务代码分离就是一个很好的例子。

因此在 React 中,为了实现这个准则,我们可以通过创建 container components 用来处理所有的逻辑,presentational component 将通过 props 的方式获取 container components 传递下来的数据。

随着 React Hooks 的出现,我们有了一种新的方法实现上述准则——使用自定义 hooks。

如何使用 React Hooks 对逻辑解耦

为了使逻辑从我们的组件中抽离出来,我们首先要创建一个自定义的 hook。

让我们用一个组件作为例子。该组件将通过基数(base number)和幂(exponent),最终计算出该指数的值。

点击查看【codepen】

代码如下所示:

  1. export const ExponentCalculator = () => {
  2. const [base, setBase] = useState(4);
  3. const [exponent, setExponent] = useState(4);
  4. const result = (base ** exponent).toFixed(2);
  5. const handleBaseChange = (e) => {
  6. e.preventDefault();
  7. setBase(e.target.value);
  8. };
  9. const handleExponentChange = (e) => {
  10. e.preventDefault();
  11. setExponent(e.target.value);
  12. };
  13. return (
  14. <div className="blue-wrapper">
  15. <input
  16. type="number"
  17. className="base"
  18. onChange={handleBaseChange}
  19. placeholder="Base"
  20. value={base}
  21. />
  22. <input
  23. type="number"
  24. className="exponent"
  25. onChange={handleExponentChange}
  26. placeholder="Exp."
  27. value={exponent}
  28. />
  29. <h1 className="result">{result}</h1>
  30. </div>
  31. );
  32. };

这代码现在看来好像还行,但是为了学习这篇教程,你可以想象一下这里有许多的逻辑。

第一步,我们将会把逻辑代码移动到自定义 Hook 当中并且在我们的组件内部调用它。

  1. const useExponentCalculator = () => {
  2. const [base, setBase] = useState(4);
  3. const [exponent, setExponent] = useState(4);
  4. const result = (base ** exponent).toFixed(2);
  5. const handleBaseChange = (e) => {
  6. e.preventDefault();
  7. setBase(e.target.value);
  8. };
  9. const handleExponentChange = (e) => {
  10. e.preventDefault();
  11. setExponent(e.target.value);
  12. };
  13. return {
  14. base,
  15. exponent,
  16. result,
  17. handleBaseChange,
  18. handleExponentChange,
  19. };
  20. };
  21. export const ExponentCalculator = () => {
  22. const {
  23. base,
  24. exponent,
  25. result,
  26. handleExponentChange,
  27. handleBaseChange,
  28. } = useExponentCalculator();
  29. // ...
  30. };

我们甚至还可以将这个 hook 封装为一个单独的文件用以凸显关注点分离这个设计原则。

除此之外,我们可以更进一步,将我们自定义的 hook 分离成更小,可复用的函数。在下面的例子中,我们可以抽离出 calculateExponent

  1. // useExponentCalculator.js
  2. const calculateExponent = (base, exponent) => base ** exponent;
  3. const useExponentCalculator = () => {
  4. const [base, setBase] = useState(4);
  5. const [exponent, setExponent] = useState(4);
  6. const result = calculateExponent(base, exponent).toFixed(2);
  7. // ...
  8. };

测试这样的函数相较于测试第一个例子中的整个组件简单的多。我们可以使用任何 Node.js 测试库进行测试,甚至不需要这个库支持 React 组件。

现在,我们在组件和 hook 中的代码是特定于框架(framework-specific)的代码。而我们的业务逻辑代码存放在了我们之后定义的其它函数中(与框架无关)。

最佳实践

命名

我喜欢使用 use + 组件名称的方式来给自定义 hooks 取名(e.g. useExponentCalculator)。并且创建一个同名的文件,用以调用 hook。

你可能需要遵守不同的命名规范,不过我推荐你与你的项目命名风格保持一致。

如果这个自定义的 hook 会被复用,我通常会将它封装成一个单独的文件,并且移动到 src/hooks 路径下。

不要上头

我们需要务实一点。如果一个组件只有短短几行 JS,就没有必要抽离出逻辑。

CSS-in-JS

如果你使用 CSS-in-JS 的库(useStyles),你可能还想将这段代码移动到其它的文件中。

你可以将其移动到与 hook 相同的文件中。但是我更喜欢将其保留在同一文件中组件代码的上面。或者如果它变的太大,将其移动到单独的文件中。

总结

不论你是否认为使用自定义 hooks 可以改进你的代码,最终这都归结于个人喜好。如果你的代码并没有很多的逻辑,这种模式的优势对你的意义确实不大。

自定义 hooks 是提高我们代码模块化的唯一方法;如果可能的话,我还是强烈推荐你将组件和函数拆分为更小的,可重用的块。

该主题还在 The Pragmatic Programmer 中有着更为普遍的讨论。我写了一篇文章涵盖了我对于这本书最喜欢的章节,如果你感兴趣的话,可以看看这里

链接

下面是我在研究本文的时候发现的一些有用的链接: