logo

从面向对象思维理解 React 组件与 Hook

2025年10月29日 · 2450 字

如果你刚开始接触 React,很可能会对 React 的组件和 Hook 的用法产生一些困惑。在 React 的世界里,就像是随意丢着一些零零散散的工具,然后告诉你去尝试把这些工具拼接成完整的页面和应用。一不注意,就可能出现状态和函数满天飞的混乱局面。

作为一个后端开发出身的程序员,最近我开始尝试用 React 搭建有一定规模的复杂应用。在开发的过程中,我逐渐发现:理解 React 的关键,也许在于理解「自定义 Hook」的作用。而这个东西,是可以从面向对象的思想来理解的。

如果你和我一样有后端开发的背景,但是对 React 里的东西感到迷惑,那么这篇文章可以给你一些思路上的启发。

组件与 Hook 的关系

在 React 中,组件与 Hook 是什么关系?我们看到的理解一般都是:

组件负责视图(UI 展示和交互),Hook 负责逻辑(状态和操作)。

这个说法有一定道理,我们来看一个最简单的计数器例子:

// useCounter.js (自定义 Hook)
function useCounter() {
  const [count, setCount] = useState(0);
  
  const increment = () => setCount(count + 1);
  const decrement = () => setCount(count - 1);
  
  return { count, increment, decrement };
}

// Counter.js (组件)
function Counter() {
  const { count, increment, decrement } = useCounter();
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
      <button onClick={decrement}>Decrement</button>
    </div>
  );
}

在这个例子中,useCounter 封装了所有状态(count)和操作(increment, decrement),而 Counter 组件只负责使用它们来渲染 UI。这看起来完美印证了「组件管理视图,Hook 管理逻辑」的说法。

但是,我们可以更进一步直击本质:难道组件里只有视图吗?

虽然逻辑都放在了 useCounter 这个 hook 里,但是组件里使用了这个 hook。换句话说,组件其实通过管理 hook,间接地管理了逻辑。

这个观点可以从另一个角度印证:我们完全可以把 useCounter 里的所有代码,直接写在 Counter 组件内部,功能依然是正常的。

没错,组件其实是封装了 UI 和逻辑的独立构件。

这其实符合 React 等现代前端框架的思路:不是 HTML、CSS、JavaScript 分离,而是纵向切分,一个组件是一个独立的业务模块,具备 UI 骨架(HTML)、展示(CSS)、交互(JavaScript)的能力。

在实际写代码的时候,我们是根据实际情况,决定要不要写自定义 Hook 的:

  • 如果组件里的逻辑很简单,只有一两个 state 和一些操作,完全可以直接写在组件内部。
  • 当逻辑变得复杂时,或者需要复用业务逻辑时,我们便把逻辑抽取到自定义 Hook 中。这样组件就把逻辑的管理托管给 Hook,自己则专注于组装视图。

总结下来,用一句话概括就是:组件同时管理视图和逻辑,在逻辑复杂的情况下,我们把逻辑抽取到自定义 Hook 中,让 Hook 管理逻辑,组件通过 Hook 间接使用逻辑。

把自定义 Hook 看成「类」

既然自定义 Hook 是用来管理复杂逻辑的,那么它最常用于项目的什么地方呢?

自然是靠近业务需求、架构、模型的地方。

对于后端开发者来说,这些概念可能有点眼熟。后端经常使用的面向对象设计,其实就是给业务需求建模。

那么,现在我有一个好消息要告诉你:你完全可以用「类」来理解自定义 Hook。它们的核心思路完全相同。

我们不妨用 Java 来做一个类比。一个 Java 类(People)长这样:

class People {
    private String name;

    // 构造函数
    public People(String name) {
        this.name = name;
    }

    // 方法
    public void run() {
        System.out.println(this.name + " is running");
    }
}

// 实例化
People p = new People("Tom");
p.run();

而在 React 里,一个自定义 Hook(usePeople)是这样的:

function usePeople(initialName) {
   const [name, setName] = useState(initialName);

   const run = () => {
      console.log(name + " is running");
   };

   // 返回公共接口
   return { name, run, setName };
}

// 在组件中“实例化”
const p = usePeople("Tom");
p.run();

对比一下它们之间的关系:

  • 类的属性对应 Hook 的状态(state)
  • 类的方法对应 Hook 的函数(操作)
  • 类的构造函数对应 useState 中的初始值,以及 Hook 返回的对象

Amazing!我们把 Hook 联系到了一个已知的事物上。让我们抛弃 React 官方文档中对自定义 Hook 的定义:「自定义 Hook 是一种在组件间共享逻辑的东西」,改成我们自己可以理解的定义:「自定义 Hook 类似面向对象中的类,封装了状态和操作,用于管理复杂的业务逻辑」。

当面向对象设计原则遇到 Hook

既然 Hook 可以类比成「类」,那么面向对象中我们熟悉的设计原则是否也适用呢?我们一个个看一下。

1. 封装

Hook 完美体现了封装。它将内部的状态和操作打包在一起。组件只关心 Hook 暴露了什么,不关心内部实现(比如内部状态是如何更新的)。

2. 继承

这是最大的不同点:Hook 没有继承

在面向对象中,我们可能会让 Athlete 类继承 People 类。但在 React 中,我们不这么做。React 的答案是「组合」。一个 Hook 可以调用另一个 Hook 来扩展它的功能。

// 基础的 People hook
function usePeople(initialName) {
  const [name, setName] = useState(initialName);
  const run = () => console.log(name + " is running");
  return { name, setName, run };
}

// Athlete hook 扩展了 People hook 的能力
function useAthlete(initialName) {
  // 1. “继承” People 的能力
  const { name, setName, run } = usePeople(initialName); 
  
  // 2. 增加自己的能力
  const train = () => console.log(name + " is training hard");

  // 3. 返回一个“子类”实例
  return { name, setName, run, train };
}

useAthlete 通过调用 usePeople 来复用其能力,并增加了自己的 train 逻辑。

3. 多态

因为不存在继承,所以 Hook 并没有所谓的多态。不过对于 JavaScript 这种动态语言,我们完全可以用鸭子类型达成同样的效果。

function usePerson() {
  const work = () => console.log("Person is working");
  return { work };
}

function useRobot() {
  const work = () => console.log("Robot is working");
  return { work };
}

function Worker({ useWorker }) {
  const { work } = useWorker();
  return <button onClick={work}>Work</button>;
}

function App() {
  return (
    <div>
      <Worker useWorker={usePerson} />
      <Worker useWorker={useRobot} />
    </div>
  );
}

4. 实例化

就像面向对象中类和对象的关系一样。在组件中每次调用 usePeople(),都会得到一个全新的、独立的状态实例。

这就像 new People("Tom")new People("Jerry") 两次,会得到两个不同的对象,它们的状态(name)互不干扰。

全新的开发思路:用面向对象建模,用 Hook 实现

我个人在学习时,走过一个弯路:我曾经以为自定义 Hook 是个很高级、很难的东西,导致很晚才去主动学习和使用它。

但是从上面的讨论中我们可以发现:自定义 Hook 不是高级功能,而是组织复杂业务逻辑的必需品

这也是我认为很多前端项目中经常出现的误区:把所有业务逻辑(异步请求、数据处理、副作用)都会堆积在组件中,导致项目难以维护。

即使后续把组件中的业务逻辑抽取到了 Hook 中,因为一开始是面向 UI 进行开发的,最终抽取的 Hook 有可能是注重「技术上的复用」,而不是「业务的建模」。这种自底向上式的抽象方法,在面对复杂业务逻辑时的效果并不好。

那么,更合适的抽象方法,其实是自顶向下的。这种方法后端开发者应该会很熟悉,我们完全可以利用 Hook 和类的相似性,在 React 中实现这种方法。

整体的开发流程是这样的:

  1. 面向对象建模: 面对复杂需求时,先不要想 UI,而是用面向对象思路进行建模和设计。例如,一个订单系统,我需要一个 Order 模型,它有若干属性、若干方法。
  2. 翻译成 Hook 实现:Order 模型翻译成对应的自定义 Hook,也就是 useOrder。属性和方法变成 Hook 中的状态和函数。
  3. 构建视图组件: 最后一步才是编写组件。组件使用 useOrder 获取状态和操作,把它们绑定到 UI 元素上。

React 官方的 UI = f(state) 理念是函数式的,这个理念本身很好,它让视图渲染变得可预测。但在宏观的架构设计上,函数式思维可能让新手无从下手。

我们的策略可以是:高层架构用面向对象思维(业务建模),底层实现用函数式思维(构建 UI)。这两者并不冲突,结合起来可能更清晰实用。

总结

这篇文章为你讲述了一种理解和使用 React 的新思路:把自定义 Hook 看成类,从而在 React 开发中使用面向对象的思想。

必须要注意的一点是:把 Hook 看作类,只是一种视角,或者说「心智模型」。它的作用是帮助我们理解,而不是在 React 世界中一比一地复刻面向对象设计。React 的 Hook 在底层实现和很多细节上,和「类」有巨大的区别。

在 React 项目开发的过程中,你可以大胆使用面向对象的思想进行业务逻辑的建模,将后端思想与前端技术融会贯通,你的上限会高于 99% 的专业前端开发者。

希望这篇文章可以帮助你消除对 Hook 的恐惧,在面对复杂的业务逻辑时,能更自信地使用它来组织和管理业务逻辑。