为什么选择 React Hooks?
本文最初发布于ui.dev ,是我们React Hooks课程的一部分。如果您喜欢这篇文章,不妨看看。
当你要学习新东西时,你应该做的第一件事就是问自己两个问题 -
1)这个东西为什么存在?
2)这个东西解决了什么问题?
如果您从未对这两个问题给出令人信服的答案,那么在深入研究细节时,您将没有足够坚实的基础。这些问题对于React Hooks来说特别有趣。当 Hooks 发布时,React 是JavaScript 生态系统中最流行和最受欢迎的前端框架。尽管获得了现有的赞誉,但 React 团队仍然认为有必要构建和发布 Hooks。在各种 Medium 帖子和博客评论文章中,关于 Hooks 的是原因(1) 为什么以及为了什么(2) 好处,尽管获得了很高的赞誉和受欢迎程度,React 团队还是决定花费宝贵的资源来构建和发布 Hooks。为了更好地理解这两个问题的答案,我们首先需要更深入地了解我们过去是如何编写 React 应用程序的。
创建类
如果你玩 React 时间够长,你肯定记得这个React.createClass
API。它是我们创建 React 组件的原始方式。所有用来描述组件的信息都会以对象的形式传递给createClass
。
const ReposGrid = React.createClass({
getInitialState () {
return {
repos: [],
loading: true
}
},
componentDidMount () {
this.updateRepos(this.props.id)
},
componentDidUpdate (prevProps) {
if (prevProps.id !== this.props.id) {
this.updateRepos(this.props.id)
}
},
updateRepos (id) {
this.setState({ loading: true })
fetchRepos(id)
.then((repos) => this.setState({
repos,
loading: false
}))
},
render() {
const { loading, repos } = this.state
if (loading === true) {
return <Loading />
}
return (
<ul>
{repos.map(({ name, handle, stars, url }) => (
<li key={name}>
<ul>
<li><a href={url}>{name}</a></li>
<li>@{handle}</li>
<li>{stars} stars</li>
</ul>
</li>
))}
</ul>
)
}
})
createClass
是一种创建 React 组件的简单有效方法。React 最初使用该createClass
API 的原因是,当时 JavaScript 没有内置的类系统。当然,这种情况最终发生了改变。在 ES6 中,JavaScript 引入了class
关键字,并随之提供了一种在 JavaScript 中创建类的原生方法。这让 React 陷入了困境。要么继续使用createClass
并对抗 JavaScript 的发展,要么屈服于 EcmaScript 标准的意志并拥抱类。正如历史所表明的那样,他们选择了后者。
React.Component
我们认为我们并非致力于设计一个类系统。我们只想使用 JavaScript 惯用的方式创建类。—— React v0.13.0 发布
React v0.13.0 引入了React.Component
新的 API,允许你从(现在的)原生 JavaScript 类创建 React 组件。这是一个巨大的进步,因为它使 React 更好地与 EcmaScript 标准保持一致。
class ReposGrid extends React.Component {
constructor (props) {
super(props)
this.state = {
repos: [],
loading: true
}
this.updateRepos = this.updateRepos.bind(this)
}
componentDidMount () {
this.updateRepos(this.props.id)
}
componentDidUpdate (prevProps) {
if (prevProps.id !== this.props.id) {
this.updateRepos(this.props.id)
}
}
updateRepos (id) {
this.setState({ loading: true })
fetchRepos(id)
.then((repos) => this.setState({
repos,
loading: false
}))
}
render() {
if (this.state.loading === true) {
return <Loading />
}
return (
<ul>
{this.state.repos.map(({ name, handle, stars, url }) => (
<li key={name}>
<ul>
<li><a href={url}>{name}</a></li>
<li>@{handle}</li>
<li>{stars} stars</li>
</ul>
</li>
))}
</ul>
)
}
}
尽管这是朝着正确方向迈出的明显一步,React.Component
但也存在一些弊端。
构造函数
使用类组件时,你可以在方法内部将组件的状态初始化constructor
为state
实例的属性(this
)。但是,根据 ECMAScript 规范,如果你要扩展子类(在本例中为React.Component
),则必须先调用super
才能使用this
。具体来说,在使用 React 时,你还必须记住将 传递props
给super
。
constructor (props) {
super(props) // 🤮
...
}
自动绑定
使用 时createClass
,React 会自动将所有方法绑定到组件实例this
。使用 时React.Component
,情况并非如此。很快,世界各地的 React 开发者都意识到他们不知道 this 关键字是如何工作的。.bind
现在,你不得不记住类的 中的方法,而不是“直接”调用constructor
。如果不这样做,就会出现常见的“无法读取setState
未定义的属性”错误。
constructor (props) {
...
this.updateRepos = this.updateRepos.bind(this) // 😭
}
现在我知道你可能在想什么。首先,这些问题很肤浅。调用super(props)
和记住bind
你的方法确实很烦人,但这并没有什么根本性的问题。其次,这些问题甚至不一定是 React 的问题,而是 JavaScript 类的设计方式的问题。这两点都说得通。然而,我们是开发者。即使是最肤浅的问题,当你每天处理 20 多次时,也会变得很麻烦。幸运的是,在从 切换createClass
到后不久React.Component
,Class Fields提案就诞生了。
类字段
类字段允许您直接将实例属性作为类的属性添加,而无需使用constructor
。对于我们来说,有了类字段,我们之前讨论的两个“表面”问题都将迎刃而解。我们不再需要使用constructor
来设置组件的初始状态,也不再需要.bind
在 中设置,constructor
因为我们可以在方法中使用箭头函数。
class ReposGrid extends React.Component {
state = {
repos: [],
loading: true
}
componentDidMount () {
this.updateRepos(this.props.id)
}
componentDidUpdate (prevProps) {
if (prevProps.id !== this.props.id) {
this.updateRepos(this.props.id)
}
}
updateRepos = (id) => {
this.setState({ loading: true })
fetchRepos(id)
.then((repos) => this.setState({
repos,
loading: false
}))
}
render() {
const { loading, repos } = this.state
if (loading === true) {
return <Loading />
}
return (
<ul>
{repos.map(({ name, handle, stars, url }) => (
<li key={name}>
<ul>
<li><a href={url}>{name}</a></li>
<li>@{handle}</li>
<li>{stars} stars</li>
</ul>
</li>
))}
</ul>
)
}
}
所以现在一切都好了,对吧?很遗憾,没有。从createClass
到 的转变React.Component
带来了一些权衡,但正如我们所见, Class Fields 解决了这些问题。不幸的是,我们所见过的所有先前版本中仍然存在一些更深刻(但较少被讨论)的问题。
React 的理念在于,通过将应用程序分解成多个独立的组件,然后再将它们组合在一起,可以更好地管理应用程序的复杂性。正是这种组件模型让 React 如此优雅,也正是它成就了 React。然而,问题并不在于组件模型本身,而在于组件模型的实现方式。
重复逻辑
过去,我们构建 React 组件的方式是与组件的生命周期紧密相关的。这种划分自然迫使我们将相关的逻辑分散到整个组件中。在ReposGrid
我们之前使用的示例中,我们可以清楚地看到这一点。我们需要三个独立的方法(componentDidMount
、componentDidUpdate
和updateRepos
)来完成同一件事——保持repos
与组件的同步props.id
。
componentDidMount () {
this.updateRepos(this.props.id)
}
componentDidUpdate (prevProps) {
if (prevProps.id !== this.props.id) {
this.updateRepos(this.props.id)
}
}
updateRepos = (id) => {
this.setState({ loading: true })
fetchRepos(id)
.then((repos) => this.setState({
repos,
loading: false
}))
}
为了解决这个问题,我们需要一个全新的范例来处理 React 组件中的副作用。
共享非视觉逻辑
当你思考 React 中的组合时,你很可能会想到 UI 组合。这很自然,因为这正是 React 所擅长的。
view = fn(state)
实际上,构建一个应用不仅仅只有 UI 层。编写和复用非可视化逻辑的情况并不少见。然而,由于 React 将 UI 与组件耦合,这会带来一些挑战。从历史上看,React 在这方面并没有找到很好的解决方案。
继续我们的例子,假设我们需要创建另一个也需要repos
状态的组件。现在,该状态及其处理逻辑都位于ReposGrid
组件内部。我们该如何处理呢?最简单的方法是复制所有获取和处理状态的逻辑repos
,然后粘贴到新组件中。这听起来很诱人,但其实不然。更明智的做法是创建一个高阶组件,封装所有共享逻辑,并将其loading
作为repos
props 传递给任何需要它的组件。
function withRepos (Component) {
return class WithRepos extends React.Component {
state = {
repos: [],
loading: true
}
componentDidMount () {
this.updateRepos(this.props.id)
}
componentDidUpdate (prevProps) {
if (prevProps.id !== this.props.id) {
this.updateRepos(this.props.id)
}
}
updateRepos = (id) => {
this.setState({ loading: true })
fetchRepos(id)
.then((repos) => this.setState({
repos,
loading: false
}))
}
render () {
return (
<Component
{...this.props}
{...this.state}
/>
)
}
}
}
现在,无论何时我们的应用程序需要任何组件repos
(或loading
),我们都可以将其包装在我们的withRepos
HOC 中。
// ReposGrid.js
function ReposGrid ({ loading, repos }) {
...
}
export default withRepos(ReposGrid)
// Profile.js
function Profile ({ loading, repos }) {
...
}
export default withRepos(Profile)
这种方案有效,并且从历史上看(以及Render Props)一直是共享非视觉逻辑的推荐解决方案。然而,这两种模式都有一些缺点。
首先,如果你不熟悉它们(即使你熟悉),你的大脑也可能会有点难以理解其中的逻辑。在我们的withRepos
HOC 中,我们有一个函数,它以最终渲染的组件作为第一个参数,但返回一个新的类组件,而这正是我们逻辑所在的地方。这真是一个复杂的过程。
接下来,如果我们要使用的 HOC 不止一个,该怎么办呢?你可以想象,情况很快就会失控。
export default withHover(
withTheme(
withAuth(
withRepos(Profile)
)
)
)
比 ^ 更糟糕的是最终渲染的内容。HOC(以及类似的模式)迫使你重构和包装组件。这最终会导致“包装地狱”,这又会让事情变得更加难以理解。
<WithHover>
<WithTheme hovering={false}>
<WithAuth hovering={false} theme='dark'>
<WithRepos hovering={false} theme='dark' authed={true}>
<Profile
id='JavaScript'
loading={true}
repos={[]}
authed={true}
theme='dark'
hovering={false}
/>
</WithRepos>
</WithAuth>
<WithTheme>
</WithHover>
当前状态
这就是我们现在的处境。
- React 非常流行。
- 我们对 React 组件使用类,因为这在当时是最有意义的。
- 调用 super(props) 很烦人。
- 没有人知道“这”是如何运作的。
- 好吧,冷静一下。我知道你知道“这”是怎么回事,但对某些人来说,这简直是不必要的障碍。
- 通过生命周期方法组织我们的组件迫使我们在整个组件中散布相关逻辑。
- React 没有很好的原语来共享非视觉逻辑。
现在我们需要一个新的组件 API,它能解决所有这些问题,同时又能保持简单、可组合、灵活和可扩展。这可是个艰巨的任务,但 React 团队最终还是完成了。
React Hooks
从 React v0.14.0 开始,我们有两种创建组件的方式——类和函数。区别在于,如果组件有状态或需要使用生命周期方法,则必须使用类。否则,如果组件只是接受 props 并渲染一些 UI,则可以使用函数。
那么,如果情况并非如此呢?如果我们不必使用类,而只需使用函数,情况会怎样呢?
有时候,优雅的实现只是一个函数。不是一个方法。不是一个类。不是一个框架。只是一个函数。
——约翰·卡马克。Oculus VR 首席技术官。
当然,我们需要找到一种方法来增加功能组件具有状态和生命周期方法的能力,但假设我们这样做了,我们会看到什么好处?
好吧,我们不再需要调用super(props)
,不再需要担心bind
方法或this
关键字的调用,也不再需要使用类字段了。本质上,我们之前讨论的所有“表面”问题都将消失。
(ノಥ,_」ಥ)ノ彡 React.Component 🗑
function ヾ(Ő‿Ő✿)
现在,问题变得更难了。
- 状态
- 生命周期方法
- 共享非视觉逻辑
状态
由于我们不再使用类或this
,我们需要一种新的方式来在组件内部添加和管理状态。从 React v16.8.0 开始,React 通过useState
方法为我们提供了这种新的方式。
useState
这是本课程中你将看到的众多“Hook”中的第一个。本文的其余部分将作为一个简单的介绍。我们将useState
在后续章节中更深入地探讨其他 Hook。
useState
接受一个参数,即状态的初始值。它返回一个数组,其中第一项是状态片段,第二项是用于更新该状态的函数。
const loadingTuple = React.useState(true)
const loading = loadingTuple[0]
const setLoading = loadingTuple[1]
...
loading // true
setLoading(false)
loading // false
如您所见,逐个抓取数组中的每个项目并不是最佳的开发者体验。这只是为了演示如何useState
返回数组。通常,您可以使用数组解构在一行内抓取所有值。
// const loadingTuple = React.useState(true)
// const loading = loadingTuple[0]
// const setLoading = loadingTuple[1]
const [ loading, setLoading ] = React.useState(true) // 👌
现在让我们ReposGrid
使用新发现的 Hook 知识来更新我们的组件useState
。
function ReposGrid ({ id }) {
const [ repos, setRepos ] = React.useState([])
const [ loading, setLoading ] = React.useState(true)
if (loading === true) {
return <Loading />
}
return (
<ul>
{repos.map(({ name, handle, stars, url }) => (
<li key={name}>
<ul>
<li><a href={url}>{name}</a></li>
<li>@{handle}</li>
<li>{stars} stars</li>
</ul>
</li>
))}
</ul>
)
}
- 州✅
- 生命周期方法
- 共享非视觉逻辑
生命周期方法
有件事可能会让你感到难过(或者高兴?)。在使用 React Hooks 时,我希望你抛开所有关于传统 React 生命周期方法的知识,以及那种思维方式。我们已经看到了以组件生命周期为视角思考的问题——“这种[生命周期]划分自然会迫使我们将相关的逻辑散布到整个组件中。” 相反,应该以同步为视角来思考。
回想一下你曾经使用过生命周期事件。无论是设置组件的初始状态、获取数据、更新 DOM 还是其他任何事情,最终目标始终是同步。通常,将 React 之外的内容(API 请求、DOM 等)与 React 内部的内容(组件状态)同步,反之亦然。
当我们从同步的角度而不是生命周期事件的角度来思考时,它允许我们将相关的逻辑片段组合在一起。为此,React 为我们提供了另一个名为 的 Hook useEffect
。
DefineduseEffect
允许你在函数组件中执行副作用。它接受两个参数:一个函数和一个可选数组。函数定义要运行哪些副作用,而(可选)数组定义何时“重新同步”(或重新运行)该副作用。
React.useEffect(() => {
document.title = `Hello, ${username}`
}, [username])
在上面的代码中,传递给的函数useEffect
会在每次发生更改时运行username
。因此,文档的标题会与Hello, ${username}
解析到的内容同步。
现在,我们如何使用useEffect
代码中的 Hook 来同步repos
我们的fetchRepos
API 请求?
function ReposGrid ({ id }) {
const [ repos, setRepos ] = React.useState([])
const [ loading, setLoading ] = React.useState(true)
React.useEffect(() => {
setLoading(true)
fetchRepos(id)
.then((repos) => {
setRepos(repos)
setLoading(false)
})
}, [id])
if (loading === true) {
return <Loading />
}
return (
<ul>
{repos.map(({ name, handle, stars, url }) => (
<li key={name}>
<ul>
<li><a href={url}>{name}</a></li>
<li>@{handle}</li>
<li>{stars} stars</li>
</ul>
</li>
))}
</ul>
)
}
是不是很漂亮?我们成功去掉了React.Component
、constructor
、super
,this
更重要的是,我们的效果逻辑不再散布(并且重复)在整个组件中。
- 州✅
- 生命周期方法✅
- 共享非视觉逻辑
共享非视觉逻辑
之前我们提到,React 之所以无法很好地共享非可视化逻辑,是因为“React 将 UI 与组件耦合”。这导致了诸如高阶组件或Render props 之类的过于复杂的模式。你现在可能已经猜到了,Hooks 也提供了解决方案。然而,这可能和你的想法不一样。React 没有内置的 Hook 来共享非可视化逻辑,你可以创建自定义的 Hook,使其与任何 UI 解耦。
我们可以通过创建自定义useRepos
Hook 来实际演示这一点。这个 Hook 会接收id
我们想要获取的 Repos 中的一个,然后(为了保持 API 的一致性)返回一个数组,其中第一项是loading
状态,第二项也是repos
状态。
function useRepos (id) {
const [ repos, setRepos ] = React.useState([])
const [ loading, setLoading ] = React.useState(true)
React.useEffect(() => {
setLoading(true)
fetchRepos(id)
.then((repos) => {
setRepos(repos)
setLoading(false)
})
}, [id])
return [ loading, repos ]
}
更棒的是,任何与获取 相关的逻辑repos
都可以在这个自定义 Hook 中抽象出来。现在,无论我们位于哪个组件中,即使它是非可视化逻辑,只要我们需要相关的数据repos
,我们就可以调用这个useRepos
自定义 Hook。
function ReposGrid ({ id }) {
const [ loading, repos ] = useRepos(id)
...
}
function Profile ({ user }) {
const [ loading, repos ] = useRepos(user.id)
...
}
- 州✅
- 生命周期方法✅
- 分享非视觉逻辑✅
Hooks 的营销宣传是能够在函数组件内部使用状态。实际上,Hooks 的作用远不止于此。它能够提升代码复用、代码组合以及更好的默认值。关于 Hooks,我们还有很多内容需要介绍,但现在你已经了解了它们存在的原因,我们便有了一个坚实的基础来继续学习。
本文最初发布于TylerMcGinnis.com,是我们React Hooks课程的一部分。如果您喜欢这篇文章,欢迎查看。
文章来源:https://dev.to/tylermcginnis/why-react-hooks-51lj