如何使用 Hooks 编写出更简洁 90% 的代码

2025-05-24

如何使用 Hooks 编写出更简洁 90% 的代码

2018 年,React 生态系统迎来了许多新功能。这些功能的加入有助于开发人员将更多精力放在用户体验上,而不是花时间编写代码逻辑。

看起来 React 正在向函数式编程范式投入更多,以寻找构建更强大、更具可扩展性的 UI 的优秀工具。

在 2018 年 10 月的ReactConf大会上,React 发布了一项名为 Hooks 的提案 API,这引起了社区的轰动。开发者们开始探索和实验 Hooks,并在RFC(征求意见稿)中获得了积极的反馈。React 16.8.0 是第一个支持 Hooks 的版本 🎉。

本文我试图解释:

  • 为什么要引入 Hooks

  • 我们如何为这个 API 做好准备

  • 如何使用 React Hooks 编写出 90% 更简洁的代码?

如果您只是想先体验一下这个新的 API,我已经创建了一个demo供您使用。否则,我们先来看看目前面临的三个主要问题:

1. 重用代码逻辑

大家都知道,复用代码逻辑很难,需要相当多的经验才能理解。大约两年前,我刚开始学习 React 时,习惯于创建类组件来封装所有逻辑。当需要在不同的组件之间共享逻辑时,我会创建一个外观相似的组件,渲染不同的 UI。但这样做并不好。我违反了DRY原则,理想情况下,并没有复用逻辑。

旧方法

慢慢地,我了解了HOC模式,它允许我使用函数式编程来复用我的代码逻辑。HOC 只不过是一个简单的高阶函数,它接受另一个组件(dumb)作为参数,并返回一个新的增强组件。这个增强组件将封装你的逻辑。

高阶函数是将函数作为参数或返回函数的函数。

export default function HOC(WrappedComponent){
  return class EnhancedComponent extends Component {
   /*
     Encapsulate your logic here...
   */

    // render the UI using Wrapped Component
    render(){
      return <WrappedComponent {...this.props} {...this.state} />
    }
  }

  // You have to statically create your
  // new Enchanced component before using it
  const EnhancedComponent = HOC(someDumbComponent);

  // And then use it as Normal component
  <EnhancedComponent />
Enter fullscreen mode Exit fullscreen mode

随后,我们开始关注将函数作为 props 传递的趋势,这标志着渲染 props模式的兴起。渲染 props 是一种强大的模式,其中“渲染控制器”掌握在自己手中。这促进了控制反转 (IoC)设计原则的实现。React 文档将其描述为一种在组件之间共享代码的技术,使用一个值为函数的props


具有渲染属性的组件采用返回React 元素的函数并调用它,而不是实现自己的渲染逻辑。

简单来说,您创建一个组件来封装您的逻辑(副作用),当涉及到渲染时,该组件只需传递渲染 UI 所需的数据即可调用您的函数。

export default class RenderProps extends Component {
/*
  Encapsulate your logic here...
*/

  render(){
    // call the functional props by passing the data required to render UI
    return this.props.render(this.state);
  }
 }

// Use it to draw whatever UI you want. Control is in your hand (IoC)
<RenderProps render={data => <SomeUI {...data} /> } />
Enter fullscreen mode Exit fullscreen mode

尽管这两种模式都解决了重用代码逻辑问题,但它们给我们留下了包装器地狱问题,如下所示:

替代文本

因此,总而言之,我们可以看到重用代码逻辑存在一些问题:

  • 实现起来不太直观
  • 大量代码
  • 包装地狱

2. 巨型组件

组件是 React 中代码复用的基本单位。当我们必须将多个行为抽象到类组件中时,它的大小往往会变得庞大,难以维护。

一个类应该有且仅有一个改变的原因,
这意味着一个类应该只有一项工作。

通过查看下面的代码示例,我们可以推断出以下内容:

export default class GiantComponent extends Component {
  componentDidMount(){
    //side effects
    this.makeRequest();
    document.addEventListener('...');
    this.timerId = startTimer();
    // more ...
  }

  componentdidUpdate(prevProps){
   // extra logic here
  }

  componentWillUnmount(){
    // clear all the side effects
    clearInterval(this.timerId);
    document.removeEventListener('...');
    this.cancelRequest();
  }
  render(){ return <UI />; }
Enter fullscreen mode Exit fullscreen mode
  • 代码分布在不同的生命周期钩子上
  • 没有单一的责任
  • 难以测试

3. 课程对于人类和机器来说都很难

从人的角度看这个问题,我们都曾经尝试调用子组件内的函数,结果显示:

TypeError: Cannot read property 'setState' of undefined
Enter fullscreen mode Exit fullscreen mode

然后我们绞尽脑汁想找出原因:忘记在构造函数中绑定它了。所以,即使是一些经验丰富的开发人员,仍然是一个令人困惑的问题。

获取调用该函数的对象的值

此外,您还需要编写大量样板代码才能开始实现第一个副作用:

extends -> state -> componentDidMount -> componentWillUnmount -> render -> return
Enter fullscreen mode Exit fullscreen mode

由于以下原因,分类对于机器来说也很难:

  • 缩小版本不会缩小方法名称
  • 未使用的方法不会被删除
  • 热重载和编译器优化困难

我们上面讨论的所有三个问题并不是三个不同的问题,而是一个问题的症状,那就是 React 没有比类组件更简单的状态原语。


随着新的 React Hooks 提案 API 的出现,我们可以通过将逻辑完全抽象到组件之外来解决这个问题。简而言之,你可以将状态逻辑 hook 到函数式组件中。

React Hooks 允许您无需编写类即可使用状态和其他 React 功能。

让我们在下面的代码示例中看看:

import React, { useState } from 'react';

export default function MouseTracker() {

  // useState accepts initial state and you can use multiple useState call

  const [mouseX, setMouseX] = useState(25);
  const [mouseY, setMouseY] = useState(25);

  return (
    <div>
      mouseX: {mouseX}, mouseY: {mouseY}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

调用useState hook 会返回一对值:当前状态和一个更新它的函数。在我们的例子中,当前状态值是mouseX,设置函数是setMouseX。如果将参数传递给 useState ,该参数将成为组件的初始状态。

现在的问题是,我们应该在哪里调用 setMouseX。在 useState 钩子下面调用它会引发错误。这与在类组件的render函数中调用this.setState是一样的。

因此,答案是 React 还提供了一个名为useEffect的占位符钩子来执行所有副作用。

import React, { useState } from 'react';

export default function MouseTracker() {

  // useState accepts initial state and you can use multiple useState call
  const [mouseX, setMouseX] = useState(25);
  const [mouseY, setMouseY] = useState(25);

  function handler(event) {
    const { clientX, clientY } = event;
    setMouseX(clientX);
    setMouseY(clientY);
  }
  useEffect(() => {
    // side effect
    window.addEventListener('mousemove', handler);

    // Every effect may return a function that cleans up after it
    return () => window.removeEventListener('mousemove', handler);
  }, []);

  return (
    <div>
      mouseX: {mouseX}, mouseY: {mouseY}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

此效果将在首次渲染后和每次更新后调用。您还可以返回一个可选函数,该函数将成为清理机制。这使我们能够将添加和删除订阅的逻辑保持在彼此接近的位置。

useEffect 调用的第二个参数是一个可选数组。只有当数组中的元素值发生变化时,你的 effect 才会重新运行。可以将其视为shouldComponentUpdate 的工作原理。如果你希望只运行一次 effect 并清理它(在挂载和卸载时),你可以传递一个空数组([]) 作为第二个参数。这会告诉 React,你的 effect 不依赖于任何 props 或 state 的值,因此它永远不需要重新运行。这类似于我们熟悉的componentDidMountcomponentWillUnmount的思维模型。如果你想深入了解useEffect hook,我在这里写了另一篇文章

但是我们的MouseTracker组件不是还保留着内部逻辑吗?如果另一个组件也想共享mousemove行为怎么办?此外,再添加一个效果(例如调整窗口大小)会使其管理起来有点困难,我们又回到了类组件中遇到的问题。

现在,真正的魔力在于,你可以在函数组件之外创建自定义钩子。这类似于将逻辑抽象到一个单独的模块,并在不同的组件之间共享。让我们看看实际效果。

// you can write your custom hooks in this file
import { useState, useEffect } from 'react';

export function useMouseLocation() {
  const [mouseX, setMouseX] = useState(25);
  const [mouseY, setMouseY] = useState(25);

  function handler(event) {
    const { clientX, clientY } = event;
    setMouseX(clientX);
    setMouseY(clientY);
  }
  useEffect(() => {
    window.addEventListener('mousemove', handler);

    return () => window.removeEventListener('mousemove', handler);
  }, []);

  return [mouseX, mouseY];
}
Enter fullscreen mode Exit fullscreen mode

现在我们可以将 MouseTracker 组件代码(90%)清理为较新的版本,如下所示:

import React from 'react';
import { useMouseLocation } from 'customHooks.js';

export default function MouseTracker() {

  // using our custom hook
 const [mouseX, mouseY] = useMouseLocation();

  return (
    <div>
      mouseX: {mouseX}, mouseY: {mouseY}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

这真是一个“尤里卡”的时刻!不是吗?

但在安顿下来并赞美 React Hooks 之前,让我们看看我们应该注意哪些规则。

Hooks 规则

  • 只调用顶层的钩子
  • 不能在类组件中使用钩子

解释这些规则超出了本文的范围。如果你感兴趣,我推荐你阅读 React文档Rudi Yardley 的这篇文章。

React 还发布了一个名为eslint-plugin-react-hooks的 ESLint 插件,用于强制执行这两条规则。你可以运行以下命令将其添加到你的项目中:

# npm 
npm install eslint-plugin-react-hooks --save-dev

# yarn 
yarn add eslint-plugin-react-hooks --dev
Enter fullscreen mode Exit fullscreen mode

本文是我在 2018 年 12 月 ReactSydney 聚会上的演讲的一部分。希望这篇文章能激发你尝试 React Hooks 的兴趣。我对 React路线图感到非常兴奋,它看起来前景光明,并有可能改变我们目前使用 React 的方式。

您可以在此链接找到源代码和演示

如果你喜欢这篇文章,给我点几声❤️肯定能让我开心起来😀。接下来还有更多。

文章来源:https://dev.to/aman_singh/how-to-write-90-cleaner-code-with-hooks-1mmj
PREV
你认为你了解 JavaScript 吗?ToPrimitive
NEXT
精选的新闻通讯列表,提升您的编码技能