2020-03-02

原文: The Guide to Learning React Hooks
作者: Eric Bishard

第一章:關(guān)于React Hooks

它發(fā)布于2018年10月份的16.7.0-alpha.0測試版本中,當(dāng)時Facebook已經(jīng)在生產(chǎn)中使用了一個月,確保了社區(qū)不會面臨重大的漏洞和問題。由于對于破壞向后兼容的大型重構(gòu)往往會出現(xiàn)問題,所以React采用了漸進(jìn)遷移策略( gradual migration and adoption strategy)允許新的API和模式與舊的API和模式共存。
Hooks是對核心庫的添加。這意味著它是可選以及向后兼容的。他們在GITHUB之前發(fā)表了對評論過程的請求。如果您想使用它們,只需安裝最新版本的React。
這種Hooks模式提供了一種對于類組件的替代寫法,可以簡單的使用狀態(tài)以及生命周期方法。Hooks使得函數(shù)組件也可以使用一些只有類組件才能使用的東西,比如我們可以通過useState, useEffect 以及 useContext訪問 React local state, effectscontext。
其它的Hooks還有useReducer, useCallback, useMemo, useRef, useImperativeHandle, useLayoutEffect 以及 useDebugValue。

所以怎樣使用Hooks

最有效的展示方式就是舉一個對比的例子,一種使用類組件的方式寫,需要訪問狀態(tài)以及生命周期方法;另一種使用函數(shù)組件的方式實現(xiàn)同樣的功能。
下面我提供了一個與ReactJS文檔中的示例類似的工作示例,但是您可以修改和測試它,在我們學(xué)習(xí)的每個階段都可以使用StackBlitz演示來親自修改測試。所以讓我們停止談?wù)?,開始學(xué)習(xí)React Hooks。

使用Hooks的優(yōu)點

Hooks對于開發(fā)者又很對優(yōu)點,它改變了我們書寫組件的方式??梢詭椭覀儗懗龈逦⒏啙嵉拇a——就像我們進(jìn)行了代碼節(jié)食,我們減輕了很多體重,看起來更好,感覺更好。它使我們的下頜線突出,使我們的腳趾感到更輕。就像下面這樣。

image

好了,不開玩笑了。Hooks確實減少了代碼體積,它減少并使我們的代碼更加可讀、簡潔和清晰。為了證明這一點,我們來看一段類組件的版本的代碼,它和用Hooks重寫過的有什么不同。
可以看出,代碼量少了多少。使用Hooks不僅減少了差不多5行代碼,而且提升了可讀性以及可測性。將現(xiàn)有的代碼改成Hooks的方式確實可以減少代碼量提高可讀性,但我還是需要提醒你要慎重。記住Hooks是向后兼容的,并且可以與舊版本共存,所以不需要立即重寫整個代碼庫。
image

Hooks的五條重要規(guī)則

在我們創(chuàng)建自己的Hooks之前,讓我們回顧一些我們必須始終遵循的主要規(guī)則。

  1. 不要從循環(huán)、條件或嵌套函數(shù)內(nèi)部調(diào)用Hooks
  2. 只在最頂層使用 Hook
  3. 只在 React 函數(shù)中調(diào)用 Hook
  4. 不要在普通的 JavaScript 函數(shù)中調(diào)用 Hook
  5. 在自定義 Hook 中調(diào)用其他 Hook
    如果需要,可以使用ES Lint插件在團(tuán)隊中強(qiáng)制執(zhí)行這些規(guī)則。同樣在同一頁上也有關(guān)于為什么需要這些規(guī)則的很好的解釋。

Hooks for State and Effects

這個GIF中展示的類組件和函數(shù)組件之間的不同,我們會在后面詳細(xì)解釋。


image
使用useState展示類和函數(shù)計數(shù)組件的不同

下面我們先看一下在React文檔中展示的計數(shù)組件例子。它是一個很簡單的組件,包括一個按鈕,只要點擊按鈕,它就將狀態(tài)向前推進(jìn)一步,并更新state.count以進(jìn)行渲染。
首先,我們先看一下類組件,使用setState更新狀態(tài)。

import React from 'react';

class Counter extends React.Component {
  constructor() {
    this.state = { count: 0 };
    this.incrementCount = this.incrementCount.bind(this);
  }

  incrementCount() {
    this.setState({ count: this.state.count + 1 });
  }

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={this.incrementCount}>Click Me</button>
      </div>
    );
  }
}

export default Counter;

首先要注意的是,我們需要使用類語法,聲明一個constructor,在這里面可以引用this關(guān)鍵詞。在構(gòu)造器中有一個state屬性,使用setState()方法更新狀態(tài)。
下面我們看下函數(shù)組件使用Hooks怎么來實現(xiàn)。

import React, { useState } from 'react';

const Counter = () => {
  const [count, setCount] = useState(0);
  const incrementCount = () => setCount(count + 1);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={incrementCount}>Click Me</button>
    </div>
  )
}

export default Counter;

在這個函數(shù)組件中,我們引進(jìn)了一個useState屬性,并沒有其它的類語法或者構(gòu)造器。它的賦值設(shè)置了默認(rèn)值,不僅提供count屬性,還提供了一個修改該狀態(tài)的函數(shù)setCount。這個setCount是一個函數(shù)方法,可以隨便命名。
組件方法incrementCount更加易讀,可以之間引用我們的state值,而不是引用this.state

useEffect方法的對比

當(dāng)更新狀態(tài)的時候,有時候會發(fā)生一些副作用。在我們的計數(shù)組件中,我們可能需要更新數(shù)據(jù)庫、修改本地存儲或者修改document的title。在React JS文檔中,后一個示例用于使事情盡可能簡單。所以讓我們并更新我們的例子,使用新的鉤子useffect產(chǎn)生一個副作用。
讓我們將這個副作用添加到我們現(xiàn)有的例子中,然后再看一下使用類和使用鉤子的方法。首先看下使用類組件的實現(xiàn)。

import React from 'react';

class Counter extends React.Component {
  constructor() {
    this.state = { count: 0 };
    this.incrementCount = this.incrementCount.bind(this);
  }
  incrementCount() {
    this.setState({ count: this.state.count + 1 });
  }
  
  componentDidMount() { document.title = `You clicked ${this.state.count} times`; }
  componentDidUpdate() { document.title = `You clicked ${this.state.count} times`; }

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={this.incrementCount}>Click Me</button>
      </div>
    );
  }
}

export default Counter;

然后,使用Hooks實現(xiàn)同樣的方法。

import React, { Component, useState, useEffect } from 'react';
function Counter() {
  const [count, setCount] = useState(0);
  const incrementCount = () => setCount(count + 1);

  useEffect(() => {
    document.title = `You clicked ${count} times`
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={incrementCount}>Click me</button>
    </div>
  )
}

export default Counter;

現(xiàn)在我們引入了額外的行為,我們開始看到更多的證據(jù)表明,如何切換到鉤子提供了一種更干凈的方式來處理狀態(tài)和副作用。在類組件中使用兩個方法才能實現(xiàn)的作用,在函數(shù)組件中,使用一個useEffect方法就可以實現(xiàn)。只需去掉類語法和一個額外的方法就可以使我們的代碼更具可讀性。完全值得。

根據(jù)你的需要可以多次調(diào)用useState 和 useEffect

就像使用setState,你也可以多次調(diào)用useState。讓我們換一個示例,它顯示了一個稍微復(fù)雜的情況,我們在頁面上顯示了一個名稱,一些允許更改名稱的輸入,我們希望同時控制名字和姓氏。我們需要創(chuàng)建兩個屬性,每個屬性都有各自的更新和設(shè)置方法。只需對每個調(diào)用useState來設(shè)置默認(rèn)值。
在下面的GIF中,您可以看到它是什么樣子的,以及它在基于類的版本中是什么樣子的,我們將在下面進(jìn)一步探討。

image

正如您所期望的,我們還為每個名稱提供了一個更新函數(shù),以便您可以獨立處理對它們的更改。
我們看下基于類的組件。

import React, { Component } from 'react';

export default class Greeting extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      firstName: 'Harry',
      lastName: 'Poppins'
    };
    this.handleFirstNameChange = this.handleFirstNameChange.bind(this);
    this.handleLastNameChange = this.handleLastNameChange.bind(this);
  }

  handleFirstNameChange = (e) => this.setState({ firstName: e.target.value });
  handleLastNameChange = (e) => this.setState({ lastName: e.target.value });

  render() {
    return (
      <div>
        <input value={this.state.firstName} onChange={this.handleFirstNameChange}/><br />
        <input value={this.state.lastName} onChange={this.handleLastNameChange}/>
        <p>
          <span>{this.state.firstName} {this.state.lastName}</span>
        </p>
      </div>
    );
  }
}

使用Hooks:

import React, { Component, useState } from 'react';
export default function Greeting() {
  
  const [firstName, setFirstName] = useState("Bat");
  const [lastName, setLastName] = useState("Man");;

  const handleFirstNameChange = (e) => setFirstName(e.target.value);
  const handleLastNameChange = (e) => setLastName(e.target.value);

  return (
    <div>
      <input value={firstName} onChange={handleFirstNameChange} /><br />
      <input value={lastName} onChange={handleLastNameChange} />
      <p>
        Hello, <span>{firstName} {lastName}</span>
      </p>
    </div>
  );
}

我不會再討論所有的差異,但我希望您看到一個稍微復(fù)雜一點的例子。希望您開始看到使用Hooks的好處。
讓我們對這個示例再做一個更改,并使用useffect將我們的名稱保存到本地存儲,這樣在刷新頁面時不會丟失狀態(tài)。
看下基于類的組件。

import React, { Component } from 'react';

export default class Greeting extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      firstName: window.localStorage.getItem('classFirstName') || '',
      lastName: window.localStorage.getItem('classLastName') || ''
    };
    this.handleFirstNameChange = this.handleFirstNameChange.bind(this);
    this.handleLastNameChange = this.handleLastNameChange.bind(this);
  }
  handleFirstNameChange = (e) => this.setState({ firstName: e.target.value });
  handleLastNameChange = (e) => this.setState({ lastName: e.target.value });

  componentDidUpdate() {
    window.localStorage.setItem('classFirstName', this.state.firstName),
      [this.state.firstName];
    window.localStorage.setItem('classLastName', this.state.lastName),
      [this.state.lastName];
  }

  render() {
    return (
      <div>
        <input value={this.state.firstName} onChange={this.handleFirstNameChange} /><br />
        <input value={this.state.lastName} onChange={this.handleLastNameChange} />
        <p>
          <span>{this.state.firstName} {this.state.lastName}</span>
        </p>
      </div>
    );
  }
}

對比一下Hooks:

import React, { Component, useState, useEffect } from 'react';
export default function Greeting() {

  const [firstName, setFirstName] = useState(() =>
    window.localStorage.getItem('hooksFirstName') || ''
  );
  const [lastName, setLastName] = useState(() =>
    window.localStorage.getItem('hooksLastName') || ''
  );
  const handleFirstNameChange = (e) => setFirstName(e.target.value);
  const handleLastNameChange = (e) => setLastName(e.target.value);

  useEffect(() => {
    window.localStorage.setItem('hooksFirstName', firstName), [firstName];
    window.localStorage.setItem('hooksLastName', lastName), [lastName];
  });

  return (
    <div>
      <input value={firstName} onChange={handleFirstNameChange} /><br />
      <input value={lastName} onChange={handleLastNameChange} />
      <p>
        Hello, <span>{firstName} {lastName}</span>
      </p>
    </div>
  );
}

第三節(jié):Hooks For Context

為了更好的理解Hooks的另一個基礎(chǔ)鉤子useContext,我們需要對Context API有一個深入的認(rèn)識,它是 React 16.3發(fā)布的一個特性。像學(xué)習(xí)大多數(shù)東西一樣,有時我們在前進(jìn)之前必須完全理解另一個概念。如果你熟悉Context API,那么可以跳過這一節(jié)。如果你第一次接觸Context API,我們會簡要介紹一下并通過demo展示。首先要在你的應(yīng)用程序中添加一個上下文,否則不能呢使用useContext
使用上下文環(huán)境的一個很好的例子是profile組件,我們想一下這個組件都需要有哪些東西。當(dāng)我登錄到xyz.com,有一些數(shù)據(jù)需要在所有或者部分子組件中使用。我們假定需要兩個子組件:<user><team>。一個組件用來展示用戶信息和圖片,另一個組件展示我的團(tuán)隊。我們有React、Angular和Vue團(tuán)隊的成員,因此我們將使用這些框架名稱作為團(tuán)隊名稱。
回到代碼,我們需要通過組件value屬性,將需要的數(shù)據(jù)傳入到<provider>組件中。這樣,我們允許任何組件和她的子組件調(diào)用這些數(shù)據(jù)。
讓我們了解如何通過簡單地將props傳遞給children(在pre-Context API 階段的一個選項)來實現(xiàn)這個組件。在使用“prop透傳”這種方法的時候,就需要一層一層向每個子組件傳遞數(shù)據(jù)。這就為每個組件創(chuàng)建了物理輸入,允許數(shù)據(jù)(狀態(tài))從外部流向每個組件及其子組件。

image

讓我們看下pre-context 階段的例子。

import React from 'react';
import { render } from 'react-dom';
import './style.css';

const profileData = {
  company: 'Progress',
  companyImage: 'https://svgshare.com/i/9ir.svg',
  url: 'https://www.telerik.com/kendo-react-ui/',
  userImage: 'https://i.imgur.com/Y1XRKLf.png',
  userName: 'Kendoka',
  fullName: 'Kendō No Arikata',
  team: 'KendoReact'
}

const App = () => (
  <Profile data={profileData} />
)

const Profile = (props) => (
  <div className="profile">
    <img src={props.data.companyImage}/>
    <User data={props.data} />

  </div>
)

const User = (props) => {
  return (
    <div className="user">
      <a href={props.data.url}>
        <img src={props.data.userImage} width="138px" />
      </a>
      <h1 className="profile-userName">{props.data.userName}</h1>
      <p className="profile-fullName">({props.data.fullName})</p>
      <Team data={props.data} />

      
    </div>
  )
}

const Team = (props) => {
  return (
    <div className="team">
      <p className="profile-team">{props.data.team}</p>
    </div>
  )
}

render(<App />, document.getElementById('root'));

如果一個應(yīng)用有10個組件,每個組件都有自己的組件樹,這些組件樹又有一個組件樹。您愿意手動將props傳遞給可能需要或不需要數(shù)據(jù)的組件嗎?或者您更愿意從組件樹中的任何點使用該數(shù)據(jù)?Context允許在組件之間共享值,而不必顯式地在樹的每個級別傳遞一個prop。我們看下 Context API本身如何應(yīng)用于基于類的組件:

image

來看下代碼

import React from 'react';
import { render } from 'react-dom';
import './style.css';

const ProfileContext = React.createContext();
class ProfileProvider extends React.Component {
  state = {
    company: 'Progress',
    companyImage: 'https://svgshare.com/i/9ir.svg',
    url: 'https://www.telerik.com/kendo-react-ui/',
    userImage: 'https://i.imgur.com/Y1XRKLf.png',
    userName: 'Kendoka',
    fullName: 'Kendō No Arikata',
    team: 'KendoReact'
  }
  render() {
    return (
      <ProfileContext.Provider value={this.state}>
        {this.props.children}
      </ProfileContext.Provider>
    )
  }
}

const App = () => (
  <ProfileProvider>
    <Profile />
  </ProfileProvider>
)

const Profile = () => (
  <div className="profile">
    <ProfileContext.Consumer>
      {context => <img src={context.companyImage} />}
    </ProfileContext.Consumer>
    <User />
  </div>
)

const User = () => (
  <div className="user">
    <ProfileContext.Consumer>
      {context =>
        <React.Fragment>
          <a href={context.url}>
            <img src={context.userImage} width="138px" />
          </a>
          <h1 className="profile-userName">{context.userName}</h1>
          <p className="profile-fullName">({context.fullName})</p>
        </React.Fragment>
      }
    </ProfileContext.Consumer>
    <Team />
  </div>
)

const Team = () => (
  <ProfileContext.Consumer>
    {context =>
      <div className="team">
        <p className="profile-team">{context.team}</p>
      </div>
    }
  </ProfileContext.Consumer>
)

render(<App />, document.getElementById('root'));
Context API入門

我們現(xiàn)在需要了解如何使用useState獲取數(shù)據(jù)狀態(tài),使用useEffect替換組件生命周期方法,如何使用useContext提高provider。
在生命Context API的例子中我們使用prop傳遞的方法分享數(shù)據(jù)。這是個有效的方法,但是在有些情況下會顯得很笨重。更好的情況下,可以是使用Context API
從簡單的prop傳遞示例到更易于維護(hù)的Context API示例,我們最終得到了一些代碼,雖然這是解決我們問題的更好方法,但卻進(jìn)一步在我們的組件中造成了混亂,使得組件的復(fù)用性變差。
我們看下:

image

每個需要獲取數(shù)據(jù)的地方都要使用<ProfileContext.Consumer>包裹。這會在JSX中造成額外的混亂。最好可以在函數(shù)組件的頂部創(chuàng)建一個const變量,以便在整個JSX中使用Profile上下文。我們希望JSX盡可能簡單,盡可能接近我們想要的HTML輸出,讓開發(fā)人員更容易閱讀。Hooks改善了這種情況,是一種非常優(yōu)雅的上下文消費方式。就像我提及的,它允許我們刪除那些曾經(jīng)把JSX弄得一團(tuán)糟的標(biāo)簽。
image

這樣看起來好多了。這是Hooks如何改變我們編寫普通的日常組件的一個例子。
現(xiàn)在來看下使用Hooks重寫之后的Profile組件。不過多解釋代碼內(nèi)容了-如果你還記得,當(dāng)我們調(diào)用useState時,我們有一組值需要去理解。一個是具體的狀態(tài)值,一個是這個值的更新方法。使用useEffect,某個狀態(tài)發(fā)生更改時允許改方法。使用useContext,我們只是將它指向一個現(xiàn)有上下文,而該屬性現(xiàn)在擁有對該上下文的引用。整個使用方法很簡單。

import React, { Component, useContext } from 'react';
import { render } from 'react-dom';
import './style.css';

// Look Ma, No Provider
const ProfileContext = React.createContext({
  company: 'Progress',
  companyImage: 'https://svgshare.com/i/9ir.svg',
  url: 'https://www.telerik.com/kendo-react-ui/',
  userImage: 'https://i.imgur.com/Y1XRKLf.png',
  userName: 'Kendoka',
  fullName: 'Kendō No Arikata',
  team: 'KendoReact'
});

const Profile = () => {
  const context = useContext(ProfileContext);
  return (
    <div className="profile">
      <img src={context.companyImage}/>
      <User />
    </div>
  )
}

const User = () => {
  const context = useContext(ProfileContext);
  return (
    <div className="user">
      <a href={context.url}>
        <img src={context.userImage} width="138px" />
      </a>
      <h1 className="profile-userName">{context.userName}</h1>
      <p className="profile-fullName">({context.fullName})</p>
      <Team />
    </div>
  )
}

const Team = () => {
  const context = useContext(ProfileContext);
  return (
    <div className="team">
      <p className="profile-team">{context.team}</p>
    </div>
  )
}

const App = () => <Profile />;

render(<App />, document.getElementById('root'));
不需要定義Provider

上面的這個demo,我們介紹使用useContext,因為我們只讀取上下文中的數(shù)據(jù),我們直接將數(shù)據(jù)傳遞給createContext()方法。這種方法很有效,不再需要使用< Provider >包裹內(nèi)容。盡管如此,但這不是我想要的方式-為了能夠改變team屬性,我確實需要創(chuàng)建一個Provider。但我想說明的是,如果您傳入一些數(shù)據(jù),而沒有像我們希望的那樣訪問任何函數(shù),那么您可以在沒有提供程序的情況下設(shè)置它。
但有些情況下我們需要訪問并修改上下文中的狀態(tài),就需要使用Provider。比如,我希望能夠改變我們的用戶所屬的團(tuán)隊。
為了上面這種情況,我們又需要創(chuàng)建一個Provider,而不僅僅傳遞默認(rèn)的狀態(tài)數(shù)據(jù)。

使用Provider更新數(shù)據(jù)

我們回到之前使用Context API 的例子中,使用setState更新數(shù)據(jù)。我們應(yīng)該能夠通過調(diào)用一個函數(shù)來更新team屬性的值,這個函數(shù)將作為鍵-值對放在我們的狀態(tài)中。

import React from 'react';
import { render } from 'react-dom';
import './style.css';

const ProfileContext = React.createContext();
class ProfileProvider extends React.Component {
  state = {
    company: 'Progress',
    companyImage: 'https://svgshare.com/i/9ir.svg',
    url: 'https://www.telerik.com/kendo-react-ui/',
    userImage: 'https://i.imgur.com/Y1XRKLf.png',
    userName: 'Kendoka',
    fullName: 'Kendō No Arikata',
    team: 'KendoReact',
    changeTeam: (team) => this.setState({
      team: `Kendo${team}`
    })
  }
  render() {
    return (
      <ProfileContext.Provider value={this.state}>
        {this.props.children}
      </ProfileContext.Provider>
    )
  }
}

const App = () => (
  <ProfileProvider>
    <Profile />
  </ProfileProvider>
)

const Profile = () => (
  <div className="profile">
    <ProfileContext.Consumer>
      {context => <img src={context.companyImage} />}
    </ProfileContext.Consumer>
    <User />
  </div>
)

const User = () => (
  <div className="user">
    <ProfileContext.Consumer>
      {context =>
        <React.Fragment>
          <a href={context.url}>
            <img src={context.userImage} width="138px" />
          </a>
          <h1 className="profile-userName">{context.userName}</h1>
          <p className="profile-fullName">({context.fullName})</p>
          <Team />
          <button className="profile-button"
            onClick={() => context.changeTeam('Angular')}>Angular</button>
          <button className="profile-button"
            onClick={() => context.changeTeam('Vue')}>Vue</button>
          <button className="profile-button"
            onClick={() => context.changeTeam('React')}>React</button>
        </React.Fragment>
      }
    </ProfileContext.Consumer>
  </div>
)

const Team = () => (
  <ProfileContext.Consumer>
    {context =>
      <div className="team">
        <p className="profile-team">{context.team}</p>
      </div>
    }
  </ProfileContext.Consumer>
)

render(<App />, document.getElementById('root'));

讓我們確保用戶只需按一個帶有您要切換到的團(tuán)隊名稱的按鈕,就可以在團(tuán)隊中發(fā)起更改。我們希望這個修改發(fā)生在< User>組件中,但按鈕顯示在Profile視圖的底部。

image

通過添加這些按鈕,我們將需要每個按鈕處理一次單擊,并將正確的團(tuán)隊框架類型傳遞給函數(shù),該函數(shù)將接受團(tuán)隊名稱的參數(shù):Vue, Angular 或者 React。
image

我們需要一個修改狀態(tài)的方法。在狀態(tài)中添加一個新的屬性changeTeam,它的值是名為setState的方法。我們將通過context調(diào)用這個方法。
image

現(xiàn)在我們可以改變team的值,也可以從上下文中讀取。這個模式可以讓我們設(shè)置和訂閱屬性值。
image

我提供了另一個對于之前的Context API例子的優(yōu)化。這個例子仍然沒有使用Hooks,后面我們將看下如何使用Hooks實現(xiàn)。

import React from 'react';
import { render } from 'react-dom';
import './style.css';

const ProfileContext = React.createContext();
class ProfileProvider extends React.Component {
  state = {
    company: 'Progress',
    companyImage: 'https://svgshare.com/i/9ir.svg',
    url: 'https://www.telerik.com/kendo-react-ui/',
    userImage: 'https://i.imgur.com/Y1XRKLf.png',
    userName: 'Kendoka',
    fullName: 'Kendō No Arikata',
    team: 'KendoReact',
    changeTeam: (team) => this.setState({
      team: `Kendo${team}`
    })
  }
  render() {
    return (
      <ProfileContext.Provider value={this.state}>
        {this.props.children}
      </ProfileContext.Provider>
    )
  }
}

const App = () => (
  <ProfileProvider>
    <Profile />
  </ProfileProvider>
)

const Profile = () => (
  <div className="profile">
    <ProfileContext.Consumer>
      {context => <img src={context.companyImage} />}
    </ProfileContext.Consumer>
    <User />
  </div>
)

const User = () => (
  <div className="user">
    <ProfileContext.Consumer>
      {context =>
        <React.Fragment>
          <a href={context.url}>
            <img src={context.userImage} width="138px" />
          </a>
          <h1 className="profile-userName">{context.userName}</h1>
          <p className="profile-fullName">({context.fullName})</p>
          <Team />
          <button className="profile-button"
            onClick={() => context.changeTeam('Angular')}>Angular</button>
          <button className="profile-button"
            onClick={() => context.changeTeam('Vue')}>Vue</button>
          <button className="profile-button"
            onClick={() => context.changeTeam('React')}>React</button>
        </React.Fragment>
      }
    </ProfileContext.Consumer>
  </div>
)

const Team = () => (
  <ProfileContext.Consumer>
    {context =>
      <div className="team">
        <p className="profile-team">{context.team}</p>
      </div>
    }
  </ProfileContext.Consumer>
)

render(<App />, document.getElementById('root'));

接下來我們看另一個例子,包括一個類似按鈕的設(shè)置,以及一個用于修改teams狀態(tài)的方法。實際上,在這個Hooks版本中,按鈕語法和state對象沒有區(qū)別。最大的優(yōu)點就是移除了<ProfileContext.Consumer>,我們只需在每個功能組件中創(chuàng)建一個const,它將保存對我們上下文的引用:

const context = useContext(ProfileContext);

我們只需要像以前那樣調(diào)用上下文及其方法。


import React, { Component, useContext } from 'react';
import { render } from 'react-dom';
import './style.css';

const ProfileContext = React.createContext();

class ProfileProvider extends Component {
  state = {
    company: 'Progress',
    companyImage: 'https://svgshare.com/i/9ir.svg',
    url: 'https://www.telerik.com/kendo-react-ui/',
    userImage: 'https://i.imgur.com/Y1XRKLf.png',
    userName: 'Kendoken',
    fullName: 'Kendoken No Michi',
    team: 'KendoReact',
    toggleTeam: (team) => this.setState({
      team: `Kendo${team}`
    })
  }
  render() {
    return (
      <ProfileContext.Provider value={this.state}>
        {this.props.children}
      </ProfileContext.Provider>
    )
  }
}

let Profile = () => {
  const context = useContext(ProfileContext);
  return (
    <div className="profile">
      <img src={context.companyImage} />
      <User />
    </div>
  )
}

let User = () => {
  const context = useContext(ProfileContext);
  return (
    <div className="user">
      <a href={context.url}>
        <img src={context.userImage} width="138px" />
      </a>
      <h1 className="profile-userName">{context.userName}</h1>
      <p className="profile-fullName">({context.fullName})</p>
      <Team />
      <button className="profile-button"
        onClick={() => context.toggleTeam('Angular')}>Angular</button>
      <button className="profile-button"
        onClick={() => context.toggleTeam('Vue')}>Vue</button>
      <button className="profile-button"
        onClick={() => context.toggleTeam('React')}>React</button>
    </div>
  )
}

let Team = () => {
  const context = useContext(ProfileContext);
  return (
    <div className="team">
      <p className="profile-team">{context.team}</p>
    </div>
  )
}

class App extends Component {
  render() {
    return (
      <ProfileProvider>
        <Profile />
      </ProfileProvider>
    );
  }
}

render(<App />, document.getElementById('root'));

我會再舉一個例子,把我們當(dāng)前的profile組件和“Change Team”按鈕重構(gòu)成放進(jìn)它們自己的獨立組件中,并將提供Context API的組件轉(zhuǎn)為函數(shù)組件-使用useState替代this.state。注意,在最后一個例子中,我們移除了ProfileContext.Consumer標(biāo)簽,以及引入了useContext?,F(xiàn)在我們所有的組件都改成函數(shù)組件了。

import React, { Component, useContext, useState } from 'react';
import { render } from 'react-dom';
import './style.css';

const ProfileContext = React.createContext();
const ProfileProvider = (props) => {
  const userInformation = {
    company: 'Progress',
    companyImage: 'https://svgshare.com/i/9ir.svg',
    url: 'https://www.telerik.com/kendo-react-ui/',
    userImage: 'https://i.imgur.com/Y1XRKLf.png',
    userName: 'Kendoken',
    fullName: 'Kendoken No Michi',
    team: 'KendoReact',
    toggleTeam: (property, value) => {
      setUserInfo(
        {...userInfo,[property]: value}
      );
    }
  }
  const [userInfo, setUserInfo] = useState(userInformation);
  return (
    <ProfileContext.Provider value={userInfo}>
      {props.children}
    </ProfileContext.Provider>
  )
}

const Profile = () => {
  const context = useContext(ProfileContext);
  return (
    <div className="profile">
      <img src={context.companyImage} />
      <User />
    </div>
  )
}

const User = () => {
  const context = useContext(ProfileContext);
  return (
    <div className="user">
      <a href={context.url}>
        <img src={context.userImage} width="138px" />
      </a>
      <h1 className="profile-userName">{context.userName}</h1>
      <p className="profile-fullName">({context.fullName})</p>
      <Team />
      <ChangeTeam />
    </div>
  )
}

const Team = () => {
  const context = useContext(ProfileContext);
  return (
    <div className="team">
      <p className="profile-team">{context.team}</p>
    </div>
  )
}

const ChangeTeam = () => {
  const context = useContext(ProfileContext);
  return (
    <>
      <button className="profile-button"
        onClick={() => context.toggleTeam('team', 'Kendo for Angular')}>Angular</button>
      <button className="profile-button"
        onClick={() => context.toggleTeam('team', 'KendoVue')}>Vue</button>
      <button className="profile-button"
        onClick={() => context.toggleTeam('team', 'KendoReact')}>React</button>
    </>
  )
}

const App = () => (
  <ProfileProvider>
    <Profile />
  </ProfileProvider>
)

render(<App />, document.getElementById('root'));

第四節(jié):Hooks for Reducers

在前面的幾節(jié)我們了解了幾個基礎(chǔ)的React Hooks。現(xiàn)在,讓我們把我們學(xué)到的知識應(yīng)用到一個更高級的演示中并學(xué)會使用useReducer鉤子。在這之前需要保證你對useState有一定了解,如果沒有接觸過,可以回到前面的部分看一下介紹。
Redux是除了setState之外使用reducer處理單向數(shù)據(jù)的最流行方法之一,React團(tuán)隊鼓勵Redux管理state。然而,從16.9 版本發(fā)布之后,React現(xiàn)在有了useReducer,它為我們提供了一種強(qiáng)大的方法來使用reducer,而不依賴Redux庫作為依賴項來管理UI狀態(tài)。

Reducers入門

讓我們討論Redux狀態(tài)reducer和JavaScript方法Array.prototype.reduce之間的區(qū)別。
求和函數(shù)是數(shù)組原型的典型例子。當(dāng)我們在只包含數(shù)字的數(shù)組中調(diào)用reducer時,我們可以返回一個數(shù)值,將數(shù)組中的所有值相加。reducer可以輸入一個初始值作為可選的第二個參數(shù)。讓我們簡要地看一看一些代碼,這些代碼演示了JavaScript的Array.prototype中的reduce方法。


    const votesByDistrict = [250, 515, 333, 410];
    const reducer = (accumulator, currentValue) => {
      return accumulator + currentValue;
    }

    console.log(votesByDistrict.reduce(reducer));
    // expected output: 1508

    // and below we simply add a value to start from:

    console.log(votesByDistrict.reduce(reducer, 777));
    // expected output: 2285

sum函數(shù)是最簡單的例子,但是在這個reduce中,您可以在花括號之間迭代地執(zhí)行任何工作。可以把它想象成一個食譜。給定相同的輸入,總是產(chǎn)生相同的結(jié)果。正在討論的方法可以成為純函數(shù)。這個概念是很重要的,尤其是用來處理狀態(tài)管理的時候。
讓我們再看一個reduce示例,幫助我們更好地理解它們。不必在每次迭代中都累積一個值,還可以在每次迭代中進(jìn)行比較。就像求和運算一樣,我們每次都會存儲一個值,但我們不會將這些值相加,而是存儲到目前為止最高的值。每次迭代,我們將比較數(shù)組中的下一個項與目前為止的最高值,如果它較大,它將替換它,如果不是,繼續(xù)迭代,而不更新最高值。


    const todos = [
    { name: “dishes”, priority: 2 },
    { name: “l(fā)aundry”, priority: 3 ),
    { name: “homework”, priority: 1 }
    ];

    let reducer = (highest, todo) => {
    return highest.priority > todo.priority
      ? Highest
          : todo;
    }

    todos.recuce(reduce, {})
    // output: { name: “l(fā)aundry”, priority: 3 }

這個例子演示了使用reducer的另一種方法。在每次迭代中可以做你任意想做的。這個概念很簡單,我們獲取一個項目數(shù)組(在本例中是對象數(shù)組),執(zhí)行一個操作并將其處理為一個返回值。
React Hooks Reducer類似于JavaScript數(shù)組Reducer,返回一些東西的累積——在我們的例子中,就是React state?;谒幸郧昂彤?dāng)前的操作以及過去發(fā)生的狀態(tài)修改,我們的Reducer接收一個狀態(tài)和一個作為參數(shù)的操作,狀態(tài)根據(jù)action.type被處理,并且我們在運行與該特定action.type的大小寫相匹配的指令后返回新狀態(tài)。
就像我們在現(xiàn)實生活中烹調(diào)一些東西,比如波爾多風(fēng)格的波爾多醬一樣,我們從許多配料開始:黃油、蔥、小牛肉、胡椒,當(dāng)然還有葡萄酒。所有這些原料在平底鍋中混合,然后文火燉。如果重復(fù)并給出相同的步驟,使用相同的成分、相同的量、相同的爐子和相同的溫度,我們每次都應(yīng)該得到相同的結(jié)果。

State 和 Actions概述

我們將構(gòu)建一個Todo應(yīng)用程序。首先,我們希望todo列表有一個初始todo項,該項簡單地說:“ Get Started”。

image

當(dāng)我們添加一個新的todo項時,首先要分派一個操作。
此操作由Reducer函數(shù)處理。我們的操作類型是ADD_TODO。當(dāng)reducer函數(shù)注意到類型變?yōu)?code>ADD_TODO時,它的作用是把舊的狀態(tài)取出來,把它展開,然后把新的todo項附加到最后,我們就得到了新的狀態(tài)。
另一個需要處理的操作是COMPLETE_TODO或者換個更好的名字TOGGLE_COMPLETE。因為我們真正想做的是像開關(guān)一樣打開和關(guān)閉那個狀態(tài)。
在這個例子中,reducer不會向列表中添加任何新項,它會修改現(xiàn)有todo項的一個屬性。如果我們從一個todo開始,上面寫著“Get Started”,然后添加一個新的todo:“Take a break”,我們的狀態(tài)現(xiàn)在應(yīng)該有兩項:


    {
      id: 1,
      name: 'Get started',
      complete: false
    },
    {
      id: 2,
      name: 'Take a break',
      complete: false
    }

注意,每一項都包括幾個屬性,其中一個就是id。這是一個擁有唯一值的鍵,我們使用它來定位一個特定的todo,并在不影響其他todo屬性值的情況下更改該todo的一個屬性。TOGGLE_COMPLETE用來將complete屬性由false修改為true。 完成此操作后,任何更改都將向下傳播到使用該狀態(tài)的任何組件,從而觸發(fā)它們更新。
因為列表中的completed初始值都是false,如果我們觸發(fā)TOGGLE_COMPLETED方法更新id為1的項,狀態(tài)變?yōu)橄旅孢@樣:


    {
      id: 1,
      name: 'Get started',
      complete: true // We changed it!
    },
    {
      id: 2,
      name: 'Take a break',
      complete: false
    }

在使用Hooks之前,如果不引入第三方庫,很難處理reducer操作。現(xiàn)在,我們可以在任何React應(yīng)用程序中輕松實現(xiàn)reducer模式,而不必包含其他依賴項。
這使得處理內(nèi)部狀態(tài)比以前更加容易。這不會取代Redux的所有用法,但它為React開發(fā)人員提供了一種清晰、簡潔的Redux風(fēng)格的方法,可以在不安裝任何依賴項的情況下立即管理內(nèi)部狀態(tài)。

狀態(tài)管理

通常在Redux中,決定如何對狀態(tài)進(jìn)行分類以及存儲在哪里對于初學(xué)者是最大的問題之一。這是他們的Redux常見問題解答中的第一個問題,以下是他們所說的:并沒有正確的答案。有些用戶更喜歡將每一條數(shù)據(jù)都保存在Redux中,以便隨時維護(hù)其應(yīng)用程序的完全可序列化和受控版本。其他人更喜歡在組件的內(nèi)部狀態(tài)中保持非關(guān)鍵或UI狀態(tài),例如“此下拉列表當(dāng)前是否打開”。
Hooks在應(yīng)用程序?qū)臃浅?qiáng)大,我們可以跟蹤諸如“是下拉式打開”和“是菜單關(guān)閉”之類的內(nèi)容。我們可以以Redux風(fēng)格的方式妥善管理UI數(shù)據(jù),而不必脫離React核心。
在唯一的責(zé)任是不斷地改變和附加狀態(tài)的一臺機(jī)器中,reducer是每種操作的不同部分。它的邏輯將增加一個計數(shù)器或管理一個復(fù)雜的對象,這些對象的變化將對當(dāng)前狀態(tài)產(chǎn)生影響。讓我們從功能組件中訪問它和設(shè)置狀態(tài)是這個謎題的最后一部分,同時也是新謎題的第一部分。
讓我們看看如何管理非常簡單的todo類型應(yīng)用程序。這是一個很好的示范例子。以下是我們的Todo應(yīng)用程序的規(guī)則。
我們將需要定義一些部分來設(shè)計一個使用useReducer的簡單的真實案例。我們需要通過添加、完成和清除等操作來跟蹤狀態(tài)的修改和更新。使用熟悉的Redux模式,我們通常會將這些進(jìn)程中的每一個與分配器處理的特定操作類型相關(guān)聯(lián):

  • 一個允許我們進(jìn)入任務(wù)的表單域
  • 一個當(dāng)我們提交時處理表單的分配器
  • 一個包含所有內(nèi)容的實際任務(wù)組件
  • 一個處理狀態(tài)變化的Reducer
    讓我們從添加和組合所有這些片段開始。我不會從創(chuàng)建一個React項目開始,有很多方式可以實現(xiàn)。我會提供一些關(guān)鍵代碼,你可以復(fù)制下來隨意處理。
    在我們的簡單示例中,我們將創(chuàng)建Todo組件作為應(yīng)用程序的實際根級組件,該組件如下所示:

    import React from 'react';
    import { render } from 'react-dom';
    import './style.css';

    const Todo = () => {
      return (
        <>
          Todo Goes Here
        </>
      );
    }

    render(<Todo />, document.getElementById('root'));

它包括一個表單和未提交的輸入字段。也添加了一些樣式表和json數(shù)據(jù)。我們可以使用它來植入一些todo項,以測試我們的列表呈現(xiàn)方式以及數(shù)據(jù)的形狀是否符合HTML。
你可以從這里(https://stackblitz.com/edit/todos-usereducer-hook-start)fork代碼,并測試這些方法。
現(xiàn)在我們已經(jīng)完成了這個項目,我們將通過從React導(dǎo)入useReducer鉤子來進(jìn)行第一個更改。更新代碼中的第一行。

    import React, { useReducer } from 'react';

現(xiàn)在我們需要向useReducer添加調(diào)用,它需要stateaction 作為輸入。我們將其分配給一個數(shù)組對象,這個數(shù)組對象是一個元組(兩個值)-這是在進(jìn)行解構(gòu),因為useReducer()將其作為返回值進(jìn)行匹配:
在Todo組件的return語句上方添加以下行:

const [todos, dispatch] = useReducer(todoReducer, initialState);

items將是todo項的實際列表,dispatch將是用于更改該項列表的實際reducer。在return語句中,我們?yōu)?code>items數(shù)組中的每個項創(chuàng)建一組div。
我們的應(yīng)用程序還有一個問題,因為我們還沒有創(chuàng)建一個名為todoReducer的函數(shù)。讓我們將代碼添加到設(shè)置initialState賦值的行的正下方。


    const todoReducer = (state, action) => {
      switch (action.type) {
        case 'ADD_TODO': {
          return (action.name.length)
            ? [...state, {
              id: state.length ? Math.max(...state.map(todo => todo.id)) + 1 : 0,
              name: action.name,
              complete: false
            }]
            : state;
        }
        default: {
          return state;
        };
      }
    }

一開始這似乎很復(fù)雜。它所做的就是建立一個函數(shù)來執(zhí)行狀態(tài)和動作。通過switch還判斷action.type。一開始,我們只有一個操作,但我們也希望設(shè)置默認(rèn)的catch all,此默認(rèn)值將返回當(dāng)前狀態(tài)。
但是如果它捕捉到一個真正的ADD_TODO,我們將返回當(dāng)前狀態(tài),展開,并將有效數(shù)據(jù)附加到末尾。棘手的部分是分配新的ID。我們在這里所做的是獲取todo的現(xiàn)有列表,并返回最大id加1,否則為零。
既然我已經(jīng)設(shè)置了一個初始狀態(tài),我們很高興進(jìn)入下一步。我們需要確保當(dāng)我們按enter鍵時在輸入字段中鍵入時,我們輸入的值被發(fā)送到一個函數(shù),該函數(shù)將進(jìn)行處理。
因此,首先讓我們用類名todo input替換div,如下所示:


    <div className="todo-input">
      <form onSubmit={addTodo}>
        <input ref={inputRef} type="search" id="add-todo" placeholder="Add Todo..." />
      </form>
    </div>

這確保當(dāng)我們點擊enter時,我們將表單信息發(fā)送給一個名為addTodo()的函數(shù)。我們還使用ref屬性引用輸入,并為該元素提供inputRef的引用值。隨著這些更新,我們需要做更多的事情。
1)我們需要創(chuàng)建一個名為inputRef的屬性,它調(diào)用useRef鉤子2)我們需要創(chuàng)建一個名為addTodo()的函數(shù)
讓我們從創(chuàng)建inputRef屬性開始。在todo組件的頂部,添加以下屬性:

const inputRef = useRef();

我們將使用ref屬性獲取對輸入的引用,這將允許我們稍后訪問其值。他的引用將由todo函數(shù)組件中的本地屬性支持,但這只是對useRef鉤子的調(diào)用。調(diào)用創(chuàng)建的inputRef屬性,使用inputRef.value獲取輸入的值。
你需要像我們引入useReducer一樣導(dǎo)入另一個鉤子。

import React, { useReducer, useRef } from 'react';

最后,我們需要創(chuàng)建addTodo()函數(shù),該函數(shù)將使用此引用并負(fù)責(zé)分配ADD_TODO類型的操作。在返回的正上方添加以下函數(shù):


    function addTodo(event) {
      event.preventDefault();
      dispatch({
        type: 'ADD_TODO',
        name: inputRef.current.value,
        complete: false
      });
        inputRef.current.value = '';
    }

在函數(shù)內(nèi)部,為了防止我們點擊提交表單時頁面刷新。我們調(diào)用了preventDefault ()方法。
然后,我們使用inputRef從表單中獲取輸入值來觸發(fā)ADD_TODO操作。所有todo項最初的completed都是false。最后,我們將inputRef值設(shè)置為空。這將清除輸入字段。
最后,在ADD_TODO觸發(fā)之前,我們還需要進(jìn)行一次更新。在JSX內(nèi)部,我們?nèi)匀辉?code>initialState上進(jìn)行映射。我們需要從下面的行中更改:

{initialState.map((todo) => (

改為:

{todos.map((todo) => (

現(xiàn)在我們應(yīng)該有一個工作的useReducer鉤子,它利用addTodo函數(shù)將操作分派給todoReducer。

添加完成的待辦事項

讓我們在這個項目中也有一個useffect的工作示例。每次簽出待辦事項時,我們都將更新document.title以顯示列表中已完成待辦事項的計數(shù)或數(shù)量。
addTodo()函數(shù)的正上方,讓我們添加邏輯來計算我們有多少已完成的todo。然后,當(dāng)document.title更改時,我們需要一個useffect方法來更新它:


    const completedTodos = todos.filter(todo => todo.complete);
    useEffect(() => {
      // inputRef.current.focus();
      document.title = `You have ${completedTodos.length} items completed!`;
    })

要做到這一點,我們還需要引入鉤子:

import React, { useReducer, useRef, useEffect } from 'react';

我們還沒有完成,我們現(xiàn)在需要添加一個事件,該事件將調(diào)用函數(shù),該函數(shù)將分派完成的任務(wù)。向div添加一個onClick處理程序,類名為todo name。


    <div className="todo-name" onClick={() => toggleComplete(todo.id)}>
      {todo.name}
    </div>

接下來,我們需要一個函數(shù)來處理這個點擊事件。它很簡單,只發(fā)送一個簡單的id和操作類型。將此添加到addTodo()函數(shù)的正下方:


    function toggleComplete(id) {
      dispatch({ type: 'TOGGLE_COMPLETE', id });
    }

最后,我們添加下面代碼到todoReducer中:


    case 'TOGGLE_COMPLETE': {
      return state.map((item) =>
        item.id === action.id
          ? { ...item, complete: !item.complete }
          : item
      )
    }

我還設(shè)置了一個樣式,我們將根據(jù)todo的完整值是否為true來添加或刪除該樣式。在todos.map代碼下面,讓我們更改如下所示的代碼行:


    <div key={todo.id} alt={todo.id} className="column-item">

改為:

<div className={`column-item ${todo.complete ? 'completed' : null}`}
  key={todo.id}>

我們不再需要alt屬性,所以我們刪除了它。現(xiàn)在,當(dāng)我們單擊todo時,它將分派一個操作,并將該特定todo的completed值設(shè)置為true,現(xiàn)在,我們的過濾器將通過useffect方法來獲取這個值,該方法反過來更新document.title。我們還將添加類名completed,并且完成的todo將變得不透明,以表示完成的todo。
在這個時候,除了delete功能,以及清除列表中所有todo的按鈕之外,我們幾乎所有的東西都在起作用。為了完成我們的演示,我們將重復(fù)我們已經(jīng)學(xué)到的使最后兩個功能工作的內(nèi)容。

刪除一個Todo項

首先,為todos HTML中的close圖標(biāo)添加onClick()事件:


    <div className="todo-delete" onClick={() => deleteTodo(todo.id)}>
      &times;
    </div>

我們將添加操作函數(shù)來處理這些操作,它不必是它們自己的函數(shù),我們可以直接從onClick()傳遞,或者我們可以設(shè)置一個類似的switch語句來處理所有分配。我們可以采取任何我們想要的方法。為了演示的目的,我想逐個添加它們。
現(xiàn)在我們創(chuàng)建一個函數(shù)來處理dispatch:


    function deleteTodo(id) {
      dispatch({ type: 'DELETE_TODO', id });
    }

現(xiàn)在我們只需在reducer的switch語句中添加一個case來處理reduce。在這里,我們使用數(shù)組的.filter()方法從列表中刪除一個滿足id的todo項并返回狀態(tài)。


    case 'DELETE_TODO': {
      return state.filter((x) => x.id !== action.id);
    }

清除所有Todos

對于清除todo操作沒什么特別的,我們只需要返回一個空數(shù)組。下面是實現(xiàn)這一點所需的三段不同的代碼。
onClick()添加到HTML按鈕:


    onClick={() => clearTodos()}

添加一個方法處理dispatch:


    function clearTodos() {
      dispatch({ type: 'CLEAR_TODOS' });
    }

在我們的reducer方法里添加一個case:


    case 'CLEAR_TODOS': {
      return [];
    }

Reducers總結(jié)

我們現(xiàn)在已經(jīng)使用useReducer構(gòu)建了Todo應(yīng)用程序的基礎(chǔ)。當(dāng)處理數(shù)據(jù)子級別稍微復(fù)雜一點的狀態(tài)時,此模式將非常有用。我們了解了純函數(shù)以及為什么它們是reducer的核心,允許我們返回可預(yù)測的狀態(tài),現(xiàn)在使用這種模式在核心React庫中更容易實現(xiàn)。

第五節(jié):自定義React Hooks

讓我們學(xué)習(xí)如何創(chuàng)建一個定制的React鉤子,以及使用鉤子時必須記住的所有規(guī)則。
Hooks只是功能!任何函數(shù)都可以成為Hooks。我覺得ReactJS文檔站點上的文檔不夠簡單。這不是敲打他們,我只是覺得,如果我能嘗試用更簡單的方式來解釋,更多的人會受益。

重溫 Effect Hook

如果您對基本Hooks有足夠的了解,可以直接跳到創(chuàng)建自定義Hooks。不必再回顧所有的基本Hooks,我想我們只需要重新訪問其中一個:useffect鉤子。我在閱讀ReactJS.org文檔的Hooks時了解到,有兩種方法可以使用useffect。無需清除的 effect和需要清除的effect。我希望在這個階段使用Hooks的任何人要么知道這些術(shù)語,要么花幾分鐘來讀一下官方文檔。
在類和Hooks可用之前,effect被放在許多生命周期方法中,比如:componentDidMount 或者 componentDidUpdate。如果在這兩種方法中都有重復(fù)的代碼(執(zhí)行相同的處理和更新效果),現(xiàn)在我們可以在功能組件中執(zhí)行這些操作,只需一個鉤子就可以完成。
useffect告訴React我們的組件需要在組件呈現(xiàn)之后做一些事情。它在第一次渲染之后和每次更新之后運行。在我之前的文章中,我只討論了沒有清理的副作用,所以我想很快地介紹如何允許功能組件在清理時產(chǎn)生副作用。
下面是一個示例,說明如何在不進(jìn)行任何清理的情況下運行useffect:


    useEffect(() => {
      document.title = `You clicked ${count} times`;
    });

如果確實需要清理才能運行,可以從useffect返回函數(shù)。這是可選的,它允許您在效果之后和任何新效果運行之前運行一些代碼。訂閱某些內(nèi)容的情況可能需要取消訂閱,作為效果清理過程的一部分。React將在卸載時執(zhí)行此清理。


    useEffect(() => {
      console.log("Subscribe to Something);
      return function cleanup() {
        console.log("Unsubscribe to Something);
      };
    });

以上效果將在每個渲染上運行一次以上。React在運行下一個渲染的效果之前清除上一個渲染的效果,這應(yīng)該注意。有關(guān)為什么在每次更新時都運行hook的解釋,請查看ReactJS文檔。不過,請記住,如果此行為導(dǎo)致性能問題,則可以選擇退出。
我們還可以通過使用可選參數(shù)跳過效果來優(yōu)化性能。例如,可能我們不想運行subscribe/unsubscribe效果,除非某些id已更改。看看下面的例子,了解如何做到這一點,這是相當(dāng)簡單的!


    useEffect(() => {
      console.log("Subscribe to Something);
      return () => {
        console.log("Unsubscribe to Something);
      };
    }, [props.something.id]); // only if something.id changes

Hooks,特別是useffect,現(xiàn)在允許您根據(jù)代碼正在做什么而不是它在什么生命周期方法中拆分代碼。當(dāng)我們只有類和生命周期方法時,我們有時不得不混合關(guān)注點?,F(xiàn)在,使用多個useffect方法,React可以按指定的順序應(yīng)用每個效果。這對于在應(yīng)用程序中組織代碼是一個巨大的好處。

創(chuàng)建自定義Hook

我真的很喜歡亞當(dāng)·拉基斯(Adam Rackis)最近在推特上發(fā)表的一篇文章:“Hooks的創(chuàng)作水平遠(yuǎn)遠(yuǎn)超過我們所看到的任何東西?!标P(guān)于Hooks,我想讓你了解的是,我們在類中看到的所有偉大的變化,以及我們?nèi)绾斡羞@么多的組合選項,現(xiàn)在Hooks中都有了這些。這意味著,現(xiàn)在當(dāng)涉及到React中功能組件的組成時,我們的手是不受約束的。對于React開發(fā)人員來說,這是一個巨大的進(jìn)步。
自定義鉤子就是JavaScript函數(shù),其名稱以單use作為前綴。自定義鉤子是一個普通函數(shù),但我們使用不同的標(biāo)準(zhǔn)。通過在開頭添加use這個詞,我們知道這個函數(shù)遵循Hooks的規(guī)則。
有了對Hook的更好理解,讓我們把我們知道的作為一段簡單的代碼,我們的文檔標(biāo)題更新,并創(chuàng)建一個簡單的自定義Hook。
似乎我們需要在幾個頁面上或在應(yīng)用程序中的許多不同功能組件中執(zhí)行某些操作。當(dāng)信息更改時,我們希望用某種類型的字符串更新文檔標(biāo)題。另外,我們不想在每個功能組件中重復(fù)這個邏輯。我們將從在同一頁面的本地將此代碼提取到鉤子開始,然后查看如何將同一鉤子導(dǎo)入到多個組件并共同定位。很簡單吧?
如果這是真的,那么我們的自定義鉤子也可以調(diào)用React Core基本鉤子之一,比如useffect。讓我們再檢查一次更新文檔標(biāo)題的功能組件。


    import React, { Component, useState, useEffect } from 'react';

    function Counter() {
      const [count, setCount] = useState(0);
      const incrementCount = () => setCount(count + 1);
      useEffect(() => {
        document.title = `You clicked ${count} times`
      });

      return (
        <div>
          <p>You clicked {count} times</p>
          <button onClick={incrementCount}>Click me</button>
        </div>
      )
    }

    export default Counter;

因此,我們希望在這里創(chuàng)建一個自定義鉤子,將一段文本傳遞到鉤子中,鉤子會為我們更新文檔標(biāo)題。首先讓我們看看創(chuàng)建此自定義掛鉤所需的代碼:


    const useDocumentTitle = (title) => {
      useEffect(() => {
        document.title = title;
      }, [title])
    }

在上面你可以看到我們需要這個鉤子作為參數(shù)的是一個字符串,我們稱之為title。在鉤子中,我們調(diào)用React Core的基本useffect鉤子,并設(shè)置標(biāo)題。useffect的第二個參數(shù)將為我們執(zhí)行該檢查,并且僅當(dāng)標(biāo)題的本地狀態(tài)與我們傳入的不同時才更新標(biāo)題。你的意思是,創(chuàng)建自定義鉤子和創(chuàng)建函數(shù)一樣簡單?是的,它的核心非常簡單,而且該函數(shù)可以引用任何其他鉤子。該死的…創(chuàng)建自定義鉤子比我們想象的要容易!
讓我們回顧一下我們的整體功能組件現(xiàn)在的樣子。你會看到我把useffect的舊調(diào)用注釋掉了,上面是我們?nèi)绾问褂米远x鉤子來代替它。


    import React, { Component, useState, useEffect } from 'react';

    const useDocumentTitle = title => {
      useEffect(() => {
        document.title = title;
      }, [title])
    }

    function Counter() {
      const [count, setCount] = useState(0);
      const incrementCount = () => setCount(count + 1);
      useDocumentTitle(`You clicked ${count} times`);
      // useEffect(() => {
      //   document.title = `You clicked ${count} times`
      // });

      return (
        <div>
          <p>You clicked {count} times</p>
          <button onClick={incrementCount}>Click me</button>
        </div>
      )
    }

    export default Counter;

讓我們進(jìn)一步清理一下,看看如果這個鉤子是由某個npm包提供的,而不是被復(fù)制粘貼在文件的頂部,我們可以如何使用它。


    import React, { Component, useState } from 'react';
    import useDocumentTitle from '@rehooks/document-title';

    function Counter() {
      const [count, setCount] = useState(0);
      const incrementCount = () => setCount(count + 1);
      useDocumentTitle(`You clicked ${count} times`);

      return (
        <div>
          <p>You clicked {count} times</p>
          <button onClick={incrementCount}>Click me</button>
        </div>
      )
    }

    export default Counter;

這真是太棒了,但我也希望您注意到,我不必在我的功能組件中導(dǎo)入useffect,因為我們從“@rehooks/document title”導(dǎo)入的鉤子會處理這個問題。所以如果我不需要useffect,我可以從組件導(dǎo)入中省略它。
我希望這說明了創(chuàng)建自定義React鉤子的基本原理,并且您甚至可以通過這樣一個簡單的例子看到它的威力。

管理KendoReact組件的控制狀態(tài)

Hooks非常適合處理特定類型的應(yīng)用程序狀態(tài)。例如控制狀態(tài)、本地組件狀態(tài)和會話狀態(tài)。在使用KendoReact UI(https://www.telerik.com/kendo-react-ui/)組件時,我想利用Hooks,但我想從簡單開始。我們將重構(gòu),不再使用類,而是使用函數(shù)組件。我們將查找演示使用this.statethis.setState的實例,因為當(dāng)我們將組件轉(zhuǎn)換為函數(shù)時,我們將不再需要使用this關(guān)鍵字,我們將不需要使用構(gòu)造函數(shù)或調(diào)用setState。
因此,讓我們開始重構(gòu)KendoReact演示,演示如何使用我們的KendoReact對話框。
如果您看看下面演示的main.jsx(https://stackblitz.com/edit/kendoreact-dialog-class-based?file=app/main.jsx)頁面,我們可以確定在使用功能組件和React鉤子時會發(fā)生變化的幾個目標(biāo)區(qū)域。用綠色突出顯示的代碼和行需要修改,用紅色突出顯示的行可以完全刪除。

image

  1. 在第6行有一個類定義,我們需要把它轉(zhuǎn)換成函數(shù)組件
  2. 第7行有一個構(gòu)造器,第8行調(diào)用了super()方法,第10行有一些綁定。在使用Hooks的函數(shù)組件中這些都是不需要的。
  3. 在第9行中,我們創(chuàng)建一個狀態(tài)實例,并給它一個默認(rèn)值true,這將是對useState鉤子的調(diào)用。
  4. 在第13行,我們需要重命名toggleDialog函數(shù)并將其切換到ES6箭頭函數(shù)樣式語法,第14行到第16行只需調(diào)用useState()賦值提供的更新方法setVisible,它將引用的值將是可見的,而不是this.state.visible
  5. 在第19行中,我們有一個render()調(diào)用,這在函數(shù)組件中是不必要的
  6. 在第22、23、26和27行我們提到了這一點。而this.state需要引用visibletoggleVisible而不是toggleDialog,稍后我將解釋為什么要重命名該函數(shù)。
    首先要做的就是將組件轉(zhuǎn)為函數(shù)組件,移除constructor構(gòu)造器,刪除supr()引用以及toggleDialog()函數(shù)綁定。這里可以使用多種語法選項,我更喜歡ES6箭頭函數(shù)樣式:
const multiply = (x, y) => { return x * y };

在我們的組件中,第6行現(xiàn)在看起來如下:


    const DialogWrapper = () => {

讓我們來設(shè)置一個鉤子來代替state對象。我們將不創(chuàng)建名為state的對象,而是設(shè)置對useState()的調(diào)用,并將其返回值解構(gòu)為一個變量,該變量將保存我們的狀態(tài)和更新/設(shè)置方法來更新該狀態(tài)。我們的狀態(tài)名稱將是可見的,其更新方法將被稱為setVisible。我們將刪除整個構(gòu)造函數(shù)并將其替換為這一行:

const [visible, setVisible] = useState(true);

因為我們使用的是useState()基本鉤子,所以還需要導(dǎo)入它。我們的React導(dǎo)入現(xiàn)在看起來像:


    import React, { useState } from 'react';

接下來,我們需要一個在這個組件中調(diào)用setVisible的函數(shù)來切換它的值。我們將其命名為toggleVisible,而不是toggleDialog,因為我們在一個功能組件中,所以之前使用的語法將不起作用。相反,我將更新為ES6箭頭函數(shù)樣式。此函數(shù)只需將可視狀態(tài)設(shè)置為與當(dāng)前狀態(tài)相反的狀態(tài)。


    const DialogWrapper = () => {;
      const [visible, setVisible] = useState(true);
      const toggleVisible = () => setVisible(!visible);

現(xiàn)在我們需要去掉render()塊及其兩個大括號。此外,我們需要刪除對this.toggleDialogthis.state.visible的所有引用,并相應(yīng)地將它們更改為toggleVisiblevisible?,F(xiàn)在在return()中,我們將進(jìn)行以下更改:


    return (
      <div>
      <Button className="k-button" onClick={toggleVisible}>Open Dialog</Button>
      {visible && <Dialog title={"Please confirm"} onClose={toggleVisible}>
        <p style={{ margin: "25px", textAlign: "center" }}>Are you sure you want to continue?</p>
        <DialogActionsBar>
        <Button className="k-button" onClick={toggleVisible}>No</Button>
        <Button className="k-button" onClick={toggleVisible}>Yes</Button>
        </DialogActionsBar>
      </Dialog>}
      </div>
    );

同樣,我們剛剛更新了return()中的代碼,以不引用this關(guān)鍵字并使用新的函數(shù)名toggleVisible
我們已經(jīng)成功地將KendoReact演示轉(zhuǎn)換為使用功能組件和基本useState掛鉤。讓我們使用一個叫做githrisk的很棒的工具來看看我們的整體變化是什么樣子的:

image

總結(jié)

我希望本指南能幫助您更好地理解Hooks的基礎(chǔ)知識,并允許您在這些示例的基礎(chǔ)上創(chuàng)建新的和令人驚奇的東西。如果它對你有用,請分享和傳播。
我想讓你對Hooks的創(chuàng)建有一個很好的理解,我認(rèn)為這可以通過回顧Sophie Alpert在React Conf 2018上的演講得到最好的解釋。
在過去,一些React開發(fā)人員在何時使用和何時不使用類方面遇到了困惑。這個問題可以追溯到幾年前,在一個案例中,丹阿布拉莫夫的一篇文章中寫道:如何使用React類在晚上睡覺。
盡管我們有時可能在當(dāng)前的React中使用它們,或者在將來處理遺留代碼時遇到它們,但這個問題現(xiàn)在正在處理中,我們已經(jīng)看到開發(fā)人員有很強(qiáng)的見解,并且大多使用功能組件。
當(dāng)談到React團(tuán)隊正在做些什么,以便更容易地構(gòu)建優(yōu)秀的UI,并改進(jìn)React中的開發(fā)人員體驗時,Sophie Alpert提出了一個很好的問題。
為什么React仍然很糟糕?
以下是React Conf 2018大會上著名演講的答案:

重用邏輯

在React hook之前,我們使用了很多高階組件和渲染道具來實現(xiàn)這一點,這將需要您在使用這些模式時經(jīng)常重新構(gòu)建應(yīng)用程序,并導(dǎo)致包裝地獄(末日金字塔風(fēng)格的嵌套)。

巨大的部件

由于在不同的生命周期方法中分割出不同的邏輯片段,我們的組件中經(jīng)常出現(xiàn)混亂。

混淆類

這是我將留給你的許多引語中的第一個引語。
課程對人類來說很難,但不僅僅是人類,課程對機(jī)器來說也很難——索菲·阿爾伯特
理解JavaScript中的類可能很棘手,而且在hook之前,還需要使用類組件來訪問狀態(tài)和生命周期方法。簡單地定義類組件需要相當(dāng)多的樣板文件。鉤子有助于解決這些問題,出于這個原因,我想留給你一些其他值得注意的引用,我們的無畏反應(yīng)和社區(qū)領(lǐng)袖!
Hooks允許您始終使用函數(shù),而不是在函數(shù)、類、HOC和渲染道具之間切換——Dan Abramov
如果你想讓世界變得更美好,看看React Hooks,然后做出改變。--邁克爾杰克遜
Hooks提供了一種處理React中問題的新方法——Dave Ceddia
有了React鉤子,我們擁有了兩個世界中最好的:可以使用狀態(tài)的干凈功能組件——David Katz
鉤子是React,是React要去的地方,是React V2——邁克爾杰克遜
React鉤子從根本上簡化了我創(chuàng)建、編寫、讀取和原型組件的方式——Zach Johnson
這些就是人們所說的關(guān)于React Hooks的一些事情。

如果你希望了解更多前端知識,請關(guān)注我的公眾號“前端記事本”

<img style="width: 300px; display: block; margin: 0 auto;" src="https://user-gold-cdn.xitu.io/2020/2/12/1703767fce4b2fd0?imageView2/0/w/1280/h/960/format/webp/ignore-error/1">

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容