为何会出现React Hooks?

原文:dev.to/tylermcginn…
译者:前端技术小哥前端

当你要学习一个新事物的时候,你应该作的第一件事就是问本身两个问题react

  • 一、为何会存在这个东西?
  • 二、这东西能解决什么问题?

若是你历来没有对这两个问题都给出一个使人信服的答案,那么当你深刻到具体问题时,你就没有足够的坚实的基础。关于React Hooks,这些问题值得使人思考。当Hooks发布时,React是JavaScript生态系统中最流行、最受欢迎的前端框架。尽管React已经受到高度赞赏,React团队仍然认为有必要构建和发布Hooks。在不一样的Medium帖子和博客文章中纷纷讨论了(1)尽管受到高度赞赏和受欢迎,React团队决定花费宝贵的资源构建和发布Hooks是为何和为了什么以及(2)它的好处。为了更好地理解这两个问题的答案,咱们首先须要更深刻地了解咱们过去是如何编写React应用程序的。数组

createClass

若是你已经使用React足够久,你就会记的React.createClassAPI。这是咱们最初建立React组件的方式。用来描述组件的全部信息都将做为对象传递给createClass。bash

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最初使用createClassAPI的缘由是,当时JavaScript没有内置的类系统。固然,这最终改变了。在ES6中, JavaScript引入了class关键字,并使用它以一种本机方式在JavaScript中建立类。这使React处于一个进退两难的地步。要么继续使用createClass,对抗JavaScript的发展,要么按照EcmaScript标准的意愿提交并包含类。历史代表,他们选择了后者。前端框架

React.Component

咱们认为咱们不从事设计类系统的工做。咱们只想以任何惯用的JavaScript方法来建立类。-React v0.13.0发布 Reactiv0.13.0引入了React.ComponentAPI,容许您从(如今)本地JavaScript类建立React组件。这是一个巨大的胜利,由于它更好地与ECMAScript标准保持一致。markdown

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方法里将组件的状态初始化为实例(this)上的state属性。可是,根据ECMAScript规范,若是要扩展子类(在这里咱们说的是React.Component),必须先调用super,而后才能使用this。具体来讲,在使用React时,咱们还须记住将props传递给super。函数

constructor (props) {
    super(props) // 🤮

    ...
  }
复制代码

自动绑定

当使用createClass时,React将自动地将全部方法绑定到组件的实例上,也就是this。有了React.Component,状况就不一样了。很快,各地的React开发人员都意识到他们不知道如何运用这个“this”关键字。咱们必须记住在类的constructor中的.bind方法,而不是让使用刚刚还能用的方法调用。若是不这样作,则会出现广泛的“没法读取未定义的setState属性”错误。oop

constructor (props) {
    ...
    this.updateRepos = this.updateRepos.bind(this) // 😭
}
复制代码

如今我猜大家可能会想。首先,这些问题至关肤浅。固然,调用super(props)并牢记bind方法是很麻烦的,但这里并无什么根本错误。其次,这些React的问题并不像JavaScript类的设计方式那样严重。固然这两点都是毋庸置疑的。然而,咱们是开发人员。即便是最浅显的问题,当你一天要处理20屡次的时候,也会变得很讨厌。幸运的是,在从createClass切换到React.Component以后不久,类字段提案出现了。学习

类字段

类字段使咱们可以直接将实例属性添加为类的属性,而没必要使用constructor。这对咱们来讲意味着,在类字段中,咱们以前讨论的两个“小”问题都将获得解决。咱们再也不须要使用constructor来设置组件的初始状态,也再也不须要在constructor中使用.bind,由于咱们可使用箭头函数。

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的迁移过程当中,出现了一些权衡,但正如咱们所看到的,类字段解决了一些问题。不幸的是,咱们仍有一些更深入的(但更少说起)咱们所看到的全部之前版本存在的问题。 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做为一个属性传递给任何须要它的组件。

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高级组件中。

// ReposGrid.js
function ReposGrid ({ loading, repos }) {
  ...
}

export default withRepos(ReposGrid)
复制代码
// Profile.js
function Profile ({ loading, repos }) {
  ...
}

export default withRepos(Profile)
复制代码

这是可行的,它加上过去的Render Props一直是共享非可视逻辑的推荐解决方案。然而,这两种模式都有一些缺点。 首先,若是你不熟悉它们(即便你熟悉),你会有点懵。当咱们使用withRepos高级组件时,咱们会有一个函数,它以最终呈现的组件做为第一个参数,但返回一个新的类组件,即为逻辑所在。这是一个多么复杂的过程啊。 接下来,若是咱们耗费的是多个高级组件,又会怎样呢?你能够想象,它很快就失控了。

export default withHover(
  withTheme(
    withAuth(
      withRepos(Profile)
    )
  )
)
复制代码

比^更糟的是最终获得的结果。这些高级组件(和相似的模式)迫使咱们从新构造和包装组件。这最终可能致使“包装地狱”,这又一次使它更难遵循。

<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)很烦人。
  • 没人知道"this"是怎么回事。
  • 好吧,冷静下来。我知道你知道这是怎么回事,但对有些人来讲,这是一个没必要要的障碍。
  • 按照生命周期方法组织组件迫使咱们在组件中散布相关的逻辑。
  • React没有用于共享非可视逻辑的良好原语。

如今咱们须要一个新的组件API来解决全部这些问题,同时保持简单、可组合、灵活和可扩展。这个任务很艰巨,可是React团队最终成功了。

React Hooks

自从Reactive0.14.0以来,咱们有两种方法来建立组件-类或函数。区别在于,若是组件具备状态或须要使用生命周期方法,则必须使用类。不然,若是它只是接受道具并呈现一些UI,咱们可使用一个函数。 若是不是这样呢。若是咱们不用使用类,而是老是使用函数,那该怎么办呢?

有时候,天衣无缝的安装只须要一个函数。不用方法。不用类。也不用框架。只须要一个函数。 ——John Carmack. OculusVR首席技术官。

固然,咱们须要找到一种方法来添加功能组件拥有状态和生命周期方法的能力,可是假设咱们这样作了,咱们能获得什么好处呢? 咱们再也不须要调用super(props),再也不须要考虑bind方法或this关键字,也再也不须要使用类字段。,咱们以前讨论的全部“小”问题都会消失。

(ノಥ,_」ಥ)ノ彡 React.Component 🗑

function ヾ(Ő‿Ő✿)
复制代码

如今,更棘手的问题来了。

  • 状态
  • 生命周期方法
  • 共享非视觉逻辑

状态

因为咱们再也不使用类或this,咱们须要一种新的方法来添加和管理组件内部的状态。React v16.8.0经过useState方法为咱们提供了这种新途径。
useState是咱们将在这个课程中看到的许多“Hooks”中的第一个。让这篇文章的下面部分做为一个简单的介绍。以后,咱们将更深刻地研究useState和其余Hooks。
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) // 👌
复制代码

如今,让咱们使用新发现的关于useState的Hook的知识来更新ReposGrid组件。

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>
  )
}
复制代码
  • 状态✅
  • 生命周期方法
  • 共享非视觉逻辑

生命周期方法

有件事可能会让你难过(或开心?)。当使用ReactHooks时,咱们须要忘记所知道的关于通俗的React生命周期方法以及这种思惟方式的全部东西。咱们已经看到了考虑组件的生命周期时产生的问题-“这(指生命周期)瓜熟蒂落的迫使整个组件中散布着相关的逻辑。”相反,考虑一下同步。想一想咱们曾经用到生命周期事件的时候。无论是设置组件的初始状态、获取数据、更新DOM等等,最终目标老是同步。一般,把React land以外的东西(API请求、DOM等)与Reactland以内的(组件状态)同步,反之亦然。当咱们考虑同步而不是生命周期事件时,它容许咱们将相关的逻辑块组合在一块儿。为此,Reaction给了咱们另外一个叫作useEffect的Hook。
很确定地说useEffect使咱们能在function组件中执行反作用操做。它有两个参数,一个函数和一个可选数组。函数定义要运行的反作用,(可选的)数组定义什么时候“从新同步”(或从新运行)effect。

React.useEffect(() => {
  document.title = `Hello, ${username}`
}, [username])
复制代码

在上面的代码中,传递给useEffect的函数将在用户名发生更改时运行。所以,将文档的标题与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,更重要的是,咱们再也不在整个组件中散布(和复制)effect逻辑。

  • 状态✅
  • 生命周期方法✅
  • 共享非视觉逻辑

共享非视觉逻辑

前面咱们提到过,React对共享非可视逻辑没有很好的解决方案是由于“React将UI耦合到组件”。这致使了像高阶组件或渲染道具这样过于复杂的模式。如今您可能已经猜到了,Hooks对此也有一个答案。然而,这可能不是你想象的那样。实际上并无用于共享非可视逻辑的内置Hook,而是,咱们能够建立与任何UI解耦的自定义 。
经过建立咱们本身的自定义useRepos Hook,咱们能够看到这一点。这个 将接受咱们想要获取的Repos的id,并(保留相似的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的知识须要学习,可是如今你已经知道了它们存在的缘由,咱们就有了一个坚实的基础。

❤️ 看以后

  • 点赞,让更多的人也能看到这篇内容(收藏不点赞,都是耍流氓 -_-)
  • 关注公众号「新前端社区」,号享受文章首发体验!每周重点攻克一个前端技术难点。

相关文章
相关标签/搜索