为什么会出现React Hooks?

6,266

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

当你要学习一个新事物的时候,你应该做的第一件事就是问自己两个问题

  • 1、为什么会存在这个东西?
  • 2、这东西能解决什么问题?

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

createClass

如果你已经使用React足够久,你就会记的React.createClassAPI。这是我们最初创建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最初使用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标准保持一致。

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属性”错误。

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的知识需要学习,但是现在你已经知道了它们存在的原因,我们就有了一个坚实的基础。

❤️ 看之后

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