初探React Hooks & SSR改造

子慕大詩人發表於2019-03-28

Hooks

React v16.8 釋出了 Hooks,其主要是解決跨元件、元件複用的狀態管理問題。

class 中元件的狀態封裝在物件中,然後通過單向資料流來組織元件間的狀態互動。這種模式下,跨元件的狀態管理變得非常困難,複用的元件也會因為要相容不同的元件變得產生很多副作用,如果對元件再次拆分,也會造成冗餘程式碼增多,和元件過多帶來的問題。

後來有了 Redux 之類的狀態管理庫,來統一管理元件狀態。但是這種分層依然會讓程式碼變得很複雜,需要更多的巢狀、狀態和方法,寫程式碼時也常在幾個檔案之間不停切換。hooks 就是為了解決以上這些問題。

文章不對 hooks 做太多詳細介紹,建議閱讀此文前,先到官網做大概的瞭解。此文基於上一篇文章《實現ssr服務端渲染》的程式碼,進行 hooks 改造。程式碼已經提交到倉庫的 hooks 分支中,倉庫連結 github.com/zimv/react-…

物件導向程式設計和函數語言程式設計

在瞭解 hooks 的過程中,慢慢的感覺到了物件導向和函數語言程式設計的區別。

class 模式中狀態和屬性方法等被封裝在元件內,元件之間是相互以完整物件個體做互動,狀態的修改需要在物件內部的 setState 中處理。

hooks 模式中,一切皆函式,也就是 hooks,可以被拆分成很多小單元再進行組合,修改狀態的是一個 set 方法,此方法可以在任何其他的 hooks 中出現和呼叫。 class 更屬於物件導向程式設計,而 hooks 更屬於函數語言程式設計。

React 也並不會移除 class,而是引入 hooks 使開發者能根據場景做更好的選擇。它們依舊會在未來保持應有的迭代。

變化

使用 hooks 之後,原本的生命週期概念就會有所變化了。比如我們定義一個 hooks 元件 Index, 當元件執行時,Index 函式的呼叫就是一次 render,那麼我們第一次 render 相當於原來的 willMount,而 useEffect 會在第一次 render 以後執行。官網文件也說過你可以把 useEffectHooks 視作 componentDidMountcomponentDidUpdatecomponentWillUnmount 的結合。 state 也被 useState 替代,useState 傳入初始值並返回變數和修改變數的 set 方法。

在我們服務端渲染的時候,上篇文章說過生命週期只會執行到 willMount 後的第一次 render。 那在我們 hooks 模式下,服務端渲染會執行 Index hooks 第一次 render,而 useEffect 不會被執行。

function Index(props){
    console.log('render');
    const [desc, setDesc] = useState("惹不起");
    useEffect(() => {
        console.log('effect')
    })
    return (<div>{desc}</div>)
}複製程式碼

useEffect

如果使用了 useEffect,元件鉤子每次 render 以後,useEffect 會被執行。 useEffect 第一個入參是需要呼叫的方法,方法可以返回一個方法,返回的方法會在非首次執行此 useEffect 之前呼叫,也會在元件解除安裝時呼叫。

第二個引數是傳入一個陣列,是用來限制 useEffect 執行次數的,如果不傳入此引數,useEffect 會在每次 render 時執行。如果傳入第二個陣列引數,在非首次執行 useEffect 時,陣列中的變數較上一次 render 發生了變化,才會再次觸發 useEffect 執行。

看如下程式碼,當頁面首次 renderuseEffect 執行非同步資料獲取,當資料獲取成功,setList 設定值以後(類似 setState 會觸發 render),會再次執行 render,而 useEffect 還會再次執行,資料請求結束以後,setList 又會導致 render,因此陷入死迴圈。

const [list, setList] = useState([]);
useEffect(() => {
    API.getData().then(data=>{
        if (data) {
            setList(data.list);
        }
    });
});複製程式碼

所以需要使用第二個引數,限制執行次數,我們傳入一個 1,就可以實現僅執行一次 useEffect 。當然也可以通過傳入一個 useState 變數。

const [list, setList] = useState([]);
useEffect(() => {
    API.getData().then(data=>{
        if (data) {
            setList(data.list);
        }
    });
}, [1]);複製程式碼

class 改造

在原本的 SSR 倉庫的前提下,僅針對元件部分,進行 hooks 改造。首先回顧 getInitialPropsclass 模式下,是在 class 寫一個 static 靜態方法,如下:

export default class Index extends Base {
  static async getInitialProps() {
    let data;
    const res = await request.get("/api/getData");
    if (!res.errCode) data = res.data;
    return {
      data
    };
  }
}複製程式碼

hooks 中,class 變成了普通函式,以前的繼承變得沒有必要也無法適應需求,因此 getInitialProps 直接寫在函式的屬性中,方法本身返回的資料格式依然不變,返回一個物件。如下:

function Index(props) {
}
Index.getInitialProps = async () => {
  let data;
  const res = await request.get("/api/getData");
  if (!res.errCode) data = res.data;
  return {
    data
  };
};複製程式碼

包括定義的網頁 title,之前也是使用 static,現在我們也 Index.title = 'index' 這樣定義。

hooks 規範要求如下,援引中文 React 文件:

  • 只能在頂層呼叫鉤子。不要在迴圈,控制流和巢狀的函式中呼叫鉤子。
  • 只能從 React 的函式式元件中呼叫鉤子。不要在常規的 JavaScript 函式中呼叫鉤子。(此外,你也可以在你的自定義鉤子中呼叫鉤子。)

class 模式下,我們繼承了 BaseBase 會定義 constructorcomponentWillMount 來處理 stateprops ,可以幫助我們解決服務端渲染和客戶端渲染下初始化狀態資料的賦值和獲取,因此我們才可以統一一套程式碼在客戶端和服務端中執行(如需要,檢視上篇文章瞭解詳情)。

class 模式下的繼承 Base 屬於物件導向程式設計模式,而 hooks 模式下,由於需要在函式內使用 useState 來定義狀態,並且返回方法來設定狀態,這樣看起來更偏向函數語言程式設計,在這種場景下,繼承變得不適應。因此需要對 Base 進行改造,在 Base 編寫 hooks,在頁面元件 hooks 中使用。

class 模式下,我們使用繼承 Base 來處理 stateprops,由於 Base 已經封裝了 constructorcomponentWillMount 處理 stateprops,因此我們只需要定義好靜態 stategetInitialProps,元件便會自動處理相關邏輯,大致使用程式碼如下

export default class Index extends Base {
  static state = {
    desc: "Hello world~"
  };
  static async getInitialProps() {
    let data;
    const res = await request.get("/api/getData");
    if (!res.errCode) data = res.data;
    return {
      data
    };
  }
}複製程式碼

hooks 模式下不一樣,因為摒棄了繼承,需要用 Base 自定義 hooks,然後在頁面元件中使用。Base 中的 getPropsrequestInitialData 鉤子呼叫時,需要傳入當前 Index 元件的部分物件,然後在 Base hooks 中返回變數初始值值或者呼叫 set 修改當前 hooks 中的狀態值,大致使用如下:

import { getProps, requestInitialData } from "../base";

function Index(props) {
  const [desc, setDesc] = useState("Hello world~");
  //getProps獲取props中的ssrData,重構和服務端渲染時props有值,第三個引數為預設值
  const [data, setData] = useState(getProps(props, "data", ""));

  //在單頁面路由頁面跳轉,渲染元件時,requestInitialData呼叫getInitialProps
  requestInitialData(props, Index, { data: setData });
  return (<div>{data}</div>)
}
Index.getInitialProps = async () => {
  let data;
  const res = await request.get("/api/getData");
  if (!res.errCode) data = res.data;
  return {
    data
  };
};
export default Index;複製程式碼

如此封裝以後,我們依然保證了一套程式碼能在服務端和客戶端執行,requestInitialData 方法第三個傳入引數,是一個物件,傳入了需要被修改的狀態的 set 方法,最終 getInitialProps 返回資料後,會和傳入的物件對比,屬性名一致便會呼叫 set 方法進行狀態修改,requestInitialData 是一個 useEffect hook,程式碼如下


export function requestInitialData(props, component, setFunctions) {
  useEffect(() => {
    //客戶端執行時
    if (typeof window != "undefined") {
      //非同構時,並且getInitialProps存在
      if (!props.ssrData && component.getInitialProps) {
        component.getInitialProps().then(data => {
          if (data) {
            //遍歷結果,執行set賦值
            for (let key in setFunctions) {
              for (let dataKey in data) {
                if (key == dataKey) {
                  setFunctions[key](data[dataKey]);
                  break;
                }
              }
            }
          }
        });
      }
    }
  },[1]);
}複製程式碼

至此,針對我之前的 SSR 程式碼,就完成了 hooks 的改造。React hooks 的改造非常平滑,classhooks 混用也不會造成什麼問題,如果需要在舊的專案中使用 hooks 或者對原有的 class 進行改造,完全可以慢慢的一部分一部分迭代。當然 React Hooks 還有 useContext  useReducer 等,不妨現在就去試試 Hooks ?

關聯文章:《 實現ssr服務端渲染

關聯倉庫: github.com/zimv/react-…


初探React Hooks & SSR改造

關注大詩人公眾號,第一時間獲取最新文章。


相關文章