这篇文章《Where Did Hooks Come From?》主要讨论了 React Hooks 的来源和背景。在引入 Hooks 之前,React 类需要扩展 React.Component 或 React.PureComponent,而 React 本身没有提供共享代码的 API。因此,React 社区开发人员创建了两种有效共享组件代码的模式,分别是高阶组件(Higher Order Components,简称 HOC)和 Render Props。这些模式在一定程度上解决了代码重用的问题,但仍然存在一些局限性。为了更好地解决这些问题,React Hooks 被引入,为开发者提供了一种更简洁、易于理解的方式来共享和重用组件的逻辑。
下面是正文~~
Hooks 是用于在组件之间共享通用逻辑的。明确地说,我们所说的“逻辑”并不是指组件的 UI 部分(JSX)。我们谈论的是组件中 JSX 之前的所有内容。在基于类的组件中,我们会说它在生命周期方法和自定义方法中。在功能组件中,它只是 JSX 之上的东西。
在某种程度上,Hooks 的故事与 React 及其先前用于共享代码的 API 的故事密切相关。所以请耐心听我从头说起...
2013:第一个React API:
React 开发者不喜欢 mixins,这是共享逻辑的第一个 API。
最初,React 有一种在组件之间共享通用逻辑的方法,称为 mixins。这是在 JavaScript 拥有类之前的 React 早期。这些伪类看起来的组件允许“混入”可共享的逻辑。当时,mixins 被指责为社区开始流行的一些反模式的根本原因。因此,当 React 在 2016 年获得真正的类时,大多数 React 开发人员为 mixins 的 API 消失而欢呼。
2016:类组件
在JavaScript在ES2015(ES6)中获得类之后,React很快跟进了今天仍然可以使用的类组件。但是,如果你对React较为陌生,可能会想知道为什么普遍认为应该在React中完全避免使用类组件?
主要原因是共享逻辑困难。当我们失去了 mixins 时,我们也失去了一种原始的共享代码方式。我们可以通过创建一个新组件来共享/重用 UI,以共享 JSX,但是没有内置方法可以共享生命周期方法,例如 componentDidMount 、 componentDidUpdate 和 componentWillUnmount 。 这些特定方法是我们可能希望管理组件副作用的地方。因此,如果您用某个副作用编写 ComponentOne ,我们将不得不将该逻辑复制到 ComponentTwo ,从而使逻辑无法以一种只编写一次的方式抽象。
我们不能用继承吗?
class ComponentOne extends SharableStuff {
// ...
}
class ComponentTwo extends SharableStuff {
// ...
}
不,React 不允许我们编写从其他组件继承的组件。而且,即使 React 允许你这样做,你将如何将多个逻辑体共享到 ComponentOne ?不允许多重继承,所以这不起作用:
class ComponentOne extends SharableStuffA extends SharableStuffB {
// ...
}
React类必须扩展 React.Component 或 React.PureComponent ,并且React本身没有共享代码的API。
社区虽然很聪明。React 开发人员创建了两种模式,有效地在组件之间共享代码,这两种模式被称为高阶组件(Hoc)和 Render Props。
无状态函数组件
在同一时期,React 团队宣布了一种使用函数而不是类来创建组件的新方法。当时的主要想法是拥有一个仅接受属性并可以返回 JSX 的组件。没有状态或使用类似于类生命周期方法的 React API 的能力。
我们称之为无状态函数组件,因为它们也不能有状态。
不久之后,React 团队告诉我们不要这样称呼它们。我们应该称之为函数组件,因为...他们有计划🤔
2018 Hooks
从本质上讲,Hooks 只是我们可以从函数组件中调用的函数。我们可以使用内置的钩子并编写自己的:
- 内置钩子:这些API(如 useState() )使功能组件能够“挂钩”到React的所有功能。
- 自定义钩子:这些只是我们编写的实现内置钩子的函数。自定义钩子的一般概念是为任何想要使用它的组件创建可重用的逻辑。
React 有 useState() ,因此函数组件可以拥有与类状态类似的自己的本地状态。但是,如果刷新页面,所有本地状态都会重置(就像任何其他 JS 变量一样)。因此,我们可以创建自己的 useLocalStorageState() ,它可能的工作方式与 useState() 完全相同,但还将状态保持到 localStorage ,以便在刷新后恢复值。
下面是一个使用自定义钩子共享数据获取逻辑的示例。你不必完全了解如何使用 useState 和 useEffect ,只需要了解它们为组件执行一些逻辑,我想共享它。如果另一个组件也想根据 productId 获取产品,那么需要重新编写下面高亮的代码:
这里是相同的逻辑移至自定义钩子。现在任何组件都可以使用 useFetchProduct 钩子:
// Custom Hook
function useFetchProduct(productId) {
const [product, setProduct] = useState(null)
useEffect(() => {
fetchProduct(productId).then((product) => setProduct(product))
}, [productId])
return product
}
function BrowseProducts({ productId }) {
const product = useFetchProduct(productId)
// return <div>...</div>
}
这是一个过于简化的例子,上面的 useEffect 代码是不完整的。如果你想要一个获取数据的自定义 Hook,推荐来自 React Query 的自定义钩子,名为 useQuery() 。
如今,如果你愿意,你仍然可以使用类。如果你觉得它们更容易使用,那完全取决于你。然而,在类之间共享逻辑时,你将会遇到问题。即使你可以接受这些问题,并且你不觉得高阶组件(HOC)和 Render Props 混乱,与过去五年开始学习 React 的其他开发者合作或者组队工作时,你可能会发现困难。
他们在 Hooks 被当作 React 主要方法教授时开始接触 React。他们可能不了解类组件的“进退维谷”,如何处理这种奇怪的作用域问题,以及何时以及如何使用 HOC 或 Render Props。此外,React 生态系统中绝大多数第三方库已经放弃了 HOC 和 Render Props,转而采用了 Hooks。因此,你将无法轻松地使用它们的工具,因为 Hooks 仅适用于函数式组件。
我的一些朋友已经使用 React 很长时间了,他们记得这些讨论和权衡。但是我注意到(至少在 Twitter 上),历史正在重演。有一整代新的 React 开发者不知道这个背景故事,也不知道我们为什么要有 Hooks。我承认,Hooks 的某些部分比类更难,比如我们可能需要记忆化( useMemo 和 useCallback ),但这是一种权衡。你可以选择使用带有 HoC 和 Render Props 的类(也不容易),或者使用具有轻松共享代码能力的 Hooks,但需要理解记忆化的复杂性。