React-Hooks(附demo)

文章首發於3月29日。html

hooks誕生的緣由

react的今天和明天系列文章中,react開發人員介紹了class組件存在的三個問題。react

  • 邏輯複用
  • 龐大的組件
  • 難以理解的class

在class組件中經過HOC和render props中來實現組件的邏輯複用。這會帶來一個問題,當咱們拆分出不少細小的組件再將它們組合到一塊兒,若是在chrome打開react的擴展,會發現組件層級很是深,增長了react的計算負擔。這個問題稱爲「包裹地獄(wrapper hell)」。git

當咱們試圖解決第一個問題的時候,咱們會將更多的邏輯放在單個的組件中,致使組件日益龐大。這就是第二個問題。github

js中的class其實是function的語法糖。在使用過程當中,它隱藏了function的實現細節(static, prototype等)。而且當咱們寫一個函數組件的時候,若是要添加狀態,那麼就必須將它轉換爲class組件,這會寫不少樣板代碼。不只如此,對於機器來講,壓縮後的class的組件中的全部的方法名都是沒有通過壓縮的。而且也沒法tree shaking。這是class組件的第三個問題。算法

Dan Abramov認爲這不是三個獨立的問題,而是一個問題的三個部分。爲了解決這個問題,因而出現了hooks。chrome

hooks簡介

hooks是另外一種書寫組件的方式,在hooks中只有函數而沒有class。經過hooks提供的一系列API幾乎能夠徹底覆蓋class組件中的狀況(爲何說是幾乎?在下面差別部分會提到)。hooks是更加簡潔優雅的,邏輯分離的。json

  • v16.8。hooks是react16.8版本新添加的功能。若是要使用hooks,須要確保react版本升級到16.8.0及以上,同時保證react-dom和react的版本保持一致。redux

  • 百分之百向後兼容數組

  • react沒有計劃移除class組件。hooks是一種可選的寫法,你依然能夠選擇不用而使用class(用過了你會喜歡上它)。瀏覽器

  • hooks中使用的依舊是class中的概念。

hooks API

hooks有下列API。全部的示例demo都在這裏。codesandbox.io/s/o968n1q62… ,能夠打開並直接看到效果。

Basic Hooks

  • useState
  • useEffect
  • useContext

Additional Hooks

  • useReducer
  • useCallback
  • useMemo
  • useRef
  • useImperativeHandle
  • useLayoutEffect
  • useDebugValue

接下來會解釋這些API並附有詳細的demo。

1.useState

useState是hooks中最基礎的一個API。其使用方式相似於class中的this.setState,不過又有所不一樣。下面是demo中的示例。

function Counter() {
  const [count, setCount] = useState(4);
  return (
    <>
      <p>
        count: {count}, random: {Math.random()}
      </p>
      <button onClick={() => setCount(count < 5 ? count + 1 : count)}>
        點擊這裏加1
      </button>
      <button onClick={() => setCount(count - 1)}>點擊這裏減1</button>
    </>
  );
}
複製代碼

useState(4)用於建立一個state變量,並傳入初始值。返回值是一個數組,經過解構寫法拿到返回的值。count是一個可變的值(只能經過setCount改變),它的初始值是useState傳入的參數4。單擊按鈕的時候,經過setCount去改變它的值。從而從新渲染該組件。

它與class組件中的setState的差別有如下幾點:

  • 不會進行狀態的合併,而是進行狀態的替換。這在簡單類型沒有什麼問題,對於複雜類型如對象使用的時候須要注意(能夠在demo中渲染Count2組件)。
  • 當使用setCount改變狀態時,使用Object.is算法比較狀態的先後值。它和 === 的區別在於對於0和-0的判斷,以及NaN的判斷。
  • 若是先後狀態相同,那麼組件不會進行渲染。在class組件中使用setState,即便某個狀態先後是相同的值,仍會進行渲染(demo中點擊按鈕讓count的值改變到5能夠看到)。

另外,若是useState的參數值須要複雜的計算才能獲得,那麼能夠傳入給useState一個函數,它僅在首次渲染時執行該複雜的計算函數。useState(() => expensiveCalc())

2.useEffect/useLayoutEffect

因爲useEffect和useLayoutEffect具備相同的使用方式,就放到一塊兒來介紹。 正如其名,useEffect用來處理反作用。反作用包括DOM的改變、訂閱、定時器、日誌等。反作用不容許放到函數體中。

useEffect/useLayoutEffect能夠取代componentDidMount、componentDidUpdate、componentWillUnmount三個聲明週期。因此它是很是強大一個hook,在使用方面很靈活,也不是那麼容易理解。

useEffect接受兩個參數,其中第二個參數是可選的,不過通常狀況下都須要傳入第二個參數。

useEffect(() => {
  // some side effect
  // ...

  // return a clean up function
  return () => {

  }
}, [])
複製代碼

第一個參數是一個函數,當組件首次渲染或者其依賴的狀態改變時它會執行。該函數的返回值是可選的,能夠不寫,若是要寫的話,必須是一個函數,用於清除上一個狀態。

第二個參數是可選的,它是一個數組。數組中能夠傳入狀態值(經過useState產生的值),當狀態值改變的時候首先會執行return函數,用於清理上一個狀態,而後useEffect中的函數就會再次執行。

  • 若是不傳入第二個參數,表明組件中任何狀態的改變該effect都會執行一次,這一般不是咱們想要的行爲。
  • 若是第二個參數傳遞一個空數組,表明該effect僅會執行一次,至關於componentDidMount。return函數也只會在組件卸載的時候執行一次,至關於componentWillUnmount。
  • 若是第二個參數數組中有一個或多個狀態(demo中的useLayEffect),那麼只要有任意一個狀態值發生變化,該effect都會再次執行。至關於componentDidUpdate。
// demo--useLayoutEffect
useEffect(() => {
  if (value.length > 10) {
    setValue(value.substring(0, 10));
  }
  setLengths(value.length);
},[value]);
複製代碼

當value發生變化的時候,effect再次執行。改變length狀態。

useLayoutEffect的語法和useEffect同樣。不一樣點在於:

  • useEffect是在組件狀態改變後,而且在組件layout和paint以後,也就是說組件出如今頁面後再進行調用。useLayoutEffect是在組件狀態改變後,可是在組件layout和paint以前,也就是在組件出如今頁面以前進行調用。
  • useEffect是異步的,useLayoutEffect是同步的。能夠看這篇文章:juejin.im/post/5c8f43…

在useLayoutEffect的demo中,嘗試將useEffect改變成useLayoutEffect,而後在輸入框輸入第十一個字符,能夠看到明顯差異。在大多數狀況下你應該使用useEffect。由於useEffect是異步的,不會堵塞主線程渲染。

3.useRef

在16.3版本中,引入React.createRef,來代替字符串ref。一樣,在hooks中,useRef也能夠取代createRef。能夠查看demo中的useRef。

function xxx() {
  const inputRef = useRef(null);
  useEffect(() => {
    inputRef.current.value = 'hello';
  }, [])
  return (
    <input type="text" ref={inputRef} />
  )
}
複製代碼

useRef接受一個初始值,返回一個可變的ref對象,ref.current指向初始化的值。它能夠指向別的值。

另外,因爲是函數組件,this再也不指向這個組件,因此若是要達到class組件中實例變量的效果,也能夠經過useRef來實現。

const timerRef = useRef(null);

useEffect(() => {
  timerRef.current = setInterval(() => {
    inputRef.current.value = 'hello';
  }, 1000);
  return () => {
    clearInterval(timerRef.current);
  };
}, []);
複製代碼

這裏的timerRef.current至關於class中的實例變量。

4.useContext

在React中,若是要將上層的屬性傳遞到下層,通常來講須要一層一層的傳遞。好比A->B-C->D。Context是一種數據傳遞機制,用於跨層級傳遞數據。好比在D組件能夠直接使用A組件的數據。能夠查看demo中的useContext。

useContext是Context.Consumer(16.3)以及static contextType(16.6)的一種簡寫。

上層組件定義Context.Provider,並傳入value屬性。在子組件中經過useContext(Context)能夠獲取value屬性。

// parent.js
const parentContext = createContext();
function Parent(props) {
  const countArr = useState({
    count1: 0,
    count2: 1
  });
  return (
    <parentContext.Provider value={countArr}>
      {props.children}
    </parentContext.Provider>
  );
}

function Child() {
  const countArr = useContext(parentContext);
  const [countObj, setCountObj] = countArr;

  return (
    <>
      <div>
        count1: {countObj.count1} count2: {countObj.count2}
      </div>
    </>
  );
}

<Parent><Child /></Parent>
複製代碼

能夠看到,這裏傳遞屬性不是經過props的,而是經過Context的。須要注意的是,在父組件定義了parentContext,須要將其導出,由於子組件使用useContext的參數就是parentContext。

另外,由於沒有static的限制,同一個組件中可使用多個useContext。也就是說,可使用多個上層組件傳遞來的數據。

5.useReducer

提到reducer,首先想到的應該是redux中的reducer。useReducer這個hook與redux中的reducer有所類似又有所不一樣。能夠查看demo中的useReducer。

定義一個reducer的方式和redux中是同樣的:

const initialState = {
  count: 0
};
const reducer = (state = initialState, action) => {
  switch (action.type) {
    case 'ADD': {
      return {
        count: state.count + 1
      };
    }
    case 'MINUS': {
      return {
        count: state.count - 1
      };
    }
  }
};


function Counter1() {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <>
      <div>count: {state.count}</div>
      <button onClick={() => { dispatch({ type: 'ADD' }); }}>+</button>
      <button onClick={() => { dispatch({ type: 'MINUS' }); }}>-</button>
    </>
  );
}
複製代碼

useReducer接受連個參數,一個是reducer,一個是initialState。返回一組值,分別是state和dispatch。經過dispatch觸發一個動做,進而去更新狀態。若是你使用過redux,那麼理解起來沒有任何困難。

另外,與redux中的reducer有所不一樣的是,useReducer中的reducer是獨立的。若是有多個組件使用到了同一個reducer,那麼它們之間的狀態是獨立的。相較於redux的全局共享狀態,它還依賴於react-redux提供的Provider組件。

因此是否是忽然想到了第四點中的Context,它提供了Provider。若是能配合useReducer,就能夠實現全局狀態共享了?確實如此!具體的代碼能夠查看demo中的context-reducer。

還有一點必須須要提到的是,因爲性能緣由,react-redux沒有推出官方的useRedux。 具體緣由能夠看這篇文章:juejin.im/post/5c7c8d… 。在最近的7.0的beta版本的發佈說明中,react-redux團隊宣佈將在7.x版本中推出useRedux API。

6.useImperativeHandle

在class組件中,若是父組件須要改變子組件的狀態,有兩種方式。一種是就是經過改變父組件state,該state做爲props傳給子組件,從而改變子組件的狀態。另外一種就是經過操做子組件的ref了。傳遞ref的方式主要有兩種,createRef和forwardRef,具體的就再也不細說。useImperativeHandle這個hook就是ref的另外一種寫法。之前是在父組件中拿到子組件元素的ref,直接操做ref表明的元素節點,至關因而直接操做子元素的dom元素。如今經過這個hook能夠在子組件中暴露一些API供父組件調用,而父組件是不能直接操做子組件的dom元素的。迪米特法則就是這樣描述的:一個類對它所調用的類的細節知道的越少越好。具體代碼能夠查看demo中的useImperativeHandle。

function Child(props) {
  const inputRef = useRef(null);
  useImperativeHandle(props.myref, () => ({
    focus() {
      inputRef.current.focus();
    },
    setValue(value) {
      inputRef.current.value = value;
    }
  }));
  return (
    <>
      <input type="text" ref={inputRef} />
    </>
  );
}
複製代碼

不過在實際開發中,你應該儘量經過傳遞props來改變子組件,經過ref來改變子組件是一種不推薦的方案。

7.useCallback

useCallback用來緩存一個函數。在函數式組件中可能有這樣一種狀況,父組件調用子組件,並將一個函數傳遞給子組件,假設子組件是使用了memo的(若是屬性值沒有變化,那麼將不會從新渲染)。具體代碼能夠查看demo中的useCallback。

function Parent() {
  //...
  const handleChange = () => { // ... }
  return (
    <>
      <p>count: {count}</p>
      <Child onChange={handleChange}/>
    </>
  )
}
複製代碼

當父組件中的count狀態改變時,這時候父組件從新渲染,子組件儘管沒有任何改變,但因爲onChange這個屬性是一個新的函數,它仍是從新渲染了。這是咱們不指望的行爲。在class組件中咱們每每經過onChange={this.handleChange}將函數傳遞給子組件。那麼下次父組件渲染時,this.handleChange是沒有變化的。若是子組件是memo的,那麼子組件將不會從新渲染。

因此uesCallback就是爲了解決這樣一個問題,它緩存一個函數,並接受一系列依賴項,返回一個函數。若是依賴項沒有變化,那麼返回的函數不會變化。

function Parent() {
  const [count, setCount] = useState(0);
  const [count2, setCount2] = useState(0);
  const [result, setResult] = useState(count);
  useEffect(() => {
    setInterval(() => {
      // setCount(prevCount => prevCount + 1);
      setCount2(prevCount => prevCount + 1);
    }, 1000);
  }, []);

  const handleChange = useCallback(() => {
    setResult(count + 1);
  }, [count]);
  // const handleChange = () => { setResult(count + 1) };

  return (
    <>
      <Counter count={count} />
      <Counter count={count2} />
      <Child onChange={handleChange} />
      <p>result: {result}</p>
    </>
  );
}
複製代碼

這裏的handleChange函數是被緩存了的,除非count發生變化,它纔會發生變化。經過demo打開控制檯,發現隨着count2的改變,子組件是不會從新渲染的。

8.useMemo

useMemo用來緩存一個複雜的計算值。 useCallback(fn, deps) 等價於 useMemo(() => fn, deps)。若是經過一個輸入獲得一個值須要通過複雜的計算,那麼下次一樣的輸入再進行一遍一樣複雜的計算是沒有必要的。這正是useMemo存在的意義。具體代碼能夠查看demo中的useMemo。

function Parent() {
  const [count, setCount] = useState(10);
  const [count2, setCount2] = useState(10);

  console.time('calc');
  const result = useMemo(() => computeExpensiveValue(40), [count]);
  // const result = computeExpensiveValue(count);
  console.timeEnd('calc');

  return (
    <>
      <p>result: {result}</p>
      <div>
        <input type="number" disabled value={count} />
        <button onClick={() => setCount(count + 1)}>+</button>
        <button onClick={() => setCount(count - 1)}>-</button>
        <br />
        <input type="number" disabled value={count2} />
        <button onClick={() => setCount2(count2 + 1)}>+</button>
        <button onClick={() => setCount2(count2 - 1)}>-</button>
      </div>
    </>
  );
}
複製代碼

useMemo接受一個函數,該函數涉及到複雜的計算,並接受一系列依賴項,返回一個計算後的值。若是依賴項沒有變化,那麼返回的值不會變化。這裏useMemo依賴於count的變化。在頁面中分別嘗試改變count和count2的值,觀察控制檯輸出的時間。改變count的時候,函數進行了重進計算,打印出的值比較大。改變count2的時候,直接使用緩存的值,打印出的值很小。

須要注意的是,react文檔中說明,useMemo只是做爲一種暗示,當依賴值變化時,並不必定能保證每一次都不計算。

9.useDebugValue

一般來講你不須要它。它只會存在於自定義的hooks中用來標誌一個自定義的hooks。當在chrome中打開react擴展的時候,若是一個組件使用到了自定義的hooks,而且該hooks使用到了useDebugValue,那麼該組件下方會顯示useDebugValue傳入的參數。

function useUserInfo() {
  // ...

  useDebugValue('use-user-info');
  return userInfo;
}
複製代碼

自定義hooks

除了官方提供的hooks之外,咱們也能夠定義本身的hooks。編寫自定義的hooks是很是簡單的。你能夠像和編寫一個正常的組件同樣,區別在於它返回的是數據(或者不返回),而不是jsx。使用自定義hooks須要遵循兩點。demo中編寫了一個自定義的hooks(/components/custom-hooks/use-user-info.js)。

  1. 自定義hooks以use開頭,駝峯命名。
  2. 自定義的hooks只能被其餘hooks或者函數組件使用。
const useUserInfo = (username = 'yuwanlin') => {
  const fetchRef = useRef(null);
  const [userInfo, setUserInfo] = useState({});
  const handleData = data => {
    setUserInfo(data);
  };
  useEffect(() => {
    const fetchData = username =>
      fetch(`${prefix}${username}`)
        .then(res => res.json())
        .then(data => {
          console.log('fetch success');
          handleData(data);
        });
    fetchRef.current = debounce(fetchData, 1000);
  }, []);

  useEffect(
    () => {
      fetchRef.current(username);
    },
    [username]
  );
  // useDebugValue('use-user-info');
  return userInfo;
};
複製代碼

打開use-user-info demo能夠看到頁面中出現了用戶的信息。

如何測試hooks

enzyme目前尚不支持測試hooksreact官方推出了測試hooks的方案。打開demo,在右邊選項卡中。從Browser切換到Tests,能夠看到經過了測試。全部位於__test__文件夾下的.test.js結尾的文件都會被當成測試文件。

hooks缺乏的部分

hooks目前不支持getSnapshotBeforeUpdate和componentDidCatch/getDerivedStateFromError生命週期,之後會加上。

一些常見的問題

因爲每一個函數就是一個組件,那麼整個函數體就至關於class組件中的render函數。每次狀態改變,都要從新執行函數。下面是一些常見的問題:

  • 如何保存上一個狀態?

在class組件中咱們經過實例變量來保存上一個狀態。在函數組件中,能夠經過ref來取代實例變量。usePrevious的實現以下:

function usePrevious(value) {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
}
複製代碼
  • 如何實現shouldComponentUpdate?

經過React.memo。

  • 在渲染的時候,因爲須要建立函數,hooks是否更緩慢?

不,在現代瀏覽器中,與類相比,閉包的原始性能沒有顯著差別,除了在極端狀況下。 此外,考慮到Hooks的設計在如下幾個方面更有效:

  1. 鉤子避免了類所需的大量開銷,例如在構造函數中建立類實例和綁定事件處理程序的成本。
  2. 使用Hooks的慣用代碼不須要深層組件樹嵌套,這在使用高階組件,渲染道具和上下文的代碼庫中很常見。 使用較小的組件樹,React的工做量較少。

更多常見的問題,能夠參照:reactjs.org/docs/hooks-…

hooks須要遵循的規範

  • hooks只能出如今函數做用域的頂級,不能出如今條件語句、循環語句中、嵌套函數中。
  • 只能從react的函數式組件以及自定義hooks中使用hooks。

代碼中使用hooks的時候,最好配合eslint插件eslint-plugin-react-hooks。

// Your ESLint configuration
{
  "plugins": [
    // ...
    "react-hooks"
  ],
  "rules": {
    // ...
    "react-hooks/rules-of-hooks": "error", // Checks rules of Hooks
    "react-hooks/exhaustive-deps": "warn" // Checks effect dependencies
  }
}
複製代碼

總結

class組件存在三個問題,邏輯複用、組件龐大、難以理解的class。hooks的存在就是爲了解決這三個問題的。而且在一個函數組件中,你能夠屢次使用相同的hooks。好比:

function Example() {
  const [count, setCount] = useState(0);
  const [count2, setCount2] = useState(0);

  useEffect(() => { // ... }, []);
  useEffect(() => { // ... }, []);

  return (
    // ...
  )
}
複製代碼

將不一樣的邏輯放到不一樣的hooks,邏輯更加清晰。hooks是class的另外一種寫法,在react16.8引入,react並不打算放棄class。

相關文章
相關標籤/搜索