React Hooks 實用指南 頂 原

前言

React Conf 2018會議中,Dan Abramov 介紹了 React Hooks。官方的描述爲

Hook是一項新功能提案,可讓您在不編寫類的情況下使用狀態和其他React功能。 它們目前處於React v16.7.0-alpha中。計劃將在 2019 Q1 推出到主版本中。

痛點

以下是React Hooks功能的動機,它解決了現有React中的一些問題

組件之間很難共享狀態

React沒有一種將可重用的行爲附加到組件的方法(例如鏈接到store)。如果您使用過一段時間,您可能會使用render propsheight-order components組件解決這個問題。但是這些模式要求您再使用他們時重構組件,這會很麻煩。如果您使用React DevTools看一下您的程序,您會發現您的組件被各種組件所包裹這叫做包裝地域,比如:providers、comsumers、higher-order components、render props等。這裏有一個更深層的根本問題:React需要一個更好的方法來共享狀態邏輯。

這就是Hooks,您可以從組件中提取有狀態邏輯,以便可以獨立測試和重用。Hooks允許您不更改組件層次結構的情況下重用有狀態邏輯。這樣就可以輕鬆在多組件之間或與社區共享Hooks

組件越來越複雜,變得難以理解

我們經常不得不維護一些組件,這些組件一開始很簡單,隨着時間的延伸組件發展成一堆無法管理的有狀態邏輯和一些副作用。每個生命週期方法經常包含不相關的邏輯組合。舉個例子,組件可能會在componentDidMountcomponentDidUpdate中拉取一些數據。還有componentDidMount方法可能還包含一些事件監聽的不相關邏輯,並且再componentWillUnmount`中卸載監聽。但是完全不相關的代碼會合併到一個方法中。是很容易引起bug和不一致性。

在很多情況下,不能將這些組件拆分成更小的組件因爲邏輯遍佈許多地方。對它們進行測試也很困難。這正是很多人將React和狀態管理庫結合使用的原因。但是這更容易創建更多的抽象,要求您在許多不同的文件之間跳轉,重用組件將變得更加困難。

爲了解決這個問題,Hooks允許您根據相關的功能將他們拆分爲一個更小的函數。而不是強制基於聲明周期函數進行拆分。您還可以選擇使用reducer管理組件的本地狀態,使其更具可預測性。

類讓人和機器都混淆

除了使代碼重用和代碼組織更加困難外,我們發現類(classes)可能成爲學習React的一大障礙。您必須瞭解它在JavaScript中是如何工作的,這與它在大多數語言中的工作方式有很大不同。您必須明白如何正確的綁定事件處理和還沒穩定的新語法,代碼非常冗長。大家可能很容易就會明白屬性(props)、狀態(state)、從上往下的數據流(top-down data flow)但類(classes)就很難理解。React中的函數和類組件之間的區別以及何時使用每個組件導致即使在經驗豐富的React開發人員之間也存在分歧。使用函數可以使用prepack更好的優化代碼。但是使用類組件不能得到更好的優化。

爲了解決這些問題,Hooks 允許您在沒有類的情況下使用更多的React功能。

useState

useState可以讓您的函數組件也具備類組件的state功能

使用語法如下:

const [state, setState] = useState(initialState);

useState返回一個數組,一個是state的值,第二個是更新state的函數

在真實的程序中我們可以這樣使用:

function TestUseState() {
  const [count, setCount] = React.useState(0);

  return (
    <div>
      <p>useState api</p>
      <p>Count: {count} <button onClick={() => setCount(count + 1) }>自增</button></p>
    </div>
  )
}

使用 useState 需要注意一個事項,當你初始化是一個對象時。使用 setCount 時它不像類組件的 this.setState 會自動合併到 state 中。setCount 會使用當前的值覆蓋之前的 state。如下所示

function TestUseStateObject() {
  const [state, setState] = React.useState({
    count: 0,
    greeting: "Hello, World!",
  });
  const handleAdd = () => {
    setState({
      count: state.count + 1
    })
  }
  console.log('state > ', state)
  return (
    <div>
      <p>useStateObject api</p>
      <p>Count: {state.count} <button onClick={handleAdd}>自增</button></p>
    </div>
  )
}

1543423038609

我們可以看到,當點擊按鈕時 state 被替換成了 {count: 1}。如果想要在 state 中使用一個對象需要在更新值的時候把之前的值解構出來,如下所示:

setState({
      ...state,
      count: state.count + 1
    })

在函數中使用多個 state

function TestMultipleUseState() {
  const [count, setCount] = React.useState(0);
  const [name, setName] = React.useState('john');
  return (
    <div>
      <p>useState api</p>
      <p>Count: {count} - Name: {name}</p>
    </div>
  )
}

如需要在線測試請前往codepen useState

useEffect

默認情況下 useEffect 在完成渲染後運行,我們可以在這裏獲取DOM和處理其他副作用。但它還有兩種不同的運行階段稍候我會解釋。

function TestUseEffect() {
  const [count, setCount] = React.useState(0);
  
  React.useEffect(() => {
    console.log(`組件被更新,Count: ${count}`);
  });
  
  return (
    <div>
      <p>useEffect api</p>
      <p>Count: {count} <button onClick={() => setCount(count + 1) }>自增</button></p>
    </div>
  )
}

上面的 useEffect 在每次組件渲染後運行,每當我們點擊自增按鈕都會執行一次。

但是如果上面的代碼在每次渲染後都執行,如果我們在 useEffect 從服務器拉取數據。造成的結果就是每次渲染後都會從服務器拉取數據。或者是隻有某些 props 被更新後纔想執行 useEffect。那麼默認的 useEffect 就不是我們想要執行方式,這時 useEffect 提供了第二個參數。

useEffect(didUpdate, [])

useEffect第二個參數爲一個數組。當我們提供第二個參數時,只有第二個參數被更改 useEffect 纔會執行。利用第二個參數我們可以模擬出類組件的 componentDidMount 生命週期函數

function TestUseEffectListener() {
  const [count, setCount] = React.useState(0);
  
  React.useEffect(() => {
    console.log('componentDidMount fetch Data...');
  }, []);
  
  return (
    <div>
      <p>TestUseEffectListener</p>
      <p>Count: {count} <button onClick={() => setCount(count + 1) }>自增</button></p>
    </div>
  )
}

上面的代碼中 useEffect 只會執行一次,當您點擊自增 useEffect 也不會再次執行。

useEffect 第一個參數的函數中我們可以返回一個函數用於執行清理功能,它會在ui組件被清理之前執行,結合上面所學的知識使用 useEffect 模擬 componentWillUnmount 生命週期函數

function TestUseEffectUnMount() {
  const [count, setCount] = React.useState(0);
  
  React.useEffect(() => {
    return () => {
      console.log('componentUnmount cleanup...');
    }
  }, []);
  
  return (
    <div>
      <p>TestUseEffectUnMount</p>
    </div>
  )
}

上面的代碼中,當組件 TestUseEffectUnMount 將要銷燬時會,會執行 console.log('componentUnmount cleanup...') 代碼

如需要在線測試請前往codepen useEffect

useContext

useContext 可以讓您在函數中使用 context,它有效的解決了以前 ProviderConsumer 需要額外包裝組件的問題

使用語法如下:

const context = useContext(Context);

現在讓我們來看看實際應用中這個 useContext 是如何使用的,代碼如下:

function TestFuncContext() {
  const context = React.useContext(ThemeContext);

  return (
    <div style={context}>TestFuncContext</div>
  )
}

我們可以看到上面直接使用 React.useContext(ThemeContext) 就可以獲得 context,而在之前的版本中需要像這樣才能獲取 <Consumer>({vlaue} => {})</Consumer> ,這極大的簡化了代碼的書寫。

// 之前Consumer的訪問方式
function TestNativeContext() {
  return (
    <ThemeContext.Consumer>
      {(value) => {
        return (
          <div style={value}>TestNativeContext</div>
        )
      }}
    </ThemeContext.Consumer>
  );
}

如需要在線測試請前往codepen useContext

useReducer

useReduceruseState 的代提方案。當你有一些更負責的數據時可以使用它。

使用語法如下:

const [state, dispatch] = useReducer(reducer, initialState)

第一個參數是一個 reduce 用來處理到來的 action,函數申明爲:(state, action) => ()。第二個參數是一個初始化的state常量。

在返回值 [state, dispatch] 中,state 就是你的數據。dispatch 可以發起一個 action 到 reducer 中處理。

這個功能給我的感覺就是組件本地的redux,感覺還是不錯。在設計一些複雜的數據結構是可以使用

現在讓我們來看看實際應用中這個 useReducer 是如何使用的,代碼如下:

function TestUseReducer() {
  const [state, setState] = React.useReducer((state, action) => {
    switch(action.type) {
      case 'update':
        return {name: action.payload}
      default:
        return state;
    }
  }, {name: ''});
  
  const handleNameChange = (e) => {
    setState({type: 'update', payload: e.target.value})
  }
  return (
    <div>
      <p>你好:{state.name}</p>
      <input onChange={handleNameChange} />
    </div>
  )
}

當改變 input 中的值時會同時更新 state 中的數據,然後顯示在界面上

如需要在線測試請前往codepen useReducer

useCallback

useCallbackuseMemo 有些相似。它接收一個內聯函數和一個數組,它返回的是一個記憶化版本的函數。

使用語法如下:

const memoizedValue = useMemo(() => computeExpensiveValue(a), [a])

useCallback 的第一個參數是一個函數用來執行一些操作和計算。第二個參數是一個數組,當這個數組裏面的值改變時 useMemo 會重新執行更新這個匿名函數裏面引用到 a 的值。這樣描述可能有點不太好理解,下面看一個例子:

function TestUseCallback({ num }) {
  const memoizedCallback = React.useCallback(
    () => {
      // 一些計算
      return num;
    },
    [],
  );
  console.log('記憶 num > ', memoizedCallback())
  console.log('原始 num > ', num);
  return (
    <div>
      <p>TestUseCallback</p>
    </div>
  )
}

_6d371a9a-47a9-4a5c-ac22-28a456b4d4d5

如果我們想監聽 num 值的更新重新做一些操作和計算,我們可以給第二個參數放入 num 值,像下面這樣:

function TestUseCallback({ num }) {
  const memoizedCallback = React.useCallback(
    () => {
      // 一些計算
      return num;
    },
    [num],
  );
  console.log('記憶 num > ', memoizedCallback())
  console.log('原始 num > ', num);
  return (
    <div>
      <p>TestUseCallback</p>
    </div>
  )
}

如需要在線測試請前往codepen useCallback

useRef

我覺得 useRef 的功能有點像類屬性,或者說您想要在組件中記錄一些值,並且這些值在稍後可以更改。

使用語法如下:

const refContainer = useRef(initialValue)

useRef 返回一個可變的對象,對象的 current 屬性被初始化爲傳遞的參數(initialValue)。返回的對象將持續整個組件的生命週期。

一個保存input元素,並使其獲取焦點程序,代碼如下:

function TestUseRef() {
  const inputEl = React.useRef(null);
  const onButtonClick = () => {
    // 點擊按鈕會設置input獲取焦點
    inputEl.current.focus(); // 設置useRef返回對象的值
  };
  
  return (
    <div>
      <p>TestUseRef</p>
      <div>
        <input ref={inputEl} type="text" />
        <button onClick={onButtonClick}>input聚焦</button>
      </div>
    </div>
  )
}

useRef 返回的對象您可以在其他地方設置比如: useEffect、useCallback等

如需要在線測試請前往codepen useRef

原文鏈接

感謝閱讀

最後做一個廣告,我創建了一個前端週刊每週五發布最新的技術文章和開源項目歡迎訂閱

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