讓咱們坐上 Hooks 的托馬斯小火車

在 React 16 中,除去 Fiber 架構外,Hooks 是最使人激動的一個特性,相比於 class component,Hooks 加持後的 function component 在寫法與思路上都大有不一樣,不少時候顯得更爲簡潔與清爽(熵更低,弱化生命週期的概念),同時解決了使人煩惱的 this 指針指向問題,仍是很香的。react

但理性來講,到目前爲止,hooks 仍是一個坑不少的階段,而且也缺少一個成體系的最佳實踐,如下談談我對 hooks 的一些淺薄的認識。git

那麼,是時候發車了。github

經常使用Hooks

useState

咱們能夠把這裏的 state 看作咱們在 class component 中使用的 this.state。ajax

咱們的每一次 setState 操做,在改變了值以後,都會引起 rerender 操做,從而觸發頁面的更新(可是若是沒有改變的話,則不會觸發 rerender,咱們在後面將會利用這一特性作一件有趣的事情)。編程

同時,setState 能夠以函數做爲參數,這個時候咱們能夠獲取到最新的 state 值(在第一個回調參數)。redux

import React, { useState } from 'react';

function App() {
    const [ state, setState ] = useState(0);
    return (
        <span>{state}</span>
    )
}
複製代碼

useEffect

useEffect能夠說是全部 hooks API 中最像是聲明週期的鉤子了,很容易讓人理解成爲,若是依賴數組爲空,那麼它等價爲 componentDidMount,可是真的這樣嗎?數組

咱們能夠這樣去理解咱們的函數組件,函數組件的每次運行都至關於 class component 中的一次 render,每輪都會保留它的閉包,因此,咱們的 useEffect 實際保留了它運行輪次的 state 和 props 狀態(若是依賴不更新,那麼狀態不更新),這也就是 useEffect 和 componentDidMount 生命週期的關係。緩存

import React, { useEffect } from 'react';
function App() {
    useEffect(() => {
        console.log('I am mount');
        return () => {
            console.log('before next run, I am cleaned');
        }
    }, []);
複製代碼

useLayoutEffect

useLayoutEffect 與 useEffect 的不一樣在於,useLayoutEffect 會在 DOM 渲染以前執行,而 useEffect 會在 DOM 渲染以後執行,因此咱們能夠利用這個特性,避免一些因爲 DOM 渲染以後進行操做致使的白屏問題。性能優化

useCallback

useCallback 能夠幫助咱們緩存函數(useMemo一樣能夠作到,寫法不一樣),經過手動控制依賴,作到減小由於函數的更新致使子組件的更新(帶來的性能問題很是明顯)antd

import React, { useCallback } from 'react';
function App() {
    const cb = useCallback(() => { console.log('callback') }, []);
    return (
        <button onClick={cb}></button>
    )
}
複製代碼

useMemo

useMemo 能夠爲咱們的 function component 提供緩存的能力,在一些重計算的場景下,能夠減小重複計算的次數,起到明顯的性能提高。 固然,useMemo一樣能夠用來緩存組件,起到相似與 class component 中 shouldComponentUpdate 的做用,讓咱們手動經過管理依賴的方式作到控制子組件的更新(固然這個手動管理的成本是很是高的)

useRef

由於在 hooks 中,咱們所聲明的全部變量是隻屬於它的閉包的,因此,咱們沒法作到變量的一個共享。由於 immutable 的 state 並不適合咱們存儲一些不參與 UI 顯示的變量。hooks 爲咱們提供了 useRef 去存儲 mutable 的不參與 UI 顯示的變量,而且能夠在每一輪 render 中共享。

useRef 不只能夠用來存儲對 Dom 元素的引用(它的本意),更能夠用來存儲咱們須要在每輪 render 中共享的 mutable 的變量(可能很是經常使用)。

import React, { useRef } from 'react';
function App() {
  const td = useRef(1);
  console.log(td.current); // 1
  ...
複製代碼

useReducer

在當前版本中的 useReducer 事實上是對 useState 的一層封裝,實現了 redux 的一套原理(以前的版本是 useState 是對 useReducer 的一層封裝)

function useReducer(reducer, initialState) {
  const [state, setState] = useState(initialState);

  function dispatch(action) {
    const nextState = reducer(state, action);
    setState(nextState);
  }

  return [state, dispatch];
}
複製代碼

useContext

假定咱們已經有了一個 Context ,而且咱們的子組件已經在 Provider 包裹下,咱們能夠直接使用 useContext 去獲取值,而非使用回調去獲取值。 同時,咱們也能夠對某些 Context 進行 useContext 的封裝,讓咱們能夠在不一樣的組件中方便的使用 Context 中的數據。

// 假定咱們已經有 Context
function Child(props) {
    const { value } = useContext(Context);
    return (
      <div> {value} </div>
    )
}
複製代碼

咱們能夠將 Context 、useReducer 與 useContext 結合起來,打造咱們本身的 Redux

const CTX = React.createContext(null);
const reducer = (state, action) => {
    switch(action.type) {
        default:
            reutrn state;
    }
}
const Context = function({ children }) {
    const [state, dispatch] = useReducer(reducer, {});
    return (
        <CTX.Provider value={ state, dispatch }> {children} </CTX.Provider> ) } 複製代碼

怎麼理解Hooks中的狀態

咱們應該樹立一個理念,在 function component 中,全部的狀態,都是隸屬於它的閉包的,因此致使了 咱們每一輪的 Render 都會有本身的一個閉包,全部的 useEffect 與 useLayoutEffect 都在其最後一次更新的閉包中 Hooks處理請求

正確處理依賴

hooks 編程有些相似於響應式編程,同時,爲了能夠老是拿到最正確的值,正確的去書寫 hooks依賴 是很是重要的,也就是所謂的對依賴誠實,這樣才能保證咱們最終發送請求之時,能夠取到正確的 state 和 props。

放置依賴的請求於 useEffect 中

爲了可以正確的處理請求,有一種想法是——將請求的函數放置於 useEffect 中,這樣子就能夠確保咱們每時每刻都會去正確的處理其中的依賴問題。 處理競態 咱們知道,在 hooks 裏,每一次 Render 以及 每一次 useEffect 的執行都是在它本身所處輪次的閉包中,因此,咱們處理競態的一個思路就來源於這裏。

咱們的依賴變化會觸發咱們的 ajax 操做,因此當第二次請求發生時,實際上上一次 effect 已經到了清理反作用時期,因此執行了 return 中的函數,將咱們的flag置爲true,這樣,當咱們的請求返回之時,其effect 所在的閉包是能夠感知到執行結束的狀態的,從而拋棄舊值,達到對競態的正確處理。

useEffect(() => {
    let flag = false;
    ajax().then(res => {
        if (!flag) {
            //...do something
        }
    })
    return () => {
        flag = true;
    }
}, [deps])
複製代碼

請求與觸發分離

請求

咱們能夠將請求函數用普通函數的方法,放置於整個 function 中,這樣足以確保咱們這個函數可以拿到當前 render 輪次所依賴的 state 和 props,若是有性能方面的顧慮,能夠考慮使用 useCallback 去進行包裝(但此時必定要對依賴誠實)

function App() {
    const [flag, setFlag] = useState(0);

    const ajax = () => {
       _ajax(props)
    };

    useEffect(() => {
        ajax();
    }, [flag]);

    return (
        ...
    )
}
複製代碼

觸發

這個時候必定要注意的一點是,咱們的觸發 flag,必定要在最後修改(先進行預操做——其它的 state 修改),肯定咱們的 effect 更新時,索引用的,是最新的 ajax 請求函數。

非渲染參數使用 ref 進行保存 由於咱們在 effect 中,永遠能夠正確的獲取到 ref 值,因此,當咱們的參數不參與渲染時,咱們能夠用 useRef 生成的 ref 對其進行管理,這樣咱們就能夠不用去擔憂因爲 ref 所引用參數的變化問題(同時,也不會觸發頁面的 rerender)

const name = useRef('小明')
const ajax = useCallback(() => {
    ajax({ name })
}, []);

// 修改 param 直接操做 ref
name.current = '123';
複製代碼

極限性能Trick

減小計算

依賴數組欺騙

利用 setState 的回調處理獲取 state 的問題 由於

const [state, setState] = useState(0);

// 利用 setState 的回調拿到最新的 state,返回原值,能夠不觸發 rerender(極端狀況下能夠用於性能優化)
const update = useCallback(() => {
    setState(state => {
        // 作你想作的任何事情
        return state;
    })
}, []);
複製代碼

試想一個很騷的場景,若是咱們使用 setter 嵌套(而且都返回原始值),那麼咱們是否是能夠在無任何依賴狀況下用 state 作任何想作的事情呢(代碼可讀性忽略)

const trigger = useCallback(() => {
    setState1(state1 => {
        setState2(state2 => {
            console.log(state1 + state2);
            return state2;
        })
        return state1;
    })
});
複製代碼

利用 useReducer 和 setState 結合處理獲取 state 和 props 的問題 由於上面的方法,咱們只能確保咱們能夠無依賴的拿到 state ,可是咱們卻不能在無依賴的狀況下拿到 props 那麼咱們能夠怎麼辦呢。 咱們可能把 useReducer 的 reducer 放在 function component 函數體內,利用 dispatch 最終觸發的是最新的閉包中的 reducer 來確保咱們能夠拿處處於最新狀態的 props

function App({ a, b, c }) {
    const reducer = (state, action) => {
        switch(action.type) {
            case 'init':
                // 這裏永遠能夠拿到最新的 a
                return Object.assign(state, { a: a });
            default:
                return state;
        }
    }
    const [state, dispatch] = useReducer(reducer, {});
    return (
        <div>{ state.a }</div>
    )
}
複製代碼

減小Render

咱們能夠作相似於 class component 中的 PureComponent 這樣的操做,咱們能夠用 React.memo 包裹大部分的組件(會帶來額外的比較,性能不必定是最佳的)

直接使用 React.memo

利用 React.memo,咱們能夠作到讓 React 對咱們的組件進行淺比較,

const Child = function({ a, b, c }) {
    return <div>{a}{b}{c}</div>
} 
export default React.memo(Child);
複製代碼

使用 useMemo 進行細粒度的控制

function App({ a, b, c }) {
    const RenderComponent = useMemo(() => {
        return <div>{c}</div>
    }, [c]);
    return (
        <RenderComponent /> ) } 複製代碼

使用 useCallback 包裹函數,使函數變化減小

在這裏,可使用我上面所介紹的 trick ,減小依賴的數量,從而減小 rerender 的次數 Eg: trigger.jsx 開關

const useTrigger = () => {
    const [state, setState] = useState(false);
    const trigger = useCallback(() => {
        setState(ste => !ste);
    }, []);
    return { state, trigger };
}

// vs

const useTrigger = () => {
    const [state, setState] = useState(false);
    const trigger = useCallback(() => {
        setState(ste => !ste);
    }, [state]);
    return { state, trigger };
}
複製代碼

Hooks 實踐

談談表單

像雙向數據綁定那樣編寫表單

const useInput = () => {
  const [value, setValue] = useState('');
  const onChange = val => {
    setValue(val.target.value);
  };
  return {
    value,
    onChange
  };
};
複製代碼

表單提交

export const useSubmit = submitFunction => {
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const [res, setRes] = useState(null);

  const trigger = useCallback(() => {
    try {
      if (_.isFunction(submitFunction)) {
        (async () => {
          let res = await submitFunction();
          if (res) {
            setRes(res);
          }
        })();
      }
    } catch (e) {
      setError(e);
    } finally {
      setLoading(true);
    }
  }, [submitFunction]);

  return [loading, res, error];
};
複製代碼

利用 useMemo 操做 props 數據

不少時候,咱們都會依賴於 props 去計算咱們的 state,在 class component 中給咱們提供了 getDerivedStateFromProps 生命週期供咱們去作相似的操做,可是在 hooks 裏,咱們並無這樣的生命週期的概念,那咱們應該如何去作呢?

咱們能夠利用 useMemo 去進行對 props 的計算操做,經過正確處理依賴,就能夠籍由 useMemo 的記憶特性,讓咱們以最小的成本去正確的更新 state (高成本的方案是每一次去計算將值賦給閉包中的普通變量)。

import React, { useMemo } from 'react';

function App({ data }) {
    // 只有 data 更新時從新計算
    const info = useMemo(() => {
        // 對 data 進行一系列的計算操做
        return newData;
    }, [data]);
}
複製代碼

利用 hooks 返回組件

以前所說的大都是利用 hooks 去處理邏輯問題,那麼 hooks 是否能夠像是高階組件那樣,爲咱們返回一個組件呢,答案是能夠的,而且利用這樣的能力,咱們還能夠簡化不少狀況下咱們的編程。

import React, { useState, useCallback } from 'react';
import { Modal } from 'antd';

export default function useModal() {
  const [show, setShow] = useState<boolean>(false);

  const openModal = useCallback(() => {
    setShow(true);
  }, []);

  const closeModal = useCallback(() => {
    setShow(false);
  }, []);

  const CusModal: React.SFC = ({ children, ...props }) => {
    return (
      <Modal visible={show} {...props}> {children} </Modal>
    )
  }

  return {
    show,
    setShow,
    openModal,
    closeModal,
    CusModal
  }
}
複製代碼

利用 ref hooks 進行一些無侵入操做(react 官方 不推薦) 由於 ref 能夠拿到原始 dom,咱們能夠利用這個特性作一些操做,例如說侵入代碼性的埋點遷移至 ref(減小對原始代碼侵入)

eg:利用 ref 記錄停留時間(能夠作無侵入埋點)

export const useHoverTime = eventName => {
  const EV = `${ eventName}`;
  const ref = useRef(null);

  useEffect(() => {
    localStorage.setItem(EV, 0);
    return () => {
      const time = localStorage.getItem(EV);
      // do something 
      localStorage.setItem(EV, null);
    };
  }, []);

  useEffect(() => {
    let startTime = null;
    let endTime = null;
    const overHandler = () => {
      startTime = new Date();
    };
    const outHandler = () => {
      endTime = new Date();
      localStorage.setItem(
        EV,
        parseInt(localStorage.getItem(EV)) +
        parseInt(endTime - startTime)
      );
      startTime = 0;
      endTime = 0;
    };
    if (ref.current) {
      ref.current.addEventListener('mouseover', overHandler);
      ref.current.addEventListener('mouseout', outHandler);
    }
    return () => {
      if (ref.current) {
        ref.current.removeEventListener('mouseover', overHandler);
        ref.current.removeEventListener('mouseout', outHandler);
      }
    };
  }, [ref]);
  return ref;
};
複製代碼

React-hook-form 利用 ref 進行的表單的註冊和提交攔截(我的認爲也是一種很是清奇的思路)

Hooks with Immer.js

immutable.js 的使用複雜度是很是高的,可是有時候咱們又但願咱們的 React App 性能更好,節省沒必要要的 rerender,那麼 Immer.js 就是一個很是好的選擇(事實上dva也使用了immer做爲底層庫)

咱們能夠在使用 useReducer 的時候,使用 Immer 進行狀態的變動,從而使得咱們最新的 state 是 immutable 的。

const reducer = (state, action) => {
  switch (action.type) {
    case 'initData':
      return produce(state, draft => {
        draft.data = action.data;
      });
複製代碼

讓 useReducer 用上 Redux 中間件生態

從何種角度看,useReducer + useContext + Context 的組合都在作傳統 Redux 所在作的事情,那麼,有沒有可能讓咱們的原生 hooks 使用上 Redux 的中間件呢(本質上劫持了 action ,與 Redux 的 Api 無關)?! 是能夠的,事實上,這裏至關於把 Redux 中間件的實現遷移到了 hooks 上,咱們固然能夠本身實現,可是 react-use 這個庫裏幫咱們作了集成,咱們能夠方便的直接使用它。

// 建立加強了中間件的 reducer , 這裏的例子增長了 redux-logger 與 redux-thunk
const useLoggerReducer = createReducer(logger, thunk);

export default function App() {
  const [state, dispatch] = useLoggerReducer(reducer, initState);
複製代碼

這樣子,咱們即可以利用 redux-thunk、redux-saga 等中間件進行異步任務的處理,使用 redux-logger 進行 action 的打印和先後 state 的 diff。

打造本身的 combindReducer (代碼思路來源於 Middle)

const combineReducers = (reducers) => {
    const keys = Object.keys(reducers);
    const initObj = {};
    keys.forEach(key => {
        let draftState = reducers[key](undefined, { type: '' });
        if (!draftState) {
            draftState = {};
            console.warn(
                `[Error]: 在combineReducers 中 Reducer 須要初始化!`
            );
        }
        initObj[key] = draftState;
    })
    return (state, action) => {
        keys.forEach(key => {
            const prevState = initObj[key];
            initObj[key] = reducers[key](prevState, action);
        });
        return { ...initObj };
    }
}
複製代碼

將它和咱們的加強後的 useReducer 結合起來,咱們便擁有了一個幾乎能夠媲美 redux 的 reducer。

Hooks 第三方工具集合 react-use

多是目前社區中得到 star 和關注最多的自定義 hooks 項目,提供了很是多的自定義 hooks(不少是香的) react-use

Hooks 請求工具 swr

在 React hooks 雨後春筍般的請求庫中,最爲亮眼的當屬於 swr。詳情請見官方 github 倉庫 swr

Hooks 第三方表單庫 react-hook-form

一個很好用的 react-hook-form 表單庫。詳情請見官方 github 倉庫 react-hook-form

相關文章
相關標籤/搜索