爲什麼會出現React Hooks?

原文鏈接:https://dev.to/tylermcginnis/why-react-hooks-51lj

原文:https://dev.to/tylermcginnis/why-react-hooks-51lj…
譯者:前端技術小哥

當你要學習一個新事物的時候,你應該做的第一件事就是問自己兩個問題

  • 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的知識需要學習,但是現在你已經知道了它們存在的原因,我們就有了一個堅實的基礎。

❤️ 看之後

  • 點贊,讓更多的人也能看到這篇內容(收藏不點贊,都是耍流氓 -_-)
  • 關注公衆號「新前端社區」,號享受文章首發體驗!每週重點攻克一個前端技術難點。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章