Hooks
是一个新的概念,它允许你组成状态和副作用。它们允许你在组件之间重用有状态逻辑。
如果你已经使用 Preact
工作了一段时间,你可能会熟悉 render props
和higher order components
等模式,这些模式试图解决这些挑战。这些解决方案往往会使代码更难遵循,更抽象。钩子API 使得整齐地提取状态和副作用的逻辑成为可能,同时也简化了独立于依赖该逻辑的组件的单元测试。
钩子可以用在任何组件中,并且避免了类组件 API 所依赖的这个关键字的许多陷阱。钩子不是从组件实例中访问属性,而是依赖于闭包。这使得它们具有值约束,并消除了在处理异步状态更新时可能出现的一些陈旧数据问题。
有两种方式导入钩子:从preact/hooks
或preact/compat
。
简介
理解钩子的最简单的方法是将它们与同等的基于类的Components
进行比较。
我们将使用一个简单的计数器组件作为我们的例子,它渲染了一个数字和一个将其增加一的按钮。
1 | class Counter extends Component { |
现在,这里有一个用钩子构建的等价函数组件:
1 | function Counter() { |
在这一点上,它们似乎非常相似,然而我们可以进一步简化钩子版本。
让我们将计数器逻辑提取到一个自定义的钩子中,使其易于在各个组件中重用。
1 | function useCounter() { |
注意:CounterA
和 CounterB
都是完全独立的。它们都使用useCounter()
自定义钩子,但每个钩子都有自己的钩子相关状态的实例。
觉得这看起来有点奇怪?你并不孤单!
我们很多人花了一段时间才习惯了这种做法。
依赖性参数
许多钩子都接受一个参数,这个参数可以用来限制钩子的更新时间。Preact
会检查依赖关系数组中的每个值,并检查它是否在上次调用钩子后发生了变化。当没有指定依赖性参数时,钩子总是被执行。
在上面的useCounter()
实现中,我们向useCallback()
传递了一个依赖关系数组。
1 | function useCounter() { |
在这里传递value
会导致每当 value
改变时,useCallback
都会返回一个新的函数引用。这对于避免 “陈旧的闭包”是必要的,因为在这种情况下,回调总是会引用第一个渲染的 value
变量,当它被创建时,导致增量总是设置一个值为1
。
这样每次
value
发生变化时都会创建一个新的increment
回调。出于性能方面的考虑,使用useState
来更新状态值通常比使用依赖关系保留当前值要好。
有状态钩子
下面我们就来看看如何在功能组件中引入状态逻辑。
在引入钩子之前,任何需要状态的地方都需要类组件。
useState
这个钩子接受一个参数,这将是初始状态。当调用这个钩子时,会返回一个由两个变量组成的数组,第一个是当前状态,第二个是我们的状态设置器。
我们的setter
的行为类似于经典状态的setter
,它接受一个以currentState
为参数的值或函数。当你调用setter
而状态不同时,它将从使用了该useState
的组件开始重新渲染。
1 | import { h } from 'preact'; |
当我们的初始状态开销很大时,最好传递一个函数而不是一个值。
useReducer
useReducer
钩子与 redux
有很相似的地方。与 useState
相比,当你有复杂的状态逻辑,下一个状态取决于上一个状态时,它更容易使用。
1 | const initialState = 0; |
记忆化
在 UI 编程中,经常会有一些状态或结果需要耗时的计算。Memoization
可以缓存该计算结果,使其在使用相同的输入时可以重复使用。
useMemo
通过useMemo
钩子,我们可以记住其计算结果,只有当其中一个依赖关系发生变化时才重新计算。
1 | const memoized = useMemo( |
不要在
useMemo
里面运行任何effectful
代码。副作用属于useEffect
。
useCallback
useCallback
钩子可以用来确保只要没有依赖关系发生变化,返回的函数就会保持引用平等。当子组件依赖引用平等来跳过更新时,这可以用来优化子组件的更新(例如 shouldComponentUpdate
)。
1 | const onClick = useCallback( |
有趣的是:
useCallback(fn,deps)
相当于useMemo(()=>fn,deps)
。
使用引用
要在一个功能组件中获取一个 DOM 节点的引用,有一个 useRef
钩子。它的工作原理类似于 createRef
。
1 | function Foo() { |
注意不要把
useRef
和createRef
混淆。
使用上下文
要在功能组件中访问上下文,我们可以使用 useContext
钩子,而不需要任何高阶或包装组件。
第一个参数必须是由 createContext
调用创建的上下文对象。
1 | const Theme = createContext('light'); |
副作用
副作用是许多现代 Apps 的核心。无论你是想从 API 中获取一些数据,还是想在文档上触发一个效果,你会发现 useEffect
几乎可以满足你所有的需求。这也是 hooks API 的主要优势之一,它重新塑造了你的思维,让你以效果而不是组件的生命周期来思考。
useEffect
顾名思义,useEffect
是触发各种副作用的主要方式。如果需要的话,你甚至可以从你的effect
中返回一个清理函数。
1 | useEffect(() => { |
我们先从 Title 组件开始,它应该反映出文档的标题,这样我们就可以在浏览器标签页的地址栏中看到它。
1 | function PageTitle(props) { |
useEffect
的第一个参数是一个触发效果的无参数回调。在我们的例子中,我们只想触发它,当标题真的发生了变化。当它保持不变时,更新它就没有意义了。这就是为什么我们要用第二个参数来指定我们的依赖关系数组。
但有时我们有一个更复杂的用例。想象一下一个组件,当它挂载时需要订阅一些数据,而当它卸载时需要取消订阅。这也可以通过 useEffect
来完成。要运行任何清理代码,我们只需要在回调中返回一个函数。
1 | // Component that will always display the current window width |
清理函数是可选的。如果你不需要运行任何清理代码,你不需要在传递给
useEffect
的回调中返回任何东西。
useLayoutEffect
该签名与 useEffect
相同,但它将在组件被扩散且浏览器有机会绘制时立即启动。
useErrorBoundary
每当一个子组件抛出一个错误时,你可以使用这个钩子来捕捉它并向用户显示一个自定义的错误 UI。
1 | // error = The error that was caught or `undefined` if nothing errored. |
出于监控的目的,任何错误的通知服务都是非常有用的。为此,我们可以利用一个可选的回调,并将其作为 useErrorBoundary
的第一个参数。
1 | const [error] = useErrorBoundary(error => callMyApi(error.message)); |
一个完整的使用例子可能是这样的:
1 | const App = props => { |
如果你过去一直在使用基于类的组件 API,那么这个 hook 本质上是
componentDidCatch
生命周期方法的替代品。这个钩子是在 Preact 10.2.0 中引入的。