如何使用 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 />
随后,我们开始关注将函数作为 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} /> } />
尽管这两种模式都解决了重用代码逻辑问题,但它们给我们留下了包装器地狱问题,如下所示:
因此,总而言之,我们可以看到重用代码逻辑存在一些问题:
- 实现起来不太直观
- 大量代码
- 包装地狱
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 />; }
- 代码分布在不同的生命周期钩子上
- 没有单一的责任
- 难以测试
3. 课程对于人类和机器来说都很难
从人的角度看这个问题,我们都曾经尝试调用子组件内的函数,结果显示:
TypeError: Cannot read property 'setState' of undefined
然后我们绞尽脑汁想找出原因:忘记在构造函数中绑定它了。所以,即使是一些经验丰富的开发人员,这仍然是一个令人困惑的问题。
获取调用该函数的对象的值
此外,您还需要编写大量样板代码才能开始实现第一个副作用:
extends -> state -> componentDidMount -> componentWillUnmount -> render -> return
由于以下原因,分类对于机器来说也很难:
- 缩小版本不会缩小方法名称
- 未使用的方法不会被删除
- 热重载和编译器优化困难
我们上面讨论的所有三个问题并不是三个不同的问题,而是一个问题的症状,那就是 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>
);
}
调用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>
);
}
此效果将在首次渲染后和每次更新后调用。您还可以返回一个可选函数,该函数将成为清理机制。这使我们能够将添加和删除订阅的逻辑保持在彼此接近的位置。
useEffect 调用的第二个参数是一个可选数组。只有当数组中的元素值发生变化时,你的 effect 才会重新运行。可以将其视为shouldComponentUpdate 的工作原理。如果你希望只运行一次 effect 并清理它(在挂载和卸载时),你可以传递一个空数组([]) 作为第二个参数。这会告诉 React,你的 effect 不依赖于任何 props 或 state 的值,因此它永远不需要重新运行。这类似于我们熟悉的componentDidMount和componentWillUnmount的思维模型。如果你想深入了解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];
}
现在我们可以将 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>
);
}
这真是一个“尤里卡”的时刻!不是吗?
但在安顿下来并赞美 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
本文是我在 2018 年 12 月 ReactSydney 聚会上的演讲的一部分。希望这篇文章能激发你尝试 React Hooks 的兴趣。我对 React路线图感到非常兴奋,它看起来前景光明,并有可能改变我们目前使用 React 的方式。
您可以在此链接找到源代码和演示。
如果你喜欢这篇文章,给我点几声❤️肯定能让我开心起来😀。接下来还有更多。
文章来源:https://dev.to/aman_singh/how-to-write-90-cleaner-code-with-hooks-1mmj