原文:dev.to/tylermcginn…
譯者:前端技術小哥前端
當你要學習一個新事物的時候,你應該作的第一件事就是問本身兩個問題react
若是你歷來沒有對這兩個問題都給出一個使人信服的答案,那麼當你深刻到具體問題時,你就沒有足夠的堅實的基礎。關於React Hooks,這些問題值得使人思考。當Hooks發佈時,React是JavaScript生態系統中最流行、最受歡迎的前端框架。儘管React已經受到高度讚賞,React團隊仍然認爲有必要構建和發佈Hooks。在不一樣的Medium帖子和博客文章中紛紛討論了(1)儘管受到高度讚賞和受歡迎,React團隊決定花費寶貴的資源構建和發佈Hooks是爲何和爲了什麼以及(2)它的好處。爲了更好地理解這兩個問題的答案,咱們首先須要更深刻地瞭解咱們過去是如何編寫React應用程序的。數組
若是你已經使用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標準的意願提交併包含類。歷史代表,他們選擇了後者。前端框架
咱們認爲咱們不從事設計類系統的工做。咱們只想以任何慣用的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
屬性」錯誤。fetch
constructor (props) {
...
this.updateRepos = this.updateRepos.bind(this) // 😭
}
複製代碼
如今我猜大家可能會想。首先,這些問題至關膚淺。固然,調用super(props)並牢記bind方法是很麻煩的,但這裏並無什麼根本錯誤。其次,這些React的問題並不像JavaScript類的設計方式那樣嚴重。固然這兩點都是毋庸置疑的。然而,咱們是開發人員。即便是最淺顯的問題,當你一天要處理20屢次的時候,也會變得很討厭。幸運的是,在從createClass切換到React.Component以後不久,類字段提案出現了。ui
類字段使咱們可以直接將實例屬性添加爲類的屬性,而沒必要使用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>
複製代碼
這就是咱們如今的狀況。
如今咱們須要一個新的組件API來解決全部這些問題,同時保持簡單、可組合、靈活和可擴展。這個任務很艱鉅,可是React團隊最終成功了。
自從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的知識須要學習,可是如今你已經知道了它們存在的緣由,咱們就有了一個堅實的基礎。