初探React Hooks & SSR改造

Hooks

React v16.8 發佈了 Hooks,其主要是解決跨組件、組件複用的狀態管理問題。html

class 中組件的狀態封裝在對象中,而後經過單向數據流來組織組件間的狀態交互。這種模式下,跨組件的狀態管理變得很是困難,複用的組件也會由於要兼容不一樣的組件變得產生不少反作用,若是對組件再次拆分,也會形成冗餘代碼增多,和組件過多帶來的問題。react

後來有了 Redux 之類的狀態管理庫,來統一管理組件狀態。可是這種分層依然會讓代碼變得很複雜,須要更多的嵌套、狀態和方法,寫代碼時也常在幾個文件之間不停切換。hooks 就是爲了解決以上這些問題。git

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

 

面向對象編程和函數式編程

在瞭解 hooks 的過程當中,慢慢的感受到了面向對象和函數式編程的區別。編程

class 模式中狀態和屬性方法等被封裝在組件內,組件之間是相互以完整對象個體作交互,狀態的修改須要在對象內部的 setState 中處理。api

hooks 模式中,一切皆函數,也就是 hooks,能夠被拆分紅不少小單元再進行組合,修改狀態的是一個 set 方法,此方法能夠在任何其餘的 hooks 中出現和調用。 class 更屬於面向對象編程,而 hooks 更屬於函數式編程。數組

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

 

變化

使用 hooks 以後,本來的生命週期概念就會有所變化了。好比咱們定義一個 hooks 組件 Index, 當組件運行時,Index 函數的調用就是一次 render,那麼咱們第一次 render 至關於原來的 willMount,而 useEffect 會在第一次 render 之後執行。官網文檔也說過你能夠把 useEffect Hooks 視做 componentDidMountcomponentDidUpdate 和 componentWillUnmount 的結合。 state 也被 useState 替代,useState 傳入初始值並返回變量和修改變量的 set 方法。async

在咱們服務端渲染的時候,上篇文章說過生命週期只會執行到 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 執行。 

看以下代碼,當頁面首次 render ,useEffect 執行異步數據獲取,當數據獲取成功,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 改造。首先回顧 getInitialProps 在 class 模式下,是在 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 已經封裝了 constructor 和 componentWillMount 處理 stateprops,所以咱們只須要定義好靜態 state 和 getInitialProps,組件便會自動處理相關邏輯,大體使用代碼以下

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服務端渲染

關聯倉庫: https://github.com/zimv/react-ssr/tree/hooks

 

相關文章
相關標籤/搜索