React Hooks 實用指南

Monster000發表於2018-11-29

前言

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

原文連結

感謝閱讀 ?

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

相關文章