一文告訴你 Why React Hook

  使用了將近一週的 react Hook,期間嘗試將項目中原有的class component改造成Hook,比較Hookclass的區別,得出一些個人的思考與見解。

什麼是Hook

   react官網上面對 Hook 是這樣描述的。

Hook 是 React 16.8 的新增特性。它可以讓你在不編寫 class 的情況下使用 state 以及其他的 React 特性。

Hook提供了react中函數式組件操作state,響應state的能力。

  最簡單的todo,一個函數式組件實現功能一個按鈕點擊增加計數,一個p標籤來同步顯示計數的更新。不用Hook的情況下我們需要依賴class component來進行外部props的更新。

import React, { Component } from 'react';

function Demo({ num, addNum }) {
  return (
    <>
      <p>{num}</p>
      <button onClick={addNum}>點我增加</button>
    </>
  );
};

class UseDemo extends Component {
  state = { num: 0 };

  addNum = () => {
    this.setState({ num: this.state.num + 1 });
  };

  render() {
    return <Demo num={this.state.num} addNum={this.addNum} />;
  }
}

  使用Hook來進行對同樣的todo來進行改造。

import React, { useState } from 'react';

function Demo() {
  const [num, setNum] = useState(0);
  return (
    <>
      <p>{num}</p>
      <button onClick={() => setNum(num + 1)}>點我增加</button>
    </>
  );
};

Hook提供了函數式組件類似於生命週期的方法

  在Hook之前的設計中,函數式組件的更新由props變化決定,自行完成更新到視圖。我們先來不用Hook寫一個倒計時功能。

import React, { Component } from 'react';

function Demo({ num }) {
  return <p>{num === 0 ? '倒計時結束' : num}</p>;
};

class UseDemo extends Component {
  timer = null;

  state = { num: 60 };

  render() {
    return <Demo num={this.state.num} />;
  }

  componentDidMount() {
    this.timer = setInterval(() => {
      if (this.state.num === 0) {
        clearInterval(this.timer);
        this.timer = null;
      } else {
        this.setState({ num: this.state.num - 1 });
      }
    }, 1000);
  }

  componentWillUnmount() {
    if (this.timer) {
      clearInterval(this.timer);
    }
  }

}

  我們完成了一個60秒倒計時的功能。在 UseDemo didMount 的時候生成一個60秒的計時器。爲了防止用戶在倒計時結束前退出當前組件渲染,在componentWillUnmount的時候,如果計時器還在計時,把它清空掉。

  我們使用Hook+函數組件來完成相同的功能。

function useIntervalCountDown(countNum) {
  const [num, setNum] = useState(countNum);
  const [timer, setTimer] = useState(null);
  useEffect(() => {
    if (num === 0 && timer) {
      clearInterval(timer);
    }
    if (!timer) {
      let timeId = setInterval(() => {
        setNum(num - 1);
      }, 1000);
      setTimer(timeId); 1000);
    }
    return () => {
      if (timer) {
        clearInterval(timer);
        setTimer(null);
      }
    };
  }, [num, timer]);
  return num;
}

function Demo() {
  const num = useIntervalCountDown(60);
  return <p>{num === 0 ? '倒計時結束' : num}</p>;
};

  我們使用useEffect來完成componentDidMountcomponentWillUnmount生命週期的模擬。useEffect接收兩個參數,第一個參數爲函數,第二參數是一個數組。數組中存在的變量變化時,useEffect會觸發第一個入參的函數。第一個入參函數可以設置一個返回的函數值,這個函數將在組件取消掛載的前執行(近乎相當於componentWillUnmount)。

  Hook本質上就是給函數式組件提供各種類組件的能力。讓你像寫class Component一樣來寫functional Component。但是通過上面兩個例子可以發現,Hook改造前後,代碼量並沒有減少多少,那麼我們到底爲什麼需要react Hook。

Hook解決了什麼問題

class component 邏輯複用不方便

  舉一個最經常寫的後臺管理系統頁面的例子。如下圖:

常見的後臺管理系統頁面

  圖中可以看到一個table呈現各個詳情資料。然後最後一列是各種操作按鈕。這種模式的頁面一般會呈現在點擊左側菜單欄後出現,在一個後臺管理系統會出現很多次。事實上,這些頁面除了請求接口(url,入參)以及表格呈現(表格的標題,渲染邏輯)不同,其它有很多邏輯是相同的。比如:

  1. componentDidMount之後,請求表格的內容接口,設置到state。
  2. 翻頁,改變頁面尺寸,改變入參拉取請求列表。
  3. 請求列表前後,開啓表格loading。

  在class component模式下,如果想要複用這部分的邏輯,操作到組件內部的state,只能使用繼承的方式。

import React, { Component } from 'react';

// 僅僅舉例
export class BaseTableComponent extends Component {
  // 拉取請求列表邏輯
  fetchList = async () => {
    const {
      url,
      params,
    } = this.getRequestParams(); // 繼承子類自己內部實現
    const { list, total } = await fetch(url, params);
    this.setState({ tableList: list, total });
  };

  // 翻頁邏輯
  handlePageChange = (current, size) => {
    this.setState({ current, size }, () => {
      this.fetchList();
    });
  };

  // ... 省略其它組件複用的邏輯
  componentDidMount() {
    this.fetchList();
  }
}

  在上面簡單實現了一個抽象類BaseTableComponent,在這個類中實現了拉取接口部分邏輯的抽取,列表翻頁邏輯的抽取。接下來寫頁面組件的時候,想要實現這部分邏輯都需要繼承這個類。

import { BaseTableComponent } from './BaseTableComponent';

export default class UserPage extends BaseTableComponent {
  getRequestParams = () => {
    return {
      url: '/demo/',
      params: { page: 1, size: 10 },
    };
  };
  render() {
    const { list, total, current, size } = this.state;
    return (
      <Table
        dataSource={list}
        pagination={{
          total,
          current,
          size,
          onChange: this.handlePageChange,
          pagination: this.handlePageChange,
        }}
      />
    );
  }
}

  這樣子複用模式在實際項目中會帶來比較多的兩個問題:

  1. 複用邏輯組合複用不方便。
    比如我所有頁面都用到了input搜索請求頁面列表的邏輯,而單單頁面A,B沒有用到,我抽象出來的方法,在頁面AB組件中就存在冗餘。extends class的繼承模式不能很好的解決這個問題。

  2. 複用邏輯必須一直關注父類用到的state。
    因爲父類幫你抽象出來操作state的邏輯,因此,這部分佔用的state(比如list,total)在所有子類的方法中,都不能再使用了。隨着抽象的公用的邏輯越來越多,父類維護操作的state也會越來越多,需要關注不能使用的state也就越來越多。

  Hook能很好的解決這個問題。函數式的組件和state調用方法可以很方便的排列組合給需要的功能。

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

export function useFetch({ url, params }) {
  const [list, setList] = useState([]);
  const [total, setTotal] = useTotal(0);
  useEffect(() => {
    fetch(url, params)
      .then(({ total, list }) => {
        setList(list);
        setTotal(total);
      })
  }, [params]);
  return { list, total };
}

// 這裏爲了舉例簡單不引入 useCallback 等渲染更新優化的邏輯
// 頁面組件使用抽象的組件邏輯
export default () => {
  const [current, setCurrent] = useState(1);
  const [size, setSize] = useState(10);
  // 引入請求列表邏輯
  const { list, total } = useFetch({ url, params: { current, size } });
  return (
    <Table
      dataSource={list}
      pagination={{
        total,
        current,
        size,
        onChange: (current, size) => {
          setCurrent(current);
          setSize(size);
        },
      }}
    />
  );
};

  Hook給予了函數組件操作state,以及使用類似於class component生命週期的能力。函數式組件本身高度靈活,可以拆卸複用各種小功能,而不會像class一樣產生冗餘。

Hook實現邏輯的高聚合

  回到開頭第二個計時器的例子。在使用class component來實現計時器的時候,在componentDidMountcomponentWillUnmount中分別進行了setIntervalclearInterval的操作。這就是class component的第二個缺點,有時候我們實現一個功能,需要把邏輯分散在多個生命週期當中。當外部的prop會和內部同步更新時我們還要帶上getDerivedStateFromProps的生命週期方法。使得組件在後期的維護上存在很重的負擔。接手代碼的同學需要貫穿整個react數個生命週期方法才能明白你的一個數據處理邏輯。

  在計時器的例子中,我們抽取了useIntervalCountDown方法,把num, 和操作num的setNum邏輯放在一個函數裏面,貫穿在一起。無論是讀代碼邏輯的連貫性,還是代碼的聚合性都在一起,在維護度上的提升不是一星半點。

最後一點

  其實Hook加入對react社區建設的意義也是非常積極的。Hook鼓勵你對數據以及操作數據的邏輯進行提取.既然你在日常工作中已經提取了不少邏輯,何不發佈到社區當中進行開源.實際上react Hook發佈之後,react社區的其它核心組件包諸如react-routerreact-redux都立即響應使用React Hook進行了包的更新編寫。擁抱Hook的速度足以證明react Hook的積極意義。

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